feat(inbox): Door B derives structure and reprojects the index additively

Door B takes a profile (keyword-only, DEFAULT) and, under a profile carrying
facets, derives each dropped document's title, number, hierarchy and
cross-references, writes them into the concept's own frontmatter, and projects
them into the index entry.

The additive requirement is answered by one decision rather than by an
algorithm: the index is a PROJECTION of the concept files, recomputed from the
whole bundle each round. Nothing is diffed, so the three invariants hold by
construction -- rebuild-from-scratch equals incremental byte for byte,
re-dropping a document replaces its entry instead of doubling it, and a
relation formed in round 3 UPDATES the round-1 entry it is about, which an
append-only index could never do.

An unresolved pointer is marked '?' in the entry rather than omitted: during
build-up, pointing at something not dropped yet is normal, and the dangerous
version is the one that leaves no trace. Facet values are validated per file
BEFORE the write, so a producer value that breaks the grammar fails that file
and not the run.

DEFAULT is byte-identical with and without the new parameter, and is asserted
so. Door B keeps writing the literal 'generated: true' rather than the
profile's ownership stamp -- routing it through the profile would move
DEFAULT's bytes and orphan every bundle this door has already written; that is
a separate question and answering it here would have answered it silently.

18 new tests; suite 677 -> 695.
This commit is contained in:
Kjell Tore Guttormsen 2026-08-27 00:37:12 +02:00
commit 7ce548c09b
3 changed files with 566 additions and 16 deletions

View file

@ -30,7 +30,7 @@ from __future__ import annotations
import re
import unicodedata
from collections.abc import Mapping
from collections.abc import Mapping, Sequence
from dataclasses import dataclass, field
from pathlib import Path
@ -427,3 +427,119 @@ def _derived_supersession(
)
)
return proposed
# --- projection to and from frontmatter -----------------------------------
# The order `derived` lists its members in. Fixed rather than sorted so the
# marker reads in the order the fields are established (a title before the
# number read out of it), and so two runs over the same document emit the same
# bytes.
_DERIVED_ORDER = ("title", "number", "parent", "references", "supersedes")
def _render_flow_list(items: Sequence[str]) -> str:
# Flow form, never block: this library's parser round-trips a flow sequence
# as an opaque value and cannot read a block one at all, so a value we can
# write is a value we can read back.
return f"[{', '.join(items)}]"
def structure_frontmatter(document: DocumentStructure, keys: Sequence[str]) -> dict[str, str]:
"""The structure keys a concept file carries, for the keys a profile names.
Written into the CONCEPT rather than only into the index, because the index
is a projection: a later round rebuilds it by reading these keys back, and
a fact that lived only in the index would be lost the moment the index was
reprojected.
"""
values: dict[str, str] = {}
for key in keys:
if key == "derived":
continue
if key == "number" and document.number:
values[key] = document.number
elif key == "parent" and document.parent_number:
values[key] = document.parent_number
elif key == "version" and document.version:
values[key] = document.version
elif key == "references" and document.references:
values[key] = _render_flow_list(document.references)
elif key == "supersedes" and document.supersedes:
values[key] = _render_flow_list(document.supersedes)
elif key in document.declared:
values[key] = document.declared[key]
marked = [field_name for field_name in _DERIVED_ORDER if field_name in document.derived]
if marked and "derived" in keys:
values["derived"] = _render_flow_list(marked)
return values
def structure_from_frontmatter(values: Mapping[str, str]) -> DocumentStructure:
"""A concept file's stored frontmatter read back as its structure.
The inverse of :func:`structure_frontmatter` over the keys it writes, plus
the door's own `title` and `source_file`. Reading the STORED keys rather
than re-deriving from the body is what makes a reprojection cheap and, more
importantly, stable: re-derivation would make a bundle's index depend on
the version of this library that last touched it.
"""
return DocumentStructure(
title=values.get("title", ""),
source_file=values.get("source_file", ""),
number=values.get("number"),
parent_number=values.get("parent"),
version=values.get("version"),
references=_parse_flow_list(values["references"]) if "references" in values else (),
supersedes=_parse_flow_list(values["supersedes"]) if "supersedes" in values else (),
declared=dict(values),
derived=frozenset(_parse_flow_list(values["derived"]) if "derived" in values else ()),
)
# The suffix a subject wears when nothing in the bundle answers to it. A
# pointer that is merely absent from the index is indistinguishable from one
# that was never made -- and an absence that does not scream is the most
# dangerous state this repo knows.
UNRESOLVED_MARKER = "?"
def facet_values(name: str, bundle: BundleStructure, keys: Sequence[str]) -> dict[str, str]:
"""One document's facets, as an index entry carries them.
Relations are rendered as their SUBJECTS, each suffixed with
:data:`UNRESOLVED_MARKER` when the bundle holds nothing answering to it, so
an index reader sees the difference between "points at N200" and "points at
an N200 that is not here". `derived` gathers the document's own inferred
fields plus any relation this library proposed rather than read.
"""
document = bundle.documents[name]
values = structure_frontmatter(document, [key for key in keys if key != "derived"])
marked = {field_name for field_name in document.derived}
edges = [edge for edge in bundle.edges if edge.source == name]
for kind, key in (
("parent", "parent"),
("references", "references"),
("supersedes", "supersedes"),
):
if key not in keys:
continue
subjects = [
edge.subject + (UNRESOLVED_MARKER if edge.target is None else "")
for edge in edges
if edge.kind == kind
]
if any(edge.kind == kind and edge.derived for edge in edges):
marked.add(key)
if not subjects:
values.pop(key, None)
elif kind == "parent":
values[key] = subjects[0]
else:
values[key] = _render_flow_list(subjects)
listed = [field_name for field_name in _DERIVED_ORDER if field_name in marked]
if listed and "derived" in keys:
values["derived"] = _render_flow_list(listed)
return values