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.
- package/dist/cli/index.js +15 -15
- package/dist/cli-node/index.js +15 -15
- package/dist/index.js +14 -14
- package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
- package/dist/skills/ulw-research/SKILL.md +58 -1
- package/dist/skills/ulw-research/references/deliverable-phase.md +51 -9
- package/dist/skills/ulw-research/references/report-gates.md +2 -1
- package/dist/skills/ulw-research/scripts/contracts.mjs +3 -11
- package/dist/skills/ulw-research/scripts/contracts.test.ts +1 -0
- package/dist/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
- package/dist/skills/ulw-research/scripts/copy-returns-polish.test.ts +6 -6
- package/dist/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
- package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +14 -22
- package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +7 -2
- package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +18 -4
- package/dist/skills/ulw-research/scripts/fact-commands.mjs +6 -47
- package/dist/skills/ulw-research/scripts/fact-fill.mjs +3 -2
- package/dist/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
- package/dist/skills/ulw-research/scripts/report-tools.test.ts +245 -4
- package/dist/skills/ulw-research/scripts/source-commands.mjs +8 -3
- package/dist/tui.js +14 -14
- package/package.json +14 -14
- package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
- package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
- package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
- package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
- package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
- package/packages/omo-codex/plugin/components/rules/package.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
- package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
- package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
- package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
- package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
- package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
- package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
- package/packages/omo-codex/plugin/package-lock.json +12 -12
- package/packages/omo-codex/plugin/package.json +1 -1
- package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +58 -1
- package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +51 -9
- package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +2 -1
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +3 -11
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +14 -22
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +7 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +18 -4
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +6 -47
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +3 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +8 -3
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +2 -2
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +58 -1
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +51 -9
- package/packages/shared-skills/skills/ulw-research/references/report-gates.md +2 -1
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +3 -11
- package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +1 -0
- package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
- package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +6 -6
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +14 -22
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +7 -2
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +18 -4
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +6 -47
- package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +3 -2
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +245 -4
- 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
|
|
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
|
|
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>` |
|
|
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)
|
|
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.
|
|
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
|
|
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
|
|
53
|
-
if (
|
|
54
|
-
|
|
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,
|
|
129
|
-
|
|
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
|
|
144
|
-
if (
|
|
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
|
|
178
|
-
const
|
|
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
|
|
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,
|
|
2
|
-
//
|
|
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 {
|
|
10
|
+
import { join, resolve } from "node:path"
|
|
11
11
|
|
|
12
|
-
import { CliError, parseArgs, sha256Of
|
|
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
|
|
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
|
-
|
|
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 (!
|
|
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 = []
|