This page defines how normalize-docs turns one doc-source.json into generator-ready.json: content, view,
document block, envelope, warnings and lineage. Developers who change the command, and readers of its output, read it.
Read before: Core · Next: normalize-tests
living-doc-service-normalize-docs turns one doc-source.json, as a collector wrote it, into generator-ready.json,
the file a generator reads.
| Module | What it holds |
|---|---|
living_doc_service_normalize_docs.builder |
build_generator_ready(documentation, *, view, generated_at, title, version): the input's contract model to the output's. It reads and writes no file. |
living_doc_service_normalize_docs.lineage |
LINEAGE_TABLE, the command's lineage table |
living_doc_service_normalize_docs.service |
normalize_docs(input_path, output_path, *, view, document_title, document_version), the command, and check_no_field_loss(result) |
- Both files are
living-doc-utilitiescontracts,doc-source-v1.0.0andgenerator-ready-v1.0.0; this package keeps no copy of either - The CLI registers it as
living-doc normalize-docs, and the action runs it ascommand: normalize-docs→ CLI, Commands
A run takes these steps; nothing is written when one fails:
- The input goes through the shared input validation,
validate_inputs("normalize-docs", …), and nothing else reads it (Shared input validation). A failing step stops the run before anything is built. - The builder builds the output from the validated model, and the command sets its
metadata.runto the CI run. check_no_field_losscompares the lineage table's paths (Lineage); a lost field stops the run.write_artifactvalidates the output, then writes it.
The builder concatenates doc-source's three entity lists into content.entities[], in this order:
user_stories[];features[], each with its derivedstateand itsstub_reason;functionalities[].
- The three lists share one entity model with
content.entities[], so no field is translated. - Each list keeps its input order. This is the order the canon groups entity types in, User Stories, Features, Functionalities, and it stays stable.
- Every authored field is carried at its authored level, as the rendering page's § Records and presentation requires → rendering
--view selects the records the output holds through the core view filter, once per list:
releasedrops the records the canon's release view drops, a Feature by its derived state, and keepsdeprecatedones.document.selection_summarysums the three lists' counts; its acceptance-criteria counts cover kept entities only
| Field | Value |
|---|---|
document.view |
--view, inner by default |
document.title |
--document-title, otherwise the input's metadata.source.project_id |
document.version |
--document-version, otherwise 1.0.0 |
document.selection_summary |
what the view kept and dropped |
- The default version is
1.0.0 - The action passes no document version: a real one needs an input and a source of truth, which come later
The fields normalize-docs fills its own way; every command fills the rest the same way, as the core's Shared envelope says:
| Field | Value |
|---|---|
metadata.source |
the input's, copied |
metadata.source_inputs[] |
one entry, the input: its schema_version, producer, run and source; stats, the input's own metadata.stats; selected_stats, the same counts over the records the view kept |
- The four counts a caller reports,
sources_configured,sources_failed,unresolved_refsandentities_skipped, are the input's insource_inputs[0].stats - The input's own run stays in
source_inputs[0].run
Every input warning reaches the output's warnings[] through the core's forwarded, in input order:
- It is forwarded in the release view too, also when it names an entity the view dropped.
- Its
contextstarts withsource_input='doc-source-v1.0.0'.
LINEAGE_TABLE is a living_doc_utilities.contracts.lineage.LineageTable. It maps each of doc-source-v1.0.0's 162
field paths to its generator-ready-v1.0.0 path, and drops none.
| Input path | Output path |
|---|---|
user_stories[].<field> |
content.entities[].<field> |
features[].<field> |
content.entities[].<field> |
functionalities[].<field> |
content.entities[].<field> |
<field>is each of the 54 entity paths, fromentity_idtoacceptance_criteria[].placeholder_valuesandrationale- The table is declared here, not read from the contract
- Why: a field a later contract version adds fails
assert_complete(LINEAGE_TABLE, "doc-source-v1.0.0")in this package's tests until the table maps it.
- Why: a field a later contract version adds fails
- The command never calls
assert_complete, so a field the table does not know never stops a run - At run time,
check_no_field_losscomputes the output's stats aswrite_artifactwill, and runscheck_field_lossagainstsource_inputs[0].selected_stats; it compares the table's mapped paths only- A mapped path with data in the kept records and none in the output is
FIELD_LOSS, and the run stops. - A record the view drops is not a loss:
selected_statscounts kept records only. - Three input paths map to each output path. A field lost from one list while another still fills it is a partial drop; R11 does not stop it at run time.
- A mapped path with data in the kept records and none in the output is
- The canon's rules are the component checks: R11, and R12 checks 2 and 3 → component checks
- R12 check 3:
full_sample("doc-source-v1.0.0"), unfiltered, is written withwrite_artifactand run through the command, the shared input validation included, in both views; the release view's expected counts are derived from the sample - The command over the repository's fixture, against the goldens in
tests/expected/ - Each named authored field, and
living-doc-utilities' six golden entities (tests/fixtures/utilities-golden/) compared field by field