oh-my-opencode 5.1.47 → 5.1.48

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/cli/index.js +15 -15
  2. package/dist/cli-node/index.js +15 -15
  3. package/dist/index.js +14 -14
  4. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  5. package/dist/skills/ulw-research/SKILL.md +58 -1
  6. package/dist/skills/ulw-research/references/deliverable-phase.md +51 -9
  7. package/dist/skills/ulw-research/references/report-gates.md +2 -1
  8. package/dist/skills/ulw-research/scripts/contracts.mjs +3 -11
  9. package/dist/skills/ulw-research/scripts/contracts.test.ts +1 -0
  10. package/dist/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  11. package/dist/skills/ulw-research/scripts/copy-returns-polish.test.ts +6 -6
  12. package/dist/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  13. package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +14 -22
  14. package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +7 -2
  15. package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +18 -4
  16. package/dist/skills/ulw-research/scripts/fact-commands.mjs +6 -47
  17. package/dist/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  18. package/dist/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  19. package/dist/skills/ulw-research/scripts/report-tools.test.ts +245 -4
  20. package/dist/skills/ulw-research/scripts/source-commands.mjs +8 -3
  21. package/dist/tui.js +14 -14
  22. package/package.json +14 -14
  23. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  24. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  29. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  30. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  31. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  32. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  33. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  34. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  35. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  36. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  37. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  38. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  39. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  40. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  41. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  46. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  51. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  52. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  53. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  54. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  55. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  56. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  57. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  58. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  59. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  60. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  61. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  62. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  63. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  68. package/packages/omo-codex/plugin/package-lock.json +12 -12
  69. package/packages/omo-codex/plugin/package.json +1 -1
  70. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  71. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +58 -1
  72. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +51 -9
  73. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +2 -1
  74. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +3 -11
  75. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  76. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +14 -22
  77. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +7 -2
  78. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +18 -4
  79. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +6 -47
  80. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  81. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  82. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +8 -3
  83. package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
  84. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  85. package/packages/shared-skills/skills/ulw-research/SKILL.md +58 -1
  86. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +51 -9
  87. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +2 -1
  88. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +3 -11
  89. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +1 -0
  90. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  91. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +6 -6
  92. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  93. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +14 -22
  94. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +7 -2
  95. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +18 -4
  96. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +6 -47
  97. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  98. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  99. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +245 -4
  100. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +8 -3
@@ -54,7 +54,7 @@ QA tiers: **light** runs the static gates and one render check of the answer, an
54
54
 
55
55
  ## 4. The empty-info interview
56
56
 
57
- The interview has exactly three questions, asked together through the harness question tool without waiting for the answer. A question is **skipped** when the request, the destination, a pointed-at document, or a declared memory episode already answers it; when all three are answered, nothing is asked.
57
+ The interview has three format questions, asked together through the harness question tool without waiting for the answer; the fidelity questions (below) ride in the same call only when their triggers fire. A question is **skipped** when the request, the destination, a pointed-at document, or a declared memory episode already answers it; when all three are answered, nothing is asked.
58
58
 
59
59
  - **Q1 - destination and format.** Options in this order: the memory hit (the choice recorded for the most similar prior context, section 5) when there is one; the derived default from section 3; "describe the format in your own words" (free text, which becomes the brief for the design spec); "don't care, you decide" (resolves to the derived default).
60
60
  - **Q2 - audience and length.** Who reads it, and a length band: `short` (a page or a post), `standard` (a report of about 5 to 12 pages), or `deep` (a full dossier). The derived default is first.
@@ -76,6 +76,48 @@ format description: none
76
76
 
77
77
  `answered_by` is one of `user`, `default`, `request`. Run `outcome init` (section 9) in the same step, with the formats as the promised list.
78
78
 
79
+ ### The fidelity fields and the lock
80
+
81
+ The values below are read by `scripts/fidelity-policy.mjs`, the single registry: `SOURCE_FLOOR_RULES` and `parseSourceFloor()` for the `source floor` forms (`pointedFlag` in source registration reads the same grammar), and `FIDELITY_VALUE_RULES` for `derived numbers`, `languages`, `fallback`, and `meaning check`. `derived numbers` and `fallback` accept only the values in their row below (`FIDELITY_VALUE_RULES.tables`, any case); any other value, a free-text answer included, is a usage error (exit `2`) naming the field and the value, as an unknown `source floor` form is. Map a free-text answer onto a table value before writing it: a partial allowance ("growth rates yes, estimates no") records `derived numbers: none`.
82
+
83
+ Beside `## Deliverable`, the brief records what the report must stay faithful to, under `## Fidelity`. The fields are inferred first; only `as of`, `meaning check`, `source floor`, and `derived numbers` may be asked, each on its own trigger, in the same question call as Q1-Q3 (at most four questions in that call). The inference rules, the triggers, and the question texts are in `SKILL.md`.
84
+
85
+ | Field | Values | Default | Read by |
86
+ | --- | --- | --- | --- |
87
+ | `as of` | `latest at run start, <YYYY-MM-DD>`; one anchor (version, release, tag, commit, date, period); `multiple: <anchor>, <anchor>; rows tagged` | `latest at run start, <run date>` | `fact verify` (`quote_stale_version` when a quoted source's revision is not the anchor) |
88
+ | `meaning check` | `on`, `off`, `on; force strong: <scope>` | `on` in the full tier, `off` in the light tier | `fact pairs` (`off` writes `{"skipped": true}`) and the orchestrator's reader lanes |
89
+ | `source floor` | `standard`, `primary only`, `pointed only: <list>`, `classes: <list>` | `standard` | `fact fill` and `fact verify` (`source_outside_floor`) |
90
+ | `derived numbers` | `allowed`, `none` | `allowed` | `fact fill` and `fact verify` (`derived_not_allowed`) |
91
+ | `languages` | `report <lang>; sources <langs>; quotes original`, or `...; quotes original with translation beside` | report in the requester's language; quotes with translation when a source language differs | `fact fill` (whether a translation binds to its quote) |
92
+ | `identifier classes` | `none`, or a list of token classes | from the domain of the Phase 0 axes | recorded only: no gate enforces it yet |
93
+ | `fallback` | `block-quote`, `omit` | `block-quote` | `fact fill --replace-failed` |
94
+
95
+ ```text
96
+ ## Fidelity
97
+ as of: latest at run start, 2026-10-23 (answered_by: default)
98
+ meaning check: on (answered_by: default)
99
+ source floor: standard (answered_by: default)
100
+ derived numbers: pending (answered_by: pending)
101
+ languages: report en; sources en; quotes original (answered_by: default)
102
+ identifier classes: version strings, package names, commit ids (answered_by: default)
103
+ fallback: block-quote (answered_by: default)
104
+ asked: derived numbers
105
+ ```
106
+
107
+ Each line is `<field>: <value> (answered_by: user|default|request|pending)`; values carry no parentheses, `pending` goes only with `answered_by: pending`, and `asked:` is an optional history line. A malformed line or a second block is a usage error (exit `2`) naming the line.
108
+
109
+ **The lock.** The brief is locked when all seven fields are present and none is `pending`. Six stages refuse an unlocked brief with one `fidelity_interview_incomplete` defect (blocker, integrity), exit `1`, and write nothing: `fact fill`, `fact verify`, `fact pairs`, `fact meaning-check`, `fact polish-apply`, `fact polish-diff`. Each reads `--brief`, default `$SESSION_DIR/brief.md`. Collection never waits: `source snapshot`, `fact add`, `fact quote`, `fact derive`, `fact retract`, `fact export`, and `claims index` never read the lock. Provisional defaults are written at brief time, so the lock fires only on a `pending` field still open when the assembly lane is about to start; that one question is then asked again, blocking.
110
+
111
+ **What fill and verify enforce.** At every `fact fill`, each placeholder's row is checked against the current lock, and `fact verify` checks every `data-fact` element of the edition against it again, so tightening the brief after fill fails the next verify:
112
+
113
+ - `derived numbers: none` refuses a `derived`, `converted`, or `assumed` row (`derived_not_allowed`): the requester was promised measured values only.
114
+ - The source floor is evaluated live against the `meta.json` of every source behind the row (a derived row's sources are its operands' sources): `pointed only: <list>` admits a source whose id (`S4`), URL, or path is in the list; `primary only` admits a source whose declared provenance is `primary` (`source snapshot --provenance primary|secondary`, default `unknown`; `secondary` and `unknown` are refused); `classes: <list>` admits a source whose URL host equals a listed host suffix or ends with `.<suffix>` (`sec.gov`, `go.kr`); `standard` refuses nothing. Under any floor but `standard`, a row resting on no source (an `assumed` row, or a derived row with such an operand) is refused as well: no source of it can be inside the floor. A refused row is `source_outside_floor`. Rows registered while the floor was pending are reclassified the moment it locks or changes. The row's `outside_floor` is the cached result of the last fill, never an input; fill writes it to the ledger even when it refuses.
115
+ - `languages` with `quotes original with translation beside` lets a `{{Q7:label}}` be followed by its translation, `<span data-translates="Q7">...</span>` in HTML or `<!-- translates: Q7 -->` and the sentence in Markdown; fill binds it to Q7, and `fact pairs` emits it as a `translation` pair. Under `quotes original` fill does not bind it.
116
+ - `fallback: omit` makes `--replace-failed` remove a twice-failed sentence instead of replacing it with an attributed block quote.
117
+ - Fill records `brief_sha256` in `<out>.fill-map.json`: the sha256 of the locked block normalized to its seven `field: value` lines in field order (`answered_by` and `asked:` excluded). `fact verify` reads that map (`--fill-map`, else `<report>.fill-map.json` when present) and refuses with `brief_changed` when the hash is not the current block's or the map has none; re-fill, then verify.
118
+
119
+ **Re-entry.** A session started before this block existed (an `edit-existing` run included) has no `## Fidelity`: run the inference step and write the block with `answered_by: default` before any `fact` command.
120
+
79
121
  ## 5. Report-format memory episodes
80
122
 
81
123
  When the memory tool is present, the requester's report-format choices are kept as an append-only episode log, so a repeat requester is not asked the same question twice. When no memory tool is present, skip this section silently.
@@ -156,7 +198,7 @@ node "$SKILL_DIR/scripts/report-tools.mjs" repair decide --state "$SESSION_DIR/r
156
198
 
157
199
  - `action: repair`: fix the listed defects, re-render, re-run the gate, and ask again.
158
200
  - `action: deliver`: stop repairing and deliver. `reason` is `clean` or the stop condition that fired; the residual defects are written into the manifest and listed in the closing briefing.
159
- - `action: block` (exit `1`): do not deliver. Either a content-integrity defect remains (`missing_closing_section`, `missing_citation_section`, `broken_asset_reference`, `missing_required_section`, `missing_promised_deliverable`, or a fidelity integrity code: `unresolved_placeholder`, `value_mismatch`, `unit_mismatch`, `quote_not_verbatim`, `claim_status_invalid`, `incompatible_operands`, `pointer_label_mismatch`, `quote_anchor_unresolved`, `unit_duplicated`, `polish_diff_fail`, `fidelity_interview_incomplete`, `derived_not_allowed`, `source_outside_floor`, `snapshot_hash_mismatch`, `chart_value_untraceable`, `quote_unbound`, `edition_unverified`), or a stop fired without proof of a usable artifact (`--artifact-bytes` and `--renders`). Tell the requester what blocks delivery and record the affected rows as `failed` with the reason.
201
+ - `action: block` (exit `1`): do not deliver. Either a content-integrity defect remains (`missing_closing_section`, `missing_citation_section`, `broken_asset_reference`, `missing_required_section`, `missing_promised_deliverable`, or a fidelity integrity code: `unresolved_placeholder`, `value_mismatch`, `unit_mismatch`, `quote_not_verbatim`, `claim_status_invalid`, `incompatible_operands`, `pointer_label_mismatch`, `quote_anchor_unresolved`, `unit_duplicated`, `polish_diff_fail`, `fidelity_interview_incomplete`, `derived_not_allowed`, `source_outside_floor`, `brief_changed`, `snapshot_hash_mismatch`, `chart_value_untraceable`, `quote_unbound`, `edition_unverified`), or a stop fired without proof of a usable artifact (`--artifact-bytes` and `--renders`). Tell the requester what blocks delivery and record the affected rows as `failed` with the reason.
160
202
 
161
203
  **The fidelity barrier is separate from the budget.** A `deliver` decision does not release fidelity defects: `outcome verify` and `outcome finish` use the fidelity gate's `blocksDelivery` predicate over `DEFECT_CODES` (integrity, blocker, or major), never a CLI code list or the row's claimed severity. Unknown codes refuse with `unknown defect code <code>`; known blockers produce `fidelity_unresolved`. Fix the defect and re-verify the edition before delivery. The table and the record schema in `scripts/fidelity-records.mjs` are the spec for the barrier. Every fidelity record carries one envelope (`record_kind`, `artifact_sha256`, `report`, `written_at`, `defects`) plus the fields its kind declares; a record with a missing, unknown, or ill-typed field refuses as malformed (`edition_unverified`). `FIDELITY_RECORDS` has one row per record kind, each rule a column: where its records are, when a delivered session must carry one, the envelope field that binds it to the delivered bytes (`artifact_sha256`), and which defects block. The rows are `fidelity` (`fact verify`, one record per delivered edition, named by `--verified-by`) and `meaning` (`fidelity-meaning-defects.json` from `fact meaning-check`, one per session, required while the locked brief's `meaning check` is `on` or its `## Fidelity` block is malformed; missing: `meaning gate not run`, the same `edition_unverified` refusal, released only by the logged `fidelity not_run` exemption in a session with no ledger or fidelity record). A record whose `artifact_sha256` matches no delivered edition's source bytes refuses as `is for a different edition`, so a meaning record from an earlier draft never passes a rewritten one: rerun `fact pairs` and the readers after every rewrite. A `meaning_drift_confirmed` or `meaning_unverified` in that record holds the barrier like any other major.
162
204
 
@@ -214,17 +256,17 @@ Invoke every command as `node "$SKILL_DIR/scripts/report-tools.mjs" <command>`.
214
256
  | `outcome briefing` | `--json`, `--ledger <file>`, `--session-dir <dir>`, `--now <ISO>` | never |
215
257
  | `format-extract <reference>` | `--out <design-spec.md>` (required), `--from-url` | never (an unsupported reference exits `2`) |
216
258
  | `--help` | `--json` | never |
217
- | `source snapshot` | `--id S<n>` (required), one of `--url <u>`, `--html <file>`, `--text <file>` (with optional `--extractor NAME@VERSION`), `--command '<argv json>' --cwd <dir>`; `--kind page\|command\|document`, `--provenance primary\|secondary`, `--revision <r>`, `--session-dir <dir>` | the id already holds different content (`snapshot_exists`) |
259
+ | `source snapshot` | `--id S<n>` (required), one of `--url <u>`, `--html <file>`, `--text <file>` (with optional `--extractor NAME@VERSION`), `--command '<argv json>' --cwd <dir>`; `--kind page\|command\|document`, `--provenance primary\|secondary` (default `unknown`, read by `source floor: primary only`), `--revision <r>`, `--session-dir <dir>` | the id already holds different content (`snapshot_exists`) |
218
260
  | `fact add` | `--source S<n>` with `--find <text>` (`--unit-expected <unit>`), or `--table <id> --row <label> --col <label>`, or `--page N --line N`; `--label`, `--entity`, `--valid-at`, `--population`, `--accounting-basis`, `--observation O<n>`, `--json`, `--session-dir <dir>`. Or `--assumed <value> --unit <unit> --rationale <text> --label <label>` | `fact_not_located`, `fact_ambiguous`, `unit_mismatch` (no row written) |
219
261
  | `fact quote` | `--source S<n> --match <text>` (required), `--label`, `--json`, `--session-dir <dir>` | `fact_not_located`, `fact_ambiguous` |
220
262
  | `fact retract <id>` | `--reason <text>` (required), `--replacement <id>`, `--session-dir <dir>` | never (an illegal transition exits `2`) |
221
263
  | `fact export` | `--csv`, `--out <file>`, `--session-dir <dir>` | never |
222
- | `fact fill <src>` | `--out <file>` or `--view`, `--format html\|md\|tex`, `--inline`, `--replace-failed`, `--session-dir <dir>` | a fill error (nothing is written) |
223
- | `fact verify <report>` | `--format html\|md` (from the extension by default), `--sources <dir>`, `--claims <claims.json>`, `--fill-map <file>` (a Markdown edition reads `<report>.fill-map.json` by default), `--brief <brief.md>`, `--out <record>`, `--session-dir <dir>` | any blocker or major defect |
224
- | `fact pairs <report>` | `--format html\|md`, `--ledger <fact-ledger.json>`, `--claims <claims.json>`, `--changed-since <fill-map.json>` (keep only the pairs whose sentence text is not in the pre-polish artifact that map names, so the readers re-check exactly what polish rewrote; the map must carry `artifact_sha256` and the artifact must still hash to it, otherwise the command exits `2`), `--brief <brief.md>`, `--out <pairs.json>`, `--session-dir <dir>` | the brief has no locked `## Fidelity` block (with `meaning check: off` it writes `{"skipped": true}` and exits `0`) |
225
- | `fact polish-apply <report.html>` | `--edits <polish-edits.json>` and `--out <report.polished.html>` (both required; a missing flag or a Markdown edition exits `2`), `--format html`, `--ledger <fact-ledger.json>`, `--session-dir <dir>` | never: it exits `0` whenever it wrote the output. Each rejected (`polish_edit_rejected`) or unlocated (`polish_edit_unlocated`) edit leaves its text unchanged and is printed verbatim, one line per rejection, in the summary and in `data.rejected_reasons`; `data` also carries `applied`, `rejected`, `unlocated`, `applied_indexes`, `input_sha256`, `edits_sha256`, and `out_sha256` |
226
- | `fact polish-diff` | `--before <pre-polish.html>` and `--after <polished.html>` (both required; a missing flag exits `2`), `--ledger <fact-ledger.json>`, `--out <record>` (default `$SESSION_DIR/polish-defects.json`), `--session-dir <dir>` | a registry blocker in the diff (majors and minors are reported and exit `0`; a defect code the registry does not know refuses with `unknown defect code <code>`). The record is `{command, status, exitCode, before, artifact, artifact_sha256, defects[], counts}`, with `counts` in contract buckets |
227
- | `fact meaning-check` | `--pairs <pairs.json>` and `--verdicts <file>` (required; repeatable, one per reader tier), `--dispute P<n> --reason <text>`, `--run-id <id>`, `--calibration <file>` (default `fidelity-calibration.jsonl`), `--out <record>` (default `fidelity-meaning-defects.json`), `--session-dir <dir>` | `meaning_drift_confirmed` (stderr carries one JSON line with the defect and dropped-verdict counts) |
264
+ | `fact fill <src>` | `--out <file>` or `--view`, `--format html\|md\|tex`, `--inline`, `--replace-failed`, `--brief <brief.md>`, `--session-dir <dir>` | `fidelity_interview_incomplete`, `derived_not_allowed`, `source_outside_floor` (section 4), or a fill error: no edition, fill map, or state is written; the ledger's cached `outside_floor` is (section 4). Off-table `derived numbers` or `fallback` exits `2` |
265
+ | `fact verify <report>` | `--format html\|md` (from the extension by default), `--sources <dir>`, `--claims <claims.json>`, `--fill-map <file>` (a Markdown edition reads `<report>.fill-map.json` by default), `--brief <brief.md>`, `--out <record>`, `--session-dir <dir>` | `fidelity_interview_incomplete` (no record written), or any blocker or major defect, `brief_changed`, `derived_not_allowed`, and `source_outside_floor` under the current brief included (section 4) |
266
+ | `fact pairs <report>` | `--format html\|md`, `--ledger <fact-ledger.json>`, `--claims <claims.json>`, `--changed-since <fill-map.json>` (keep only the pairs whose sentence text is not in the pre-polish artifact that map names, so the readers re-check exactly what polish rewrote; the map must carry `artifact_sha256` and the artifact must still hash to it, otherwise the command exits `2`), `--brief <brief.md>`, `--out <pairs.json>`, `--session-dir <dir>` | `fidelity_interview_incomplete` (with `meaning check: off` it writes `{"skipped": true}` and exits `0`) |
267
+ | `fact polish-apply <report.html>` | `--edits <polish-edits.json>` and `--out <report.polished.html>` (both required; a missing flag or a Markdown edition exits `2`), `--format html`, `--ledger <fact-ledger.json>`, `--brief <brief.md>`, `--session-dir <dir>` | `fidelity_interview_incomplete` (nothing written); otherwise never: it exits `0` whenever it wrote the output. Each rejected (`polish_edit_rejected`) or unlocated (`polish_edit_unlocated`) edit leaves its text unchanged and is printed verbatim, one line per rejection, in the summary and in `data.rejected_reasons`; `data` also carries `applied`, `rejected`, `unlocated`, `applied_indexes`, `input_sha256`, `edits_sha256`, and `out_sha256` |
268
+ | `fact polish-diff` | `--before <pre-polish.html>` and `--after <polished.html>` (both required; a missing flag exits `2`), `--ledger <fact-ledger.json>`, `--out <record>` (default `$SESSION_DIR/polish-defects.json`), `--brief <brief.md>`, `--session-dir <dir>` | `fidelity_interview_incomplete` (nothing written), or a registry blocker in the diff (majors and minors are reported and exit `0`; a defect code the registry does not know refuses with `unknown defect code <code>`). The record is `{command, status, exitCode, before, artifact, artifact_sha256, defects[], counts}`, with `counts` in contract buckets |
269
+ | `fact meaning-check` | `--pairs <pairs.json>` and `--verdicts <file>` (required; repeatable, one per reader tier), `--dispute P<n> --reason <text>`, `--run-id <id>`, `--calibration <file>` (default `fidelity-calibration.jsonl`), `--brief <brief.md>`, `--out <record>` (default `fidelity-meaning-defects.json`), `--session-dir <dir>` | `fidelity_interview_incomplete`, or `meaning_drift_confirmed` (stderr carries one JSON line with the defect and dropped-verdict counts) |
228
270
  | `fact derive` | `--op <op>` (required) with the operation's operands (section 11), `--unit`, `--to`, `--rationale`, `--disjoint`, `--label`, `--json`, `--session-dir <dir>` | `incompatible_operands` (no row written) |
229
271
  | `claims index <claim-graph.md>` | `--out <claims.json>` (required) | never (a malformed claim table exits `2`) |
230
272
 
@@ -47,7 +47,8 @@ Raised only by the fidelity verifier (the fact ledger, fill, verify, meaning, an
47
47
  | `polish_diff_fail` | blocker | true | The polish pass changed protected fact spans, numerals or units, tickers, table rows, or any markup byte (including every attribute, comments, and script/style/template content). | Revert to the pre-polish artifact and apply text-node edits only; markup changes require the full fidelity gates. |
48
48
  | `fidelity_interview_incomplete` | blocker | true | A fidelity field in the brief is still pending or missing when a fidelity command runs. | A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun. |
49
49
  | `derived_not_allowed` | blocker | true | A placeholder resolves to a derived, converted, or assumed row while the brief allows no derived numbers. | The brief allows no derived or assumed numbers; cite a source row directly or change the brief with the requester. |
50
- | `source_outside_floor` | blocker | true | A ledger row's source lies outside the source floor the brief records (pointed sources only, or the allowed source classes). | The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact. |
50
+ | `source_outside_floor` | blocker | true | A ledger row's source lies outside the source floor the brief records (pointed sources only, primary only, or the allowed source classes), or the row rests on no source under any floor but `standard`. | The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact. |
51
+ | `brief_changed` | blocker | true | `fact verify` reads an edition's fill map whose recorded brief hash is not the hash of the current locked `## Fidelity` block, or that records none. | The edition's fill map was not written under the current ## Fidelity block (it changed after fill, or the map predates the binding); re-run fact fill, then fact verify. |
51
52
  | `snapshot_hash_mismatch` | blocker | true | A pinned source snapshot's text no longer hashes to its recorded hash, or indexed slices no longer equal the snapshot text. | The source snapshot changed after it was pinned; re-snapshot the source and re-register its rows. |
52
53
  | `chart_value_untraceable` | blocker | true | A chart data label, axis tick, or chart data file does not match the ledger-backed data the chart declares, or the chart declares no data source. | A chart value, tick, or data file does not match its ledger-backed data source; re-render the chart from the exported data. |
53
54
  | `quote_unbound` | blocker | true | A direct quotation in the report has no ledger quote row behind it; a citation marker alone does not bind it. | A direct quotation has no ledger quote row; register it with fact quote and insert it through its placeholder. |
@@ -62,6 +62,7 @@ export const DEFECT_CODES = deepFreeze({
62
62
  fidelity_interview_incomplete: { severity: "blocker", integrity: true, hint: "A fidelity field is pending or missing in brief.md; answer the open question or record its default, then rerun." },
63
63
  derived_not_allowed: { severity: "blocker", integrity: true, hint: "The brief allows no derived or assumed numbers; cite a source row directly or change the brief with the requester." },
64
64
  source_outside_floor: { severity: "blocker", integrity: true, hint: "The row's source is outside the source floor the brief records; use a source the brief allows or remove the fact." },
65
+ brief_changed: { severity: "blocker", integrity: true, hint: "The edition's fill map was not written under the current ## Fidelity block (it changed after fill, or the map predates the binding); re-run fact fill, then fact verify." },
65
66
  snapshot_hash_mismatch: { severity: "blocker", integrity: true, hint: "The source snapshot changed after it was pinned; re-snapshot the source and re-register its rows." },
66
67
  chart_value_untraceable: { severity: "blocker", integrity: true, hint: "A chart value, tick, or data file does not match its ledger-backed data source; re-render the chart from the exported data." },
67
68
  quote_unbound: { severity: "blocker", integrity: true, hint: "A direct quotation has no ledger quote row; register it with fact quote and insert it through its placeholder." },
@@ -99,7 +100,7 @@ export const DEFECT_CODES = deepFreeze({
99
100
  export const FIDELITY_CODES = Object.freeze([
100
101
  "unresolved_placeholder", "value_mismatch", "unit_mismatch", "quote_not_verbatim", "claim_status_invalid",
101
102
  "incompatible_operands", "pointer_label_mismatch", "quote_anchor_unresolved", "unit_duplicated", "polish_diff_fail",
102
- "fidelity_interview_incomplete", "derived_not_allowed", "source_outside_floor", "snapshot_hash_mismatch",
103
+ "fidelity_interview_incomplete", "derived_not_allowed", "source_outside_floor", "brief_changed", "snapshot_hash_mismatch",
103
104
  "chart_value_untraceable", "quote_unbound", "edition_unverified", "untraceable_number", "unsupported_verification", "claim_kind_mismatch",
104
105
  "conflicting_readings", "hedge_dropped", "direction_flipped", "causality_added", "qualifier_dropped", "unbound_sentence",
105
106
  "meaning_drift_confirmed", "meaning_unverified", "polish_edit_rejected", "polish_hedging_decreased", "polish_text_churn", "snapshot_mismatch",
@@ -134,16 +135,7 @@ export const GATE_STATUSES = Object.freeze(["pass", "fail", "not_run"])
134
135
  export const LINEAGE_MODES = Object.freeze(["inline", "section"])
135
136
  export const ANSWERED_BY = Object.freeze(["user", "default", "request", "pending"])
136
137
  export const REPAIR_DEFAULTS = Object.freeze({ maxAttempts: 3, plateauLimit: 2, wallClockMs: 15 * 60 * 1000 })
137
- export const DESIGN_SPEC_SECTIONS = Object.freeze([
138
- "Extracted from",
139
- "Tokens",
140
- "Typography",
141
- "Layout",
142
- "Structure",
143
- "Figures",
144
- "Citations",
145
- "Open questions",
146
- ])
138
+ export const DESIGN_SPEC_SECTIONS = Object.freeze(["Extracted from", "Tokens", "Typography", "Layout", "Structure", "Figures", "Citations", "Open questions"])
147
139
 
148
140
  const ISO_DATE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$/
149
141
 
@@ -0,0 +1,86 @@
1
+ // fact-commands-fill.mjs - `fact fill`, the reader-facing fill of a placeholder source.
2
+ //
3
+ // It refuses an unlocked brief (fidelity_interview_incomplete) and applies the locked `## Fidelity`
4
+ // fields: `derived numbers: none` and the source floor refuse rows (derived_not_allowed,
5
+ // source_outside_floor, evaluated at every fill), `languages` decides whether a translation after a
6
+ // quote binds to it, and `fallback` picks the block quote or omission for --replace-failed. Any
7
+ // error writes no edition, fill map, or state; the one write that survives a refusal is the
8
+ // ledger's cached outside_floor per row (skipped under --view). The fill map records brief_sha256,
9
+ // the locked block fact verify checks it against. node: builtins and sibling imports only.
10
+
11
+ import { readFileSync, writeFileSync } from "node:fs"
12
+ import { dirname, extname, resolve } from "node:path"
13
+
14
+ import { CliError, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
15
+ import { DEFECT_CODES } from "./contracts.mjs"
16
+ import { FILL_FORMATS, fill } from "./fact-fill.mjs"
17
+ import { readLedger, writeLedger } from "./fact-ledger.mjs"
18
+ import { briefHashOf, fillPolicy, lockedBrief, policyErrors } from "./fidelity-policy.mjs"
19
+ import { readState, writeState } from "./fidelity-state.mjs"
20
+ import { findPlaceholders } from "./placeholder.mjs"
21
+ import { sessionDirOf } from "./source-commands.mjs"
22
+
23
+ /** The CLI's input vocabulary: fill's output formats plus aliases (tex is filled as md), and the extension defaults. */
24
+ export const FILL_INPUT_FORMAT_RULES = Object.freeze({
25
+ aliases: Object.freeze({ tex: "md" }),
26
+ extensions: Object.freeze({ ".md": "md", ".tex": "tex" }),
27
+ })
28
+ export const FILL_INPUT_FORMATS = Object.freeze([...FILL_FORMATS, ...Object.keys(FILL_INPUT_FORMAT_RULES.aliases)])
29
+ const fillFormatOf = (format) => FILL_INPUT_FORMAT_RULES.aliases[format] ?? format
30
+
31
+ function readText(path) {
32
+ try {
33
+ return readFileSync(path, "utf8")
34
+ } catch (error) {
35
+ throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
36
+ }
37
+ }
38
+
39
+ /** The inline edition: local stylesheets and scripts inlined, relative image paths made absolute. */
40
+ function inlineAssets(html, baseDir) {
41
+ const local = (ref) => !/^[a-z][a-z0-9+.-]*:|^\/\//i.test(ref)
42
+ return html
43
+ .replace(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi, (tag) => {
44
+ const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1]
45
+ return href && local(href) ? `<style>${readText(resolve(baseDir, href))}</style>` : tag
46
+ })
47
+ .replace(/<script\b([^>]*)\bsrc=["']([^"']+)["']([^>]*)><\/script>/gi, (tag, pre, src, post) => (local(src) ? `<script${pre}${post}>${readText(resolve(baseDir, src))}</script>` : tag))
48
+ .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (tag, pre, src, post) => (local(src) && !src.startsWith("/") ? `${pre}${resolve(baseDir, src)}${post}` : tag))
49
+ }
50
+
51
+ /** @param {{ positionals: string[], flags: Record<string, any> }} args parsed by runFact */
52
+ export function runFactFill(args) {
53
+ const [source] = args.positionals
54
+ const { flags } = args
55
+ if (!source || (flags.out === undefined && !flags.view)) throw new CliError("usage: fact fill <src> (--out <path> | --view) [--format html|md|tex] [--inline] [--replace-failed] [--brief brief.md]")
56
+ const format = flags.format ?? FILL_INPUT_FORMAT_RULES.extensions[extname(source).toLowerCase()] ?? "html"
57
+ if (!FILL_INPUT_FORMATS.includes(format)) throw new CliError(`--format must be ${FILL_INPUT_FORMATS.slice(0, -1).join(", ")}, or ${FILL_INPUT_FORMATS.at(-1)}`)
58
+ if (flags.inline && format !== "html") throw new CliError("--inline applies to the html edition only")
59
+ const sessionDir = sessionDirOf(flags)
60
+ const { parsed, refusal } = lockedBrief("fact fill", flags)
61
+ if (refusal) return refusal
62
+ const policy = fillPolicy(parsed)
63
+ const text = readText(source)
64
+ const ledger = readLedger(sessionDir)
65
+ const refused = policyErrors(text, findPlaceholders(text), ledger, sessionDir, policy)
66
+ if (refused.cacheChanged && !flags.view) writeLedger(sessionDir, ledger)
67
+ const state = flags["replace-failed"] ? readState(sessionDir) : null
68
+ const result = fill(text, ledger, { format: fillFormatOf(format), view: flags.view === true, fallback: policy.fallback, ...(state ? { replaceFailed: state } : {}) })
69
+ const warnings = result.warnings
70
+ if (result.out === null || refused.errors.length > 0) {
71
+ const filled = result.errors.map((error) => ({ ...error, severity: DEFECT_CODES[error.code]?.severity ?? "blocker", hint: DEFECT_CODES[error.code]?.hint ?? null }))
72
+ const defects = [...refused.errors, ...filled]
73
+ return { json: { command: "fact fill", status: "fail", exitCode: 1, defects, warnings }, summary: `fact fill: FAIL - ${defects.length} error(s), nothing written`, exitCode: 1 }
74
+ }
75
+ const entries = policy.translations ? result.fillMap : result.fillMap.filter((entry) => entry.translates === undefined)
76
+ const out = flags.inline ? inlineAssets(result.out, dirname(resolve(source))) : result.out
77
+ if (flags.view) return { text: out, summary: `fact fill --view: ${entries.length} pointer(s), ${warnings.length} warning(s)`, exitCode: 0 }
78
+ writeFileSync(flags.out, out)
79
+ if (state) writeState(sessionDir, state)
80
+ const artifact = sha256Of(out)
81
+ const map = `${resolve(flags.out)}.fill-map.json`
82
+ const brief = briefHashOf(parsed)
83
+ writeJsonAtomic(map, { schemaVersion: 1, format, artifact: resolve(flags.out), artifact_sha256: artifact, brief_sha256: brief, entries })
84
+ const data = { out: resolve(flags.out), map, artifact_sha256: artifact, brief_sha256: brief, occurrences: entries.length, warnings }
85
+ return { json: { command: "fact fill", status: "pass", exitCode: 0, defects: [], data }, summary: `fact fill: wrote ${flags.out} (${entries.length} pointer(s))`, exitCode: 0 }
86
+ }
@@ -8,7 +8,8 @@
8
8
  // fidelity-calibration.jsonl: a header with the promotion criterion on first write, then per run
9
9
  // one run line plus one line per flagged pair. The run id hashes the inputs only, so a rerun or a
10
10
  // --dispute rerun of the same inputs replaces that run's lines in place (disputes carry forward)
11
- // instead of counting a second run. Neither command calls a model or the network.
11
+ // instead of counting a second run. Both refuse an unlocked brief (fidelity_interview_incomplete) before
12
+ // reading anything else. Neither command calls a model or the network.
12
13
 
13
14
  import { existsSync, readFileSync } from "node:fs"
14
15
  import { dirname, join, resolve } from "node:path"
@@ -18,7 +19,8 @@ import { countDefects } from "./contracts.mjs"
18
19
  import { ledgerFrom, markdownAsHtml, readEdition, sessionOf } from "./fact-commands-verify.mjs"
19
20
  import { pairsChangedSince } from "./fact-commands-polish.mjs"
20
21
  import { extractPairs, PAIR_KINDS } from "./fact-pairs.mjs"
21
- import { FIDELITY_INCOMPLETE, meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
22
+ import { FIDELITY_INCOMPLETE } from "./fidelity-brief.mjs"
23
+ import { briefPathOf, lockedBrief, meaningCheckEnabled } from "./fidelity-policy.mjs"
22
24
  import { MEANING_RECORD, writeFidelityRecord } from "./fidelity-records.mjs"
23
25
  import { blocksDelivery } from "./gates-fidelity.mjs"
24
26
  import { ingestVerdicts, unverifiedDefects, VERDICT_SCHEMA } from "./meaning-check.mjs"
@@ -36,25 +38,15 @@ const inSession = (session, name, flag) => {
36
38
  return join(session, name)
37
39
  }
38
40
 
39
- function briefState(flags, session) {
40
- const path = flags.brief ?? inSession(session, "brief.md", "--brief")
41
- const lock = requireLockedBrief(path)
42
- if (lock.exitCode === 2) throw new CliError(`${path}: malformed ## Fidelity block: ${lock.errors.join("; ")}`)
43
- return { path, locked: lock.locked, off: !meaningCheckEnabled(lock.parsed) }
44
- }
45
-
46
41
  function runPairs(args) {
47
42
  const [report] = args.positionals
48
43
  const { flags } = args
49
44
  if (!report) throw new CliError("usage: fact pairs <report> [--format html|md] [--ledger F] [--claims claims.json] [--changed-since <fill-map.json>] [--brief brief.md] [--out pairs.json]")
50
45
  const session = sessionOf(flags)
51
46
  const out = resolve(flags.out ?? inSession(session, PAIRS_FILE, "--out"))
52
- const brief = briefState(flags, session)
53
- if (!brief.locked) {
54
- const json = { command: "fact pairs", status: "fail", exitCode: 1, error: "fidelity_interview_incomplete", message: `${brief.path} has no locked ## Fidelity block` }
55
- return { json, summary: `fact pairs: ${json.message}`, exitCode: 1 }
56
- }
57
- if (brief.off) {
47
+ const { parsed, refusal } = lockedBrief("fact pairs", flags)
48
+ if (refusal) return refusal
49
+ if (!meaningCheckEnabled(parsed)) {
58
50
  const record = { schemaVersion: 1, skipped: true, reason: "meaning check: off", pairs: [] }
59
51
  writeJsonAtomic(out, record)
60
52
  return { json: { command: "fact pairs", status: "skipped", exitCode: 0, data: { out, ...record } }, summary: "fact pairs: skipped (meaning check: off)", exitCode: 0 }
@@ -125,23 +117,23 @@ function recordCalibration(path, lines, runId, pairsSha) {
125
117
  }
126
118
 
127
119
  /** A `{ skipped: true }` pairs file is accepted only when the locked brief turns the meaning check off. */
128
- function skippedRefusal(flags, session) {
129
- const brief = briefState(flags, session)
130
- if (!brief.locked) return { error: FIDELITY_INCOMPLETE, message: `${brief.path} has no locked ## Fidelity block, so a skipped meaning check cannot be accepted` }
131
- if (!brief.off) return { error: "meaning_check_required", message: `${flags.pairs} says skipped but ${brief.path} has meaning check on; rerun fact pairs` }
120
+ function skippedRefusal(flags, parsed) {
121
+ if (meaningCheckEnabled(parsed)) return { error: "meaning_check_required", message: `${flags.pairs} says skipped but ${briefPathOf(flags)} has meaning check on; rerun fact pairs` }
132
122
  return null
133
123
  }
134
124
 
135
125
  function runMeaningCheck(args) {
136
126
  const { flags } = args
137
- const usage = "usage: fact meaning-check --pairs pairs.json --verdicts cheap.json [--verdicts strong.json] [--dispute P<n> --reason TEXT] [--run-id ID] [--calibration F] [--out F]"
127
+ const usage = "usage: fact meaning-check --pairs pairs.json --verdicts cheap.json [--verdicts strong.json] [--dispute P<n> --reason TEXT] [--run-id ID] [--calibration F] [--brief brief.md] [--out F]"
138
128
  if (flags.pairs === undefined || !Array.isArray(flags.verdicts)) throw new CliError(usage)
139
129
  const session = sessionOf(flags)
130
+ const { parsed, refusal } = lockedBrief("fact meaning-check", flags)
131
+ if (refusal) return { ...refusal, json: { ...refusal.json, error: FIDELITY_INCOMPLETE } }
140
132
  const pairsText = readFileSync(flags.pairs, "utf8")
141
133
  const pairsDoc = readJsonFile(flags.pairs)
142
134
  if (pairsDoc?.skipped === true) {
143
- const refusal = skippedRefusal(flags, session)
144
- if (refusal !== null) return { json: { command: "fact meaning-check", status: "fail", exitCode: 1, ...refusal }, summary: `fact meaning-check: ${refusal.message}`, exitCode: 1 }
135
+ const skipped = skippedRefusal(flags, parsed)
136
+ if (skipped !== null) return { json: { command: "fact meaning-check", status: "fail", exitCode: 1, ...skipped }, summary: `fact meaning-check: ${skipped.message}`, exitCode: 1 }
145
137
  return { json: { command: "fact meaning-check", status: "skipped", exitCode: 0, defects: [] }, summary: JSON.stringify({ command: "fact meaning-check", status: "skipped" }), exitCode: 0 }
146
138
  }
147
139
  if (typeof pairsDoc?.artifact_sha256 !== "string" || typeof pairsDoc.report !== "string") throw new CliError(`${flags.pairs} names no artifact_sha256 and report, so the meaning record cannot be bound to an edition; rerun fact pairs`)
@@ -19,6 +19,7 @@ import { CliError, parseArgs, readJsonFile, sha256Of, writeJsonAtomic, writeText
19
19
  import { countDefects, DEFECT_CODES } from "./contracts.mjs"
20
20
  import { editionFormatOf, ledgerFrom, markdownAsHtml, sessionOf } from "./fact-commands-verify.mjs"
21
21
  import { extractPairs } from "./fact-pairs.mjs"
22
+ import { lockedBrief } from "./fidelity-policy.mjs"
22
23
  import { applyEdits, POLISH_EDIT_FIELDS, validatePolishEdits } from "./polish-apply.mjs"
23
24
  import { diffPolish } from "./polish-diff.mjs"
24
25
  import { normalizeText } from "./text-normalize.mjs"
@@ -57,8 +58,10 @@ const rejectionLine = (entry, edit) => ` rejected edit ${entry.index} ("${befor
57
58
  function runPolishApply(args) {
58
59
  const [report] = args.positionals
59
60
  const { flags } = args
60
- if (!report || flags.edits === undefined || flags.out === undefined) throw new CliError("usage: fact polish-apply <report.html> --edits polish-edits.json --out <report.polished.html> [--ledger F] [--session-dir D]")
61
+ if (!report || flags.edits === undefined || flags.out === undefined) throw new CliError("usage: fact polish-apply <report.html> --edits polish-edits.json --out <report.polished.html> [--ledger F] [--brief brief.md] [--session-dir D]")
61
62
  if (editionFormatOf(report, flags.format) === "md") throw new CliError("fact polish-apply takes the filled HTML edition; polish a Markdown edition through its HTML source")
63
+ const { refusal } = lockedBrief("fact polish-apply", flags)
64
+ if (refusal) return refusal
62
65
  const html = readText(report)
63
66
  const editsText = readText(flags.edits)
64
67
  const edits = readJsonFile(flags.edits)
@@ -79,7 +82,9 @@ function runPolishApply(args) {
79
82
 
80
83
  function runPolishDiff(args) {
81
84
  const { flags } = args
82
- if (flags.before === undefined || flags.after === undefined) throw new CliError("usage: fact polish-diff --before <pre-polish.html> --after <polished.html> [--ledger F] [--out polish-defects.json] [--session-dir D]")
85
+ if (flags.before === undefined || flags.after === undefined) throw new CliError("usage: fact polish-diff --before <pre-polish.html> --after <polished.html> [--ledger F] [--out polish-defects.json] [--brief brief.md] [--session-dir D]")
86
+ const { refusal } = lockedBrief("fact polish-diff", flags)
87
+ if (refusal) return refusal
83
88
  const after = readText(flags.after)
84
89
  const defects = diffPolish(readText(flags.before), after, ledgerFrom(flags)).map((defect) => ({ ...defect, hint: DEFECT_CODES[defect.code]?.hint ?? null }))
85
90
  const counts = countDefects(defects)
@@ -8,6 +8,11 @@
8
8
  // escaped HTML projection (same line numbers) and checked with its fill map, which is the only
9
9
  // record of where its values sit; LaTeX is verified through its Markdown source.
10
10
  //
11
+ // The locked brief is re-judged here, not only at fill: the edition's fill map (--fill-map, else
12
+ // `<report>.fill-map.json` when present) must carry the current brief's brief_sha256
13
+ // (brief_changed), and policyErrors re-runs over every data-fact the edition carries, so an
14
+ // edition that violates the current derived-numbers or source-floor rule fails here too.
15
+ //
11
16
  // The delivery barrier that reads these records (and every other fidelity record) is
12
17
  // fidelity-records.mjs.
13
18
 
@@ -19,6 +24,7 @@ import { countDefects } from "./contracts.mjs"
19
24
  import { recompute } from "./fact-derive.mjs"
20
25
  import { readLedger } from "./fact-ledger.mjs"
21
26
  import { validateLedger } from "./fact-ledger-schema.mjs"
27
+ import { briefBindingErrors, fillPolicy, lockedBrief, policyErrors } from "./fidelity-policy.mjs"
22
28
  import { FIDELITY_RECORD, writeFidelityRecord } from "./fidelity-records.mjs"
23
29
  import { blocksDelivery, checkFidelity } from "./gates-fidelity.mjs"
24
30
  import { parseMarkdown } from "./md-lite.mjs"
@@ -159,8 +165,11 @@ export function runFactVerify(args) {
159
165
  const [report] = args.positionals
160
166
  const { flags } = args
161
167
  if (!report) throw new CliError("usage: fact verify <report> [--format html|md] [--sources D] [--claims claims.json] [--fill-map F] [--brief brief.md] [--out fidelity-defects.json]")
162
- const edition = readEdition(report, flags)
163
168
  const sessionDir = sessionDirOf(flags)
169
+ const { parsed, refusal } = lockedBrief("fact verify", flags)
170
+ if (refusal) return refusal
171
+ const policy = fillPolicy(parsed)
172
+ const edition = readEdition(report, flags)
164
173
  const claimsPath = flags.claims ?? join(sessionDir, "claims.json")
165
174
  const briefPath = flags.brief ?? join(sessionDir, "brief.md")
166
175
  const ledger = readLedger(sessionDir)
@@ -174,10 +183,15 @@ export function runFactVerify(args) {
174
183
  baseDir: dirname(resolve(report)),
175
184
  recompute,
176
185
  })
177
- const counts = countDefects(result.defects)
178
- const status = result.defects.some(blocksDelivery) ? "fail" : "pass"
186
+ const checked = projected?.html ?? edition.text
187
+ const pointers = [...checked.matchAll(/\bdata-fact="([^"]+)"/gu)].map((match) => ({ token: match[0], start: match.index, parsed: { id: match[1] } }))
188
+ const mapPath = flags["fill-map"] ?? `${report}.fill-map.json`
189
+ const bound = existsSync(mapPath) ? briefBindingErrors(readJsonFile(mapPath), mapPath, parsed) : []
190
+ const defects = [...bound, ...policyErrors(checked, pointers, ledger, sessionDir, policy).errors, ...result.defects]
191
+ const counts = countDefects(defects)
192
+ const status = defects.some(blocksDelivery) ? "fail" : "pass"
179
193
  const exitCode = status === "fail" ? 1 : 0
180
194
  const out = resolve(flags.out ?? join(sessionDir, FIDELITY_RECORD))
181
- const record = writeFidelityRecord(out, "fidelity", { command: "fact verify", status, exitCode, format: edition.format, report: resolve(report), artifact_sha256: sha256Of(edition.text), defects: result.defects, counts, gatesRun: result.gatesRun })
195
+ const record = writeFidelityRecord(out, "fidelity", { command: "fact verify", status, exitCode, format: edition.format, report: resolve(report), artifact_sha256: sha256Of(edition.text), defects, counts, gatesRun: result.gatesRun })
182
196
  return { json: { ...record, out }, summary: `fact verify: ${status.toUpperCase()} - ${counts.blocker} blocker, ${counts.major} major, ${counts.minor} minor`, exitCode }
183
197
  }
@@ -1,22 +1,19 @@
1
- // fact-commands.mjs - the `fact` subcommands of report-tools: add, quote, retract, export, fill,
2
- // derive, verify (fact-commands-verify.mjs), pairs and meaning-check (fact-commands-meaning.mjs), polish-apply and
3
- // polish-diff (fact-commands-polish.mjs). Every value reaches the ledger from a source
1
+ // fact-commands.mjs - the `fact` subcommands of report-tools: add, quote, retract, export, derive,
2
+ // fill (fact-commands-fill.mjs), verify (fact-commands-verify.mjs), pairs and meaning-check
3
+ // (fact-commands-meaning.mjs), polish-apply and polish-diff (fact-commands-polish.mjs). Every value reaches the ledger from a source
4
4
  // snapshot through a deterministic locator or from a declared derivation; nothing here accepts a
5
5
  // typed value. add and quote print the row with 60 chars of source context on each side (JSON
6
6
  // with --json). Exit 0 ok, 1 semantic failure (fact_not_located, fill errors, a refused
7
7
  // derivation), 2 usage. node: builtins and sibling imports only.
8
8
 
9
9
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
10
- import { dirname, extname, join, resolve } from "node:path"
10
+ import { join, resolve } from "node:path"
11
11
 
12
- import { CliError, parseArgs, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
13
- import { DEFECT_CODES } from "./contracts.mjs"
12
+ import { CliError, parseArgs, sha256Of } from "./cli-support.mjs"
14
13
  import { derive } from "./fact-derive.mjs"
15
- import { fill } from "./fact-fill.mjs"
16
14
  import { addRow, readLedger, retractRow, supersedeRow, writeLedger } from "./fact-ledger.mjs"
17
15
  import { locateFind, locateMatch, locatePageLine, locateTable } from "./fact-locate.mjs"
18
16
  import { QUALIFIER_FORMS } from "./fidelity-lexicon.mjs"
19
- import { readState, writeState } from "./fidelity-state.mjs"
20
17
  import { exportRows } from "./fidelity-values.mjs"
21
18
  import { pointedFlag, sessionDirOf, snapshotOf } from "./source-commands.mjs"
22
19
 
@@ -157,44 +154,6 @@ function runExport(args) {
157
154
  return { json: { command: "fact export", status: "pass", exitCode: 0, data: { out: resolve(args.flags.out), format: args.flags.csv ? "csv" : "json", sha256: sha256Of(text), coverage } }, summary: coverage, exitCode: 0 }
158
155
  }
159
156
 
160
- /** The inline edition: local stylesheets and scripts inlined, relative image paths made absolute. */
161
- function inlineAssets(html, baseDir) {
162
- const local = (ref) => !/^[a-z][a-z0-9+.-]*:|^\/\//i.test(ref)
163
- return html
164
- .replace(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi, (tag) => {
165
- const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1]
166
- return href && local(href) ? `<style>${readText(resolve(baseDir, href))}</style>` : tag
167
- })
168
- .replace(/<script\b([^>]*)\bsrc=["']([^"']+)["']([^>]*)><\/script>/gi, (tag, pre, src, post) => (local(src) ? `<script${pre}${post}>${readText(resolve(baseDir, src))}</script>` : tag))
169
- .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (tag, pre, src, post) => (local(src) && !src.startsWith("/") ? `${pre}${resolve(baseDir, src)}${post}` : tag))
170
- }
171
-
172
- function runFill(args) {
173
- const [source] = args.positionals
174
- const { flags } = args
175
- if (!source || (flags.out === undefined && !flags.view)) throw new CliError("usage: fact fill <src> (--out <path> | --view) [--format html|md|tex] [--inline] [--replace-failed]")
176
- const format = flags.format ?? ({ ".md": "md", ".tex": "tex" }[extname(source).toLowerCase()] ?? "html")
177
- if (!["html", "md", "tex"].includes(format)) throw new CliError("--format must be html, md, or tex")
178
- if (flags.inline && format !== "html") throw new CliError("--inline applies to the html edition only")
179
- const sessionDir = sessionDirOf(flags)
180
- const state = flags["replace-failed"] ? readState(sessionDir) : null
181
- const result = fill(readText(source), readLedger(sessionDir), { format: format === "tex" ? "md" : format, view: flags.view === true, ...(state ? { replaceFailed: state } : {}) })
182
- const warnings = result.warnings
183
- if (result.out === null) {
184
- const defects = result.errors.map((error) => ({ ...error, severity: DEFECT_CODES[error.code]?.severity ?? "blocker", hint: DEFECT_CODES[error.code]?.hint ?? null }))
185
- return { json: { command: "fact fill", status: "fail", exitCode: 1, defects, warnings }, summary: `fact fill: FAIL - ${defects.length} error(s), nothing written`, exitCode: 1 }
186
- }
187
- const out = flags.inline ? inlineAssets(result.out, dirname(resolve(source))) : result.out
188
- if (flags.view) return { text: out, summary: `fact fill --view: ${result.fillMap.length} pointer(s), ${warnings.length} warning(s)`, exitCode: 0 }
189
- writeFileSync(flags.out, out)
190
- if (state) writeState(sessionDir, state)
191
- const artifact = sha256Of(out)
192
- const map = `${resolve(flags.out)}.fill-map.json`
193
- writeJsonAtomic(map, { schemaVersion: 1, format, artifact: resolve(flags.out), artifact_sha256: artifact, entries: result.fillMap })
194
- const data = { out: resolve(flags.out), map, artifact_sha256: artifact, occurrences: result.fillMap.length, warnings }
195
- return { json: { command: "fact fill", status: "pass", exitCode: 0, defects: [], data }, summary: `fact fill: wrote ${flags.out} (${result.fillMap.length} pointer(s))`, exitCode: 0 }
196
- }
197
-
198
157
  function runDerive(args) {
199
158
  const { flags } = args
200
159
  const op = args.required("op")
@@ -230,7 +189,7 @@ export async function runFact(argv) {
230
189
  case "export":
231
190
  return runExport(args)
232
191
  case "fill":
233
- return runFill(args)
192
+ return (await import("./fact-commands-fill.mjs")).runFactFill(args)
234
193
  case "derive":
235
194
  return runDerive(args)
236
195
  case "verify":
@@ -10,7 +10,8 @@ import { failureCount, markReplaced, sentenceKey } from "./fidelity-state.mjs"
10
10
  import { findPlaceholders, lead, qualifiedText, renderBlockquote, renderQuote, renderValueHtml, subSpan, valueParts } from "./placeholder.mjs"
11
11
  import { splitSentences } from "./sentence-split.mjs"
12
12
 
13
- const FORMATS = Object.freeze(["html", "md"])
13
+ /** The output formats fill renders. */
14
+ export const FILL_FORMATS = Object.freeze(["html", "md"])
14
15
  const TRANSLATION = Object.freeze({
15
16
  html: /^\s*<span data-translates="(Q\d+)">[\s\S]*?<\/span>/u,
16
17
  md: /^\s*<!-- translates: (Q\d+) -->[^\n]*/u,
@@ -166,7 +167,7 @@ function assemble(text, parts, context) {
166
167
  */
167
168
  export function fill(sourceText, ledger, options = {}) {
168
169
  const format = options.format ?? "html"
169
- if (!FORMATS.includes(format)) throw new TypeError(`format must be one of ${FORMATS.join(", ")}`)
170
+ if (!FILL_FORMATS.includes(format)) throw new TypeError(`format must be one of ${FILL_FORMATS.join(", ")}`)
170
171
  const rows = new Map(ledger.rows.map((row) => [row.fact_id, row]))
171
172
  const found = findPlaceholders(sourceText)
172
173
  const errors = []