feat(frontmatter): --frontmatter KEY=VALUE stamps a key on every concept of a run
K3-19 b. `okf build --frontmatter KEY=VALUE`, repeatable, split on the FIRST '=' (`cli.frontmatter_from_flags`) because a publisher's address carries '=' itself. The value is written verbatim on ONE line: the block form of a `sources` list is invisible to this package's line-oriented readers, so the flow form is the only one that survives them. Also reachable as `build(frontmatter=...)`, `measure(concept_frontmatter_values=...)`, `process_inbox(concept_frontmatter_values=...)` and `render_inbox_concept(concept_frontmatter_values=...)`, keyword-only with defaults, so every existing call site is source-compatible. Precedence: a stated value beats what the document declares, which beats the file name. A run may ADD any key and REPLACE only `sources` and `description` -- the two with a derived layer below the flag. Every other key the door writes is refused by `inbox.validate_concept_frontmatter` before a proposal is written (`run_frontmatter_invalid`): measured from the bytes, owned by another argument, the ownership stamp a later run reads back (including Door A's `ingest_manifest`, which would make that door claim a Door B file), or a derived facet whose `derived` marker would go on naming a replaced value. A value that would not read back as stated -- empty, multi-line, or padded, since `parse_frontmatter` strips -- is refused too. SPEC SS 4.1 "Extensions" lets a producer add any key and SS 11 forbids a consumer to reject one. Without the flag nothing moves: a test holds the flagged tree to the plain one minus exactly the stated line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
72617acc7a
commit
912b85026b
5 changed files with 171 additions and 2 deletions
|
|
@ -70,14 +70,14 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import Sequence
|
||||
from collections.abc import Mapping, Sequence
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
from .corpus import LOG_NAME, CorpusReport, load_plans, measure
|
||||
from .errors import IngestError
|
||||
from .extract import declared_identity
|
||||
from .inbox import walk_inbox
|
||||
from .inbox import validate_concept_frontmatter, walk_inbox
|
||||
from .materialize import reduce_to_id_grammar
|
||||
from .profiles import SEGMENTED_OKF_V0_2, STRUCTURED_V1, BundleProfile
|
||||
from .propose import ProposerError, heading_reserve_applies
|
||||
|
|
@ -251,6 +251,32 @@ DEFAULT_PDF_OUTLINE = False
|
|||
DEFAULT_STAMP = "1970-01-01T00:00:00Z"
|
||||
|
||||
|
||||
def frontmatter_from_flags(pairs: Sequence[str]) -> dict[str, str]:
|
||||
"""`--frontmatter KEY=VALUE`, split on the FIRST `=` and only there.
|
||||
|
||||
The first `=` because the value is the one that needs the rest: a
|
||||
publisher's address carries `?languageCode=nb`, and a split on every `=`
|
||||
would cut the `sources` flow mapping in half. Key and value are validated
|
||||
by the door (`validate_concept_frontmatter`); this only refuses what is not
|
||||
a pair, and a key named twice -- which of two values was meant is a guess.
|
||||
"""
|
||||
values: dict[str, str] = {}
|
||||
for pair in pairs:
|
||||
key, sep, value = pair.partition("=")
|
||||
if not sep:
|
||||
raise IngestError(
|
||||
f"--frontmatter {pair!r} has no '='; the form is KEY=VALUE",
|
||||
code="run_frontmatter_invalid",
|
||||
)
|
||||
if key in values:
|
||||
raise IngestError(
|
||||
f"--frontmatter names {key!r} twice; refusing to pick one of the two values",
|
||||
code="run_frontmatter_invalid",
|
||||
)
|
||||
values[key] = value
|
||||
return values
|
||||
|
||||
|
||||
def _document_prefixes(inbox: Path, walked: Sequence[Path]) -> dict[Path, str]:
|
||||
"""Each document's directory: the name it declares, else its file name.
|
||||
|
||||
|
|
@ -405,6 +431,7 @@ def build(
|
|||
pdf_headings_reserve: bool = DEFAULT_PDF_HEADINGS_RESERVE,
|
||||
ocr: bool = DEFAULT_OCR,
|
||||
pdf_outline: bool = DEFAULT_PDF_OUTLINE,
|
||||
frontmatter: Mapping[str, str] | None = None,
|
||||
) -> CorpusReport:
|
||||
"""Folder in, bundle out. The whole command, minus argument parsing.
|
||||
|
||||
|
|
@ -422,6 +449,11 @@ def build(
|
|||
"""
|
||||
if proposed_at is None:
|
||||
proposed_at = ingested_at
|
||||
# Refused HERE, before one proposal is written: a stated key that is wrong
|
||||
# is wrong for every document, and the proposal pass is the long half.
|
||||
concept_values = validate_concept_frontmatter(
|
||||
frontmatter or {}, profile=SEGMENTED_OKF_V0_2 if segments else STRUCTURED_V1
|
||||
)
|
||||
# Bound to THIS run's outline minimum, once, so the proposer and the door
|
||||
# cannot be handed two different thresholds for the same question.
|
||||
reserve = (
|
||||
|
|
@ -438,6 +470,7 @@ def build(
|
|||
pdf_headings=pdf_headings,
|
||||
heading_reserve=reserve,
|
||||
ocr=ocr,
|
||||
concept_frontmatter_values=concept_values,
|
||||
)
|
||||
_write_log(bundle, report, profile=STRUCTURED_V1)
|
||||
return report
|
||||
|
|
@ -495,6 +528,7 @@ def build(
|
|||
pdf_headings=pdf_headings,
|
||||
heading_reserve=reserve,
|
||||
ocr=ocr,
|
||||
concept_frontmatter_values=concept_values,
|
||||
)
|
||||
_write_log(bundle, report, profile=SEGMENTED_OKF_V0_2)
|
||||
return report
|
||||
|
|
@ -584,6 +618,20 @@ def parse_args(argv: list[str] | None) -> argparse.Namespace:
|
|||
build_parser.add_argument(
|
||||
"--okf-type", default="reference", help="okf_type for every concept and proposal"
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--frontmatter",
|
||||
action="append",
|
||||
default=None,
|
||||
metavar="KEY=VALUE",
|
||||
help=(
|
||||
"stamp KEY: VALUE on every concept of this run; repeatable. Split on "
|
||||
"the FIRST '=' and written verbatim on ONE line, so a flow mapping such "
|
||||
"as 'sources=[{ resource: <url>, title: <t> }]' survives whole. Adds "
|
||||
"any key, and REPLACES only sources and description -- the two with "
|
||||
"a layer below them (the document's own identity, else the file "
|
||||
"name). Every other key the door writes itself is refused"
|
||||
),
|
||||
)
|
||||
build_parser.add_argument(
|
||||
"--plans-dir",
|
||||
type=Path,
|
||||
|
|
@ -934,6 +982,7 @@ def main(argv: list[str] | None = None) -> int:
|
|||
pdf_headings_reserve=args.pdf_headings == "font-reserve",
|
||||
ocr=args.ocr,
|
||||
pdf_outline=args.pdf_outline,
|
||||
frontmatter=frontmatter_from_flags(args.frontmatter or ()),
|
||||
)
|
||||
except (IngestError, OSError, ValueError) as exc:
|
||||
print(f"{CLI_ID}: FAILED - {exc}", file=sys.stderr)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue