feat(identity): an STS document's doc-number names its directory and its title the address
K3-19 a. `extract.declared_identity` reads what a NISO-STS document states about itself -- exactly one <std-ident> (<doc-number>, <year>) and exactly one <title-wrap> (<full>, else <main>) -- and returns None for every other row, for XML that is not STS, for an unparseable file, and for a document that states neither. A value stated more than once is not read: an adopted standard carries one <std-ident> per issuing body, and picking one is a guess. `okf build` names a document's directory from its <doc-number> through the id grammar, replacing only the file's stem. A declared name another document in the run also claims falls back to the file name for both, said on stderr: the existing collision gate would refuse both with "rename one", and a name read from inside a document is not one a rename can change. `sources[0].title` becomes <doc-number> + <year>, then the <title-wrap> title, then the file name -- the first that survives the gate and can be written into the flow mapping verbatim. Measured on R761, <full> carries a comma, which ends a flow mapping, so it is never the title there; it is never cleaned up either. `resource` stays the inbox-relative file. Every other row, and every profile without an address, is untouched: the identity is asked for only where `sources` is written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
be169eeca0
commit
ee8d5b5776
6 changed files with 230 additions and 13 deletions
|
|
@ -70,12 +70,15 @@ from __future__ import annotations
|
|||
import argparse
|
||||
import sys
|
||||
import tempfile
|
||||
from collections.abc import 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 .materialize import reduce_to_id_grammar
|
||||
from .profiles import SEGMENTED_OKF_V0_2, STRUCTURED_V1, BundleProfile
|
||||
from .propose import ProposerError, heading_reserve_applies
|
||||
from .propose import run as propose_run
|
||||
|
|
@ -248,6 +251,60 @@ DEFAULT_PDF_OUTLINE = False
|
|||
DEFAULT_STAMP = "1970-01-01T00:00:00Z"
|
||||
|
||||
|
||||
def _document_prefixes(inbox: Path, walked: Sequence[Path]) -> dict[Path, str]:
|
||||
"""Each document's directory: the name it declares, else its file name.
|
||||
|
||||
MEASURED: a NISO-STS delivery landed every one of its 2 761 concepts under
|
||||
a directory named for the delivery path's file name, a UUID occurring 0
|
||||
times in the document, while the document's own `<doc-number>` said what it
|
||||
was. Only the file's STEM is replaced; the folders above it are the
|
||||
operator's arrangement and stay.
|
||||
|
||||
A declared name another document in this run also claims -- by declaring
|
||||
it, or by its file name reducing to it -- is not taken by either, and both
|
||||
keep their file name. The gate Door B already has would refuse both and
|
||||
tell the operator to rename one, and a name read from inside a document is
|
||||
not one a rename can change. Said on stderr rather than silently, because a
|
||||
directory that stays a UUID is otherwise indistinguishable from this rule
|
||||
never having run.
|
||||
"""
|
||||
|
||||
def scope(prefix: str) -> str:
|
||||
return "/".join(reduce_to_id_grammar(part) for part in prefix.split("/"))
|
||||
|
||||
named = {source: source.relative_to(inbox).with_suffix("").as_posix() for source in walked}
|
||||
declared: dict[Path, tuple[str, str]] = {}
|
||||
for source in walked:
|
||||
try:
|
||||
identity = declared_identity(source.name, source.read_bytes())
|
||||
except OSError:
|
||||
# The proposer reads the same file next and reports it per file.
|
||||
continue
|
||||
if identity is None or identity.doc_number is None:
|
||||
continue
|
||||
slug = reduce_to_id_grammar(identity.doc_number)
|
||||
if slug:
|
||||
parent = source.relative_to(inbox).parent
|
||||
declared[source] = ((parent / slug).as_posix(), identity.doc_number)
|
||||
claims: dict[str, set[Path]] = {}
|
||||
for source in walked:
|
||||
claims.setdefault(scope(named[source]), set()).add(source)
|
||||
for source, (prefix, _) in declared.items():
|
||||
claims.setdefault(scope(prefix), set()).add(source)
|
||||
prefixes = dict(named)
|
||||
for source, (prefix, doc_number) in declared.items():
|
||||
others = sorted(named[other] for other in claims[scope(prefix)] if other != source)
|
||||
if others:
|
||||
print(
|
||||
f"{CLI_ID}: {named[source]}: <doc-number> {doc_number!r} names {prefix!r}, "
|
||||
f"which {', '.join(others)} also claims; both keep their file name",
|
||||
file=sys.stderr,
|
||||
)
|
||||
continue
|
||||
prefixes[source] = prefix
|
||||
return prefixes
|
||||
|
||||
|
||||
def _propose_plans(
|
||||
inbox: Path,
|
||||
bundle: Path,
|
||||
|
|
@ -284,6 +341,7 @@ def _propose_plans(
|
|||
corpora the two-script path completes.
|
||||
"""
|
||||
walked, _ = walk_inbox(inbox, exclude=bundle)
|
||||
prefixes = _document_prefixes(inbox, walked)
|
||||
written = nothing = failed = 0
|
||||
for position, source in enumerate(walked, start=1):
|
||||
relative = source.relative_to(inbox)
|
||||
|
|
@ -293,7 +351,7 @@ def _propose_plans(
|
|||
plans_dir / f"{position:02d}.json",
|
||||
okf_type=okf_type,
|
||||
proposed_at=proposed_at,
|
||||
path_prefix=relative.with_suffix("").as_posix(),
|
||||
path_prefix=prefixes[source],
|
||||
outline_run=outline_run,
|
||||
table_grid=table_grid,
|
||||
unit_fold=unit_fold,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue