oh-my-opencode 5.1.46 → 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/.agents/command/publish.md +3 -4
- package/.agents/skills/publish/SKILL.md +3 -4
- package/.opencode/command/publish.md +3 -4
- package/dist/agents/atlas/agent.d.ts +3 -2
- package/dist/agents/sisyphus/index.d.ts +1 -0
- package/dist/agents/sisyphus/kimi-k3-1.d.ts +9 -0
- package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
- package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
- package/dist/agents/sisyphus-junior/index.d.ts +1 -0
- package/dist/agents/sisyphus-junior/kimi-k3-1.d.ts +6 -0
- package/dist/agents/types.d.ts +2 -2
- package/dist/cli/index.js +30 -22
- package/dist/cli/service-auth/keystore.d.ts +8 -3
- package/dist/cli-node/index.js +30 -22
- package/dist/index.js +635 -18
- package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
- package/dist/skills/ulw-research/SKILL.md +59 -2
- package/dist/skills/ulw-research/references/deliverable-phase.md +57 -8
- 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 +209 -0
- package/dist/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
- package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +21 -44
- package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
- package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
- package/dist/skills/ulw-research/scripts/fact-commands.mjs +8 -47
- package/dist/skills/ulw-research/scripts/fact-fill.mjs +3 -2
- package/dist/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
- package/dist/skills/ulw-research/scripts/fact-pairs.test.ts +7 -23
- package/dist/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
- package/dist/skills/ulw-research/scripts/references-fences.test.ts +25 -0
- package/dist/skills/ulw-research/scripts/report-tools.test.ts +329 -1
- package/dist/skills/ulw-research/scripts/source-commands.mjs +8 -3
- package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
- 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/dist/cli.js +1 -2
- 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/scripts/sync-skills.mjs +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 +57 -8
- 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 +21 -44
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +8 -47
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +3 -2
- package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
- 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/plugin/test/sync-skills-test-support.mjs +1 -1
- package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -5
- package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
- package/packages/shared-skills/skills/ulw-research/SKILL.md +59 -2
- package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +57 -8
- 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 +209 -0
- 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 +21 -44
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
- package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +8 -47
- package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +3 -2
- package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
- package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +7 -23
- package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
- package/packages/shared-skills/skills/ulw-research/scripts/references-fences.test.ts +25 -0
- package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +329 -1
- package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +8 -3
- package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
|
@@ -166,7 +166,64 @@ Never block collection on the shape of the deliverable, and never guess it eithe
|
|
|
166
166
|
1. **Derive first.** Name the lane (`template-strict`, `template-vibe`, `no-format`, `edit-existing`) and the promised formats from the request and its destination (the reference's section 3). When the request refers to an existing deliverable, decide its state with `node "$SKILL_DIR/scripts/report-tools.mjs" outcome state --deliverable <path> --session-dir "$SESSION_DIR"`: `partial` resumes the skeleton on disk, `complete` means edit-existing and never a regenerated report. `$SKILL_DIR` is this skill's own directory, the folder containing this SKILL.md.
|
|
167
167
|
2. **Read the requester's format memory** with the memory tool when the harness exposes one: the pointer `system/human/report-style.md`, then `reference/human-report-style.md`; take the choice recorded for the most similar context (same destination kind and audience) as the first option.
|
|
168
168
|
3. **Ask only what is still missing**: at most the reference's three questions (destination and format, audience and length, template lineage) in one call to the harness question tool (`ask_user_question` or `request_user_input`), non-blocking where the harness supports it, each with its default first, a free-text "describe the format" path, and "don't care, you decide". Collection starts without waiting; a late answer is folded in until the assembly worker starts, and after that it becomes a re-render request.
|
|
169
|
-
4. **
|
|
169
|
+
4. **Settle source fidelity in the same step.** Seven fields under `## Fidelity` in `brief.md` say what the report must stay faithful to; the reference's section 4 lists each field's values and default. Infer every field before asking anything; an explicit word in the request decides a field (`answered_by: request`), the destination, the sources, and the Phase 0 axes supply the rest (`answered_by: default`):
|
|
170
|
+
- `as of`: a version, release, tag, commit, date, quarter, "as of", "current", or "latest" in the request decides it; a pointed-at repository anchors on its HEAD or tag at run start, pointed-at documents on their own revisions, and a comparison of two anchors records `multiple: <a>, <b>; rows tagged`. Default `latest at run start, <run date>`, the date of the `$SESSION_DIR` timestamp. Ask when the topic is version-bound or time-sensitive (software, APIs, standards, prices, rates, rankings, laws, programs, schedules) and nothing names an anchor; a stable topic takes the default silently.
|
|
171
|
+
- `meaning check`: the QA tier decides (full: `on`, light: `off`); a chat answer on a legal, medical, financial, procurement, or public-facing question with no speed word is `on`. Ask when two explicit signals disagree: a document deliverable under a literal "quick" or "fast", or a light-tier answer that carries both a speed word and a precision demand; `on` goes first.
|
|
172
|
+
- `source floor`: "official docs only", "peer-reviewed only", "SEC filings" give `primary only` or `classes: <host suffixes>`; "from these only" gives `pointed only: <source ids, URLs, or paths>`; pointed-at sources with no restricting word are seeds, not a floor. Default `standard`. Ask when a restricting phrase sits beside an open-ended question whose answer lives outside that floor. Declare each source's provenance when you snapshot it (`source snapshot --provenance primary|secondary`, by the Phase 3b primary-source rule); `primary only` refuses every source not declared `primary`.
|
|
173
|
+
- `derived numbers`: "compare", "growth", "share", "estimate" give `allowed`; "only the sources' own numbers", "no estimates" give `none`. Default `allowed`. Ask when both kinds of phrase share one request. Record only `allowed` or `none` (any other value stops `fact fill` and `fact verify` with a usage error); a partial or free-text answer records `none`.
|
|
174
|
+
- `languages` (never asked): the report in the user's language or the one they named; sources `en`, plus the user's language for a local topic, plus the pointed-at documents' languages; quotes `original` when every source language equals the report language, else `original with translation beside`.
|
|
175
|
+
- `identifier classes` (never asked): from the Phase 0 axes' domain (software: version strings, package names, commit ids; finance: tickers, filing ids, fiscal periods; law: statute, article, and case numbers; science: gene names, DOIs, trial ids), else `none`. It is recorded only; no gate enforces it yet.
|
|
176
|
+
- `fallback` (never asked): `block-quote`, or `omit` on "drop anything you cannot verify"; no other value is accepted.
|
|
177
|
+
|
|
178
|
+
Ask nothing when the request, the destination, and the sources decide every field. Otherwise ask only the fields whose trigger fired, in the same question-tool call as the format questions (`ask_user_question` or `request_user_input`), non-blocking where the harness supports it, each with its default first, a free-text path, and "don't care, you decide" (it resolves to the first option, recorded `answered_by: user`). At most four questions ride in that call: `pending` fidelity fields first, then format Q1, Q2, Q3, then `as of`, `meaning check`, `source floor`, `derived numbers`; a question that loses its slot runs on its default and is named, with that default, in the reply that opens collection. When each reading of a field would make part of the request undeliverable, record it `pending` with no default. Collection starts regardless: `source snapshot`, `fact add`, `fact quote`, `fact derive`, `fact retract`, and `fact export` keep working while answers are pending; only the six reader-facing commands (`fact fill`, `fact verify`, `fact pairs`, `fact meaning-check`, `fact polish-apply`, `fact polish-diff`) and the assembly lane wait for the lock, and those commands exit 1 with `fidelity_interview_incomplete` while a field is `pending` or missing. When the assembly lane is about to start, a field still awaiting its answer keeps its default; a `pending` field is asked once more, blocking this time.
|
|
179
|
+
5. **Record it.** Write `## Deliverable` into `brief.md` (lane, state, formats, destination, audience, template, format description, each with `answered_by: user|default|request`) plus `## Fidelity` (the block below), and open the manifest in the same step: `node "$SKILL_DIR/scripts/report-tools.mjs" outcome init --promised <formats> --lane <lane> --session-dir "$SESSION_DIR"`. When the ask is to recreate, reproduce, or rebuild something visual, add `Reference compare: required` with the source (URL or file) and the moments to compare; the comparison runs on every round of the build, not only at the end.
|
|
180
|
+
|
|
181
|
+
#### The fidelity questions
|
|
182
|
+
|
|
183
|
+
Ask them in the user's language, translating these English texts faithfully and keeping the option order; `<run date>` is the `$SESSION_DIR` date.
|
|
184
|
+
|
|
185
|
+
- **`as of`**, header `As of`: "Which version or date must the facts hold for? Sources are snapshotted and quoted against that anchor."
|
|
186
|
+
1. `Latest as of today, <run date>` - The state published at the run's start; anything older is flagged as stale.
|
|
187
|
+
2. `Other: a named version, release, or date` - Type it (v3.2, 2026-Q3, a commit or tag). Every quote and number is checked against sources valid at that anchor.
|
|
188
|
+
3. `Don't care, you decide` - Resolves to option 1.
|
|
189
|
+
- **`meaning check`**, header `Drift check`: "Numbers and quotes are always script-checked against source snapshots. Should every explanatory sentence also be read against its source quote by a separate reader? Slower and more tokens; it catches widened scope and dropped hedges."
|
|
190
|
+
1. `On` - Every sentence is paired with its source and read; confirmed drift is rewritten from the quote or listed in the briefing.
|
|
191
|
+
2. `Off (faster)` - Script checks only: numbers, units, quotes, qualifiers, bindings. A paraphrase that shifts meaning is not read.
|
|
192
|
+
3. `Other: where to focus` - Type it ("the financial sections", "anything about the competitor"); those pairs go to the strongest reader.
|
|
193
|
+
4. `Don't care, you decide` - Resolves to option 1.
|
|
194
|
+
- **`source floor`**, header `Sources`: "Which sources may back a statement of fact?"
|
|
195
|
+
1. `Standard: primary source required for high-risk claims` - Numeric, legal, financial, dated, and causal claims need a primary source plus two independent domains; other claims may cite attributed secondary sources.
|
|
196
|
+
2. `Primary only` - Every fact traces to a first-party source (standard, filing, dataset, official doc); commentary appears only as attributed opinion.
|
|
197
|
+
3. `Only the sources I pointed at` - Ledger rows come from the named files and URLs only; anything else is context, not evidence.
|
|
198
|
+
4. `Other: name the source classes` - Type it ("peer-reviewed only", "no vendor blogs").
|
|
199
|
+
5. `Don't care, you decide` - Resolves to option 1.
|
|
200
|
+
- **`derived numbers`**, header `Derived`: "May the report state numbers no source states (sums, growth rates, shares, unit conversions, estimates)?"
|
|
201
|
+
1. `Yes, computed by script and labeled DERIVED` - Each one names its operation and input rows; inputs are checked for unit, period, and population compatibility; an estimate carries its assumptions.
|
|
202
|
+
2. `No, sourced numbers only` - Every number is a MEASURED ledger value; comparisons are stated in words or shown side by side.
|
|
203
|
+
3. `Other: describe` - Type it ("growth rates yes, estimates no"). A partial allowance is recorded as `none`, and the reply that opens collection says so.
|
|
204
|
+
4. `Don't care, you decide` - Resolves to option 1.
|
|
205
|
+
|
|
206
|
+
#### The `## Fidelity` block
|
|
207
|
+
|
|
208
|
+
Write it in the same step as `## Deliverable`, provisional defaults included, one line per field with its `answered_by`, plus an `asked:` line naming the fidelity questions sent:
|
|
209
|
+
|
|
210
|
+
```text
|
|
211
|
+
## Fidelity
|
|
212
|
+
as of: latest at run start, 2026-10-23 (answered_by: default)
|
|
213
|
+
meaning check: on (answered_by: default)
|
|
214
|
+
source floor: standard (answered_by: default)
|
|
215
|
+
derived numbers: pending (answered_by: pending)
|
|
216
|
+
languages: report en; sources en; quotes original (answered_by: default)
|
|
217
|
+
identifier classes: version strings, package names, commit ids (answered_by: default)
|
|
218
|
+
fallback: block-quote (answered_by: default)
|
|
219
|
+
asked: derived numbers
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
Values carry no parentheses; `pending` goes only with `answered_by: pending`. When an answer lands, rewrite that one line with `answered_by: user` and leave `asked:` as history; a late answer after `fact fill` is a re-render: fill again, then verify.
|
|
223
|
+
|
|
224
|
+
**Re-entry.** An `edit-existing` session, or any session created before this block existed, has no `## Fidelity`. Before any `fact` command, run the inference step above and write the block with `answered_by: default` on every field. `claims index` on a free-form `claim-graph.md` (no claim table) returns `{claims: [], errors: [...]}`, exits 2, and writes no `claims.json`; run `fact verify` without it, and the gate reports each unbound sentence as `unbound_sentence` (major) instead of blocking.
|
|
225
|
+
|
|
226
|
+
**Calibration.** `fact meaning-check` appends one line per flagged pair to `--calibration` (default `$SESSION_DIR/fidelity-calibration.jsonl`). After each run, also append that run's new lines to the workspace-level `.omo/ulw-research/fidelity-calibration.jsonl`; promotion of `meaning_drift_confirmed` is decided from that log.
|
|
170
227
|
|
|
171
228
|
Phase 5 opens by turning the recorded fields into `design-spec.md`.
|
|
172
229
|
|
|
@@ -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.
|
|
@@ -133,7 +175,12 @@ Nothing reaches the requester until the gates for its QA tier pass, in this orde
|
|
|
133
175
|
node "$SKILL_DIR/scripts/report-tools.mjs" layout-probe --json # .source is one async expression
|
|
134
176
|
```
|
|
135
177
|
4. **Visual QA.** Render the artifact to images (PDF pages to PNG, the HTML in the browser skill's engine at desktop and phone widths) and look at the pixels for what a machine cannot see: composition, hierarchy, page breaks, figure legibility.
|
|
136
|
-
5. **Proofread.** A dedicated writing lane proofreads the final text only (grammar, spelling, punctuation, terminology, native register); it never composes or rewrites content.
|
|
178
|
+
5. **Proofread.** A dedicated writing lane proofreads the final text only (grammar, spelling, punctuation, terminology, native register); it never composes or rewrites content and never edits the file. Edits change text only; an edit whose before/after would alter any tag, attribute, comment, or script/style content is rejected, and markup changes go through the full fidelity gates instead. It returns `polish-edits.json` (`[{before, after, reason}]`), which the orchestrator applies edit by edit; a rejected or unlocated edit goes back to the lane with its reason, within the repair budget. The backstop diff, `fact verify` on the polished file, and the meaning readers on the pairs whose sentences polish changed run before the fidelity gate is recorded again. Edits must not span entities (such as & or <) or markup; narrow the edit to the surrounding words instead, and a rejected edit comes back with the reason to act on.
|
|
179
|
+
```bash
|
|
180
|
+
node "$SKILL_DIR/scripts/report-tools.mjs" fact polish-apply "$SESSION_DIR/report.html" --edits "$SESSION_DIR/polish-edits.json" --out "$SESSION_DIR/report.polished.html" --session-dir "$SESSION_DIR"
|
|
181
|
+
node "$SKILL_DIR/scripts/report-tools.mjs" fact polish-diff --before "$SESSION_DIR/report.html" --after "$SESSION_DIR/report.polished.html" --session-dir "$SESSION_DIR" # writes polish-defects.json
|
|
182
|
+
node "$SKILL_DIR/scripts/report-tools.mjs" fact pairs "$SESSION_DIR/report.polished.html" --changed-since "$SESSION_DIR/report.html.fill-map.json" --session-dir "$SESSION_DIR"
|
|
183
|
+
```
|
|
137
184
|
|
|
138
185
|
**Every reader-facing edition is verified.** The main report, the inline edition (section 9), LaTeX per-section sources, and slide sources are filled by `fact fill` from one placeholder source and each is verified by `fact verify` into its own record; PDF and DOCX are renders of a verified source. A delivered row is recorded with `outcome set <format> delivered --path <file> --verified-by <record>`, where the record's `artifact_sha256` equals the sha256 of the delivered source; a PDF or DOCX adds `--rendered-from <source>` and is matched against that source. In a session with a fact ledger, `outcome verify` refuses a delivered row without a matching record (`edition_unverified`, blocker, integrity). Markdown and LaTeX verification arrive with the Markdown stage; until then `fact verify --format md|tex` exits `2`, and such an edition is recorded `blocked_capability` with that reason rather than delivered unverified.
|
|
139
186
|
|
|
@@ -151,7 +198,7 @@ node "$SKILL_DIR/scripts/report-tools.mjs" repair decide --state "$SESSION_DIR/r
|
|
|
151
198
|
|
|
152
199
|
- `action: repair`: fix the listed defects, re-render, re-run the gate, and ask again.
|
|
153
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.
|
|
154
|
-
- `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.
|
|
155
202
|
|
|
156
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.
|
|
157
204
|
|
|
@@ -209,15 +256,17 @@ Invoke every command as `node "$SKILL_DIR/scripts/report-tools.mjs" <command>`.
|
|
|
209
256
|
| `outcome briefing` | `--json`, `--ledger <file>`, `--session-dir <dir>`, `--now <ISO>` | never |
|
|
210
257
|
| `format-extract <reference>` | `--out <design-spec.md>` (required), `--from-url` | never (an unsupported reference exits `2`) |
|
|
211
258
|
| `--help` | `--json` | never |
|
|
212
|
-
| `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`) |
|
|
213
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) |
|
|
214
261
|
| `fact quote` | `--source S<n> --match <text>` (required), `--label`, `--json`, `--session-dir <dir>` | `fact_not_located`, `fact_ambiguous` |
|
|
215
262
|
| `fact retract <id>` | `--reason <text>` (required), `--replacement <id>`, `--session-dir <dir>` | never (an illegal transition exits `2`) |
|
|
216
263
|
| `fact export` | `--csv`, `--out <file>`, `--session-dir <dir>` | never |
|
|
217
|
-
| `fact fill <src>` | `--out <file>` or `--view`, `--format html\|md\|tex`, `--inline`, `--replace-failed`, `--session-dir <dir>` | a fill error
|
|
218
|
-
| `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 |
|
|
219
|
-
| `fact pairs <report>` | `--format html\|md`, `--ledger <fact-ledger.json>`, `--claims <claims.json>`, `--changed-since <fill-map.json>` (
|
|
220
|
-
| `fact
|
|
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) |
|
|
221
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) |
|
|
222
271
|
| `claims index <claim-graph.md>` | `--out <claims.json>` (required) | never (a malformed claim table exits `2`) |
|
|
223
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
|
+
}
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
// fact-commands-meaning.mjs - `fact pairs` and `fact meaning-check`, the script half of the
|
|
2
2
|
// separate-session meaning verifier. fact pairs writes the (source, report sentence) pairs a
|
|
3
|
-
// reader lane gets and nothing else
|
|
3
|
+
// reader lane gets and nothing else (with --changed-since, only the pairs whose sentence polish
|
|
4
|
+
// rewrote); it honours the brief's `meaning check` field and writes
|
|
4
5
|
// `{ skipped: true }` when it is off. fact meaning-check ingests the readers' verdict files
|
|
5
6
|
// (meaning-check.mjs), writes fidelity-meaning-defects.json (a fidelity record bound to the pairs'
|
|
6
7
|
// artifact_sha256 and report, through writeFidelityRecord), and records the run in
|
|
7
8
|
// fidelity-calibration.jsonl: a header with the promotion criterion on first write, then per run
|
|
8
9
|
// one run line plus one line per flagged pair. The run id hashes the inputs only, so a rerun or a
|
|
9
10
|
// --dispute rerun of the same inputs replaces that run's lines in place (disputes carry forward)
|
|
10
|
-
// 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.
|
|
11
13
|
|
|
12
14
|
import { existsSync, readFileSync } from "node:fs"
|
|
13
15
|
import { dirname, join, resolve } from "node:path"
|
|
14
16
|
|
|
15
17
|
import { CliError, parseArgs, readJsonFile, sha256Of, writeJsonAtomic, writeTextAtomic } from "./cli-support.mjs"
|
|
16
18
|
import { countDefects } from "./contracts.mjs"
|
|
17
|
-
import { markdownAsHtml, readEdition } from "./fact-commands-verify.mjs"
|
|
18
|
-
import {
|
|
19
|
-
import { validateLedger } from "./fact-ledger-schema.mjs"
|
|
19
|
+
import { ledgerFrom, markdownAsHtml, readEdition, sessionOf } from "./fact-commands-verify.mjs"
|
|
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"
|
|
@@ -31,45 +33,20 @@ export const PROMOTION = Object.freeze({ code: "meaning_drift_confirmed", to: "i
|
|
|
31
33
|
/** The command status: fail when any defect blocks delivery under the registry policy. */
|
|
32
34
|
export const meaningStatus = (defects) => (defects.some(blocksDelivery) ? "fail" : "pass")
|
|
33
35
|
|
|
34
|
-
const sessionOf = (flags) => {
|
|
35
|
-
const dir = flags["session-dir"] ?? process.env.SESSION_DIR
|
|
36
|
-
return dir && dir !== true ? resolve(dir) : null
|
|
37
|
-
}
|
|
38
36
|
const inSession = (session, name, flag) => {
|
|
39
37
|
if (session === null) throw new CliError(`${flag} is required without --session-dir (or SESSION_DIR)`)
|
|
40
38
|
return join(session, name)
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
function readLedgerFrom(flags, session) {
|
|
44
|
-
if (flags.ledger === undefined) {
|
|
45
|
-
if (session === null) throw new CliError("--ledger is required without --session-dir (or SESSION_DIR)")
|
|
46
|
-
return readLedger(session)
|
|
47
|
-
}
|
|
48
|
-
const ledger = readJsonFile(flags.ledger)
|
|
49
|
-
const errors = validateLedger(ledger)
|
|
50
|
-
if (errors.length > 0) throw new CliError(`${flags.ledger} is not a valid fact ledger: ${errors[0]}`)
|
|
51
|
-
return ledger
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function briefState(flags, session) {
|
|
55
|
-
const path = flags.brief ?? inSession(session, "brief.md", "--brief")
|
|
56
|
-
const lock = requireLockedBrief(path)
|
|
57
|
-
if (lock.exitCode === 2) throw new CliError(`${path}: malformed ## Fidelity block: ${lock.errors.join("; ")}`)
|
|
58
|
-
return { path, locked: lock.locked, off: !meaningCheckEnabled(lock.parsed) }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
41
|
function runPairs(args) {
|
|
62
42
|
const [report] = args.positionals
|
|
63
43
|
const { flags } = args
|
|
64
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]")
|
|
65
45
|
const session = sessionOf(flags)
|
|
66
46
|
const out = resolve(flags.out ?? inSession(session, PAIRS_FILE, "--out"))
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
return { json, summary: `fact pairs: ${json.message}`, exitCode: 1 }
|
|
71
|
-
}
|
|
72
|
-
if (brief.off) {
|
|
47
|
+
const { parsed, refusal } = lockedBrief("fact pairs", flags)
|
|
48
|
+
if (refusal) return refusal
|
|
49
|
+
if (!meaningCheckEnabled(parsed)) {
|
|
73
50
|
const record = { schemaVersion: 1, skipped: true, reason: "meaning check: off", pairs: [] }
|
|
74
51
|
writeJsonAtomic(out, record)
|
|
75
52
|
return { json: { command: "fact pairs", status: "skipped", exitCode: 0, data: { out, ...record } }, summary: "fact pairs: skipped (meaning check: off)", exitCode: 0 }
|
|
@@ -77,10 +54,10 @@ function runPairs(args) {
|
|
|
77
54
|
const edition = readEdition(report, flags)
|
|
78
55
|
const claimsPath = flags.claims ?? (session === null ? null : join(session, "claims.json"))
|
|
79
56
|
const claims = claimsPath !== null && (flags.claims !== undefined || existsSync(claimsPath)) ? readJsonFile(claimsPath) : null
|
|
80
|
-
const
|
|
81
|
-
const ledger = readLedgerFrom(flags, session)
|
|
57
|
+
const ledger = ledgerFrom(flags, session)
|
|
82
58
|
const html = edition.format === "md" ? markdownAsHtml(edition.text, edition.entries, ledger).html : edition.text
|
|
83
|
-
const
|
|
59
|
+
const all = extractPairs(html, ledger, claims)
|
|
60
|
+
const pairs = flags["changed-since"] === undefined ? all : pairsChangedSince(all, flags["changed-since"], ledger, claims)
|
|
84
61
|
const counts = { total: pairs.length, force_strong: pairs.filter((pair) => pair.force_strong).length }
|
|
85
62
|
for (const kind of PAIR_KINDS) counts[kind] = pairs.filter((pair) => pair.kind === kind).length
|
|
86
63
|
const record = { schemaVersion: 1, skipped: false, report: resolve(report), format: edition.format, artifact_sha256: sha256Of(edition.text), pairs_sha256: sha256Of(JSON.stringify(pairs)), counts, verdict_schema: VERDICT_SCHEMA, pairs }
|
|
@@ -140,23 +117,23 @@ function recordCalibration(path, lines, runId, pairsSha) {
|
|
|
140
117
|
}
|
|
141
118
|
|
|
142
119
|
/** A `{ skipped: true }` pairs file is accepted only when the locked brief turns the meaning check off. */
|
|
143
|
-
function skippedRefusal(flags,
|
|
144
|
-
|
|
145
|
-
if (!brief.locked) return { error: FIDELITY_INCOMPLETE, message: `${brief.path} has no locked ## Fidelity block, so a skipped meaning check cannot be accepted` }
|
|
146
|
-
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` }
|
|
147
122
|
return null
|
|
148
123
|
}
|
|
149
124
|
|
|
150
125
|
function runMeaningCheck(args) {
|
|
151
126
|
const { flags } = args
|
|
152
|
-
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]"
|
|
153
128
|
if (flags.pairs === undefined || !Array.isArray(flags.verdicts)) throw new CliError(usage)
|
|
154
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 } }
|
|
155
132
|
const pairsText = readFileSync(flags.pairs, "utf8")
|
|
156
133
|
const pairsDoc = readJsonFile(flags.pairs)
|
|
157
134
|
if (pairsDoc?.skipped === true) {
|
|
158
|
-
const
|
|
159
|
-
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 }
|
|
160
137
|
return { json: { command: "fact meaning-check", status: "skipped", exitCode: 0, defects: [] }, summary: JSON.stringify({ command: "fact meaning-check", status: "skipped" }), exitCode: 0 }
|
|
161
138
|
}
|
|
162
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`)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// fact-commands-polish.mjs - `fact polish-apply` and `fact polish-diff`, the script half of the
|
|
2
|
+
// proofread gate, plus the --changed-since filter of `fact pairs`.
|
|
3
|
+
//
|
|
4
|
+
// The writing lane returns polish-edits.json ([{before, after, reason}]) and never edits the
|
|
5
|
+
// report. polish-apply applies each edit through polish-apply.mjs and writes the result; a
|
|
6
|
+
// rejected (major) or unlocated (minor) edit leaves its region unchanged and is listed with its
|
|
7
|
+
// index and reason so the orchestrator can re-send it; every rejection reason is printed verbatim, one
|
|
8
|
+
// line per rejected edit (index and the first 60 characters of its before), in the summary and in
|
|
9
|
+
// data.rejected_reasons. It exits 0 whenever it wrote the output.
|
|
10
|
+
// polish-diff is the whole-document backstop (polish-diff.mjs) between the pre-polish artifact
|
|
11
|
+
// and any later copy; it exits 1 on a blocker. pairsChangedSince keeps only the pairs whose
|
|
12
|
+
// sentence text (normalized) is not in the pre-polish artifact that a fill map names, so the
|
|
13
|
+
// meaning readers re-check exactly what polish rewrote. No model, no network.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
16
|
+
import { join, resolve } from "node:path"
|
|
17
|
+
|
|
18
|
+
import { CliError, parseArgs, readJsonFile, sha256Of, writeJsonAtomic, writeTextAtomic } from "./cli-support.mjs"
|
|
19
|
+
import { countDefects, DEFECT_CODES } from "./contracts.mjs"
|
|
20
|
+
import { editionFormatOf, ledgerFrom, markdownAsHtml, sessionOf } from "./fact-commands-verify.mjs"
|
|
21
|
+
import { extractPairs } from "./fact-pairs.mjs"
|
|
22
|
+
import { lockedBrief } from "./fidelity-policy.mjs"
|
|
23
|
+
import { applyEdits, POLISH_EDIT_FIELDS, validatePolishEdits } from "./polish-apply.mjs"
|
|
24
|
+
import { diffPolish } from "./polish-diff.mjs"
|
|
25
|
+
import { normalizeText } from "./text-normalize.mjs"
|
|
26
|
+
|
|
27
|
+
export { ledgerFrom, POLISH_EDIT_FIELDS, validatePolishEdits }
|
|
28
|
+
|
|
29
|
+
export const POLISH_RECORD = "polish-defects.json"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The polish-diff exit policy: only a registry blocker fails the command; majors are reported.
|
|
33
|
+
* Fail-closed like the delivery barrier: a code the registry does not know refuses outright,
|
|
34
|
+
* because a caller-claimed severity may not demote it.
|
|
35
|
+
*/
|
|
36
|
+
export const polishDiffFails = (defects) =>
|
|
37
|
+
defects.some((defect) => {
|
|
38
|
+
if (!Object.hasOwn(DEFECT_CODES, defect?.code)) throw new CliError(`unknown defect code ${defect?.code}`)
|
|
39
|
+
return DEFECT_CODES[defect.code].severity === "blocker"
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function readText(path) {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(path, "utf8")
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const withSeverity = (entry) => ({ ...entry, severity: DEFECT_CODES[entry.code].severity, hint: DEFECT_CODES[entry.code].hint })
|
|
51
|
+
|
|
52
|
+
/** The first 60 characters of an edit's before, on one line. */
|
|
53
|
+
const beforePreview = (before) => (typeof before === "string" ? before : "").replace(/\s+/gu, " ").slice(0, 60)
|
|
54
|
+
|
|
55
|
+
/** One line per rejected edit: its index, the start of its before, and its reason verbatim. */
|
|
56
|
+
const rejectionLine = (entry, edit) => ` rejected edit ${entry.index} ("${beforePreview(edit?.before)}"): ${entry.reason}`
|
|
57
|
+
|
|
58
|
+
function runPolishApply(args) {
|
|
59
|
+
const [report] = args.positionals
|
|
60
|
+
const { flags } = args
|
|
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]")
|
|
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
|
|
65
|
+
const html = readText(report)
|
|
66
|
+
const editsText = readText(flags.edits)
|
|
67
|
+
const edits = readJsonFile(flags.edits)
|
|
68
|
+
try {
|
|
69
|
+
validatePolishEdits(edits)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new CliError(`${flags.edits} must be an array of {${Object.keys(POLISH_EDIT_FIELDS).join(", ")}} objects: ${error.message}`)
|
|
72
|
+
}
|
|
73
|
+
const result = applyEdits(html, edits, ledgerFrom(flags))
|
|
74
|
+
writeTextAtomic(flags.out, result.out)
|
|
75
|
+
const defects = [...result.rejected, ...result.unlocated].sort((a, b) => a.index - b.index).map(withSeverity)
|
|
76
|
+
const counts = { applied: result.applied.length, rejected: result.rejected.length, unlocated: result.unlocated.length }
|
|
77
|
+
const data = { out: resolve(flags.out), ...counts, applied_indexes: result.applied, input_sha256: sha256Of(html), edits_sha256: sha256Of(editsText), out_sha256: sha256Of(result.out) }
|
|
78
|
+
const rejected_reasons = result.rejected.map((entry) => rejectionLine(entry, edits[entry.index]))
|
|
79
|
+
const summary = [`fact polish-apply: ${counts.applied} applied, ${counts.rejected} rejected, ${counts.unlocated} unlocated of ${edits.length} edit(s)`, ...rejected_reasons].join("\n")
|
|
80
|
+
return { json: { command: "fact polish-apply", status: "pass", exitCode: 0, defects, data: { ...data, rejected_reasons } }, summary, exitCode: 0 }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runPolishDiff(args) {
|
|
84
|
+
const { flags } = args
|
|
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
|
|
88
|
+
const after = readText(flags.after)
|
|
89
|
+
const defects = diffPolish(readText(flags.before), after, ledgerFrom(flags)).map((defect) => ({ ...defect, hint: DEFECT_CODES[defect.code]?.hint ?? null }))
|
|
90
|
+
const counts = countDefects(defects)
|
|
91
|
+
const status = polishDiffFails(defects) ? "fail" : "pass"
|
|
92
|
+
const exitCode = status === "fail" ? 1 : 0
|
|
93
|
+
const record = { command: "fact polish-diff", status, exitCode, before: resolve(flags.before), artifact: resolve(flags.after), artifact_sha256: sha256Of(after), defects, counts }
|
|
94
|
+
const session = sessionOf(flags)
|
|
95
|
+
const out = flags.out ?? (session === null ? null : join(session, POLISH_RECORD))
|
|
96
|
+
if (out !== null) writeJsonAtomic(resolve(out), record)
|
|
97
|
+
const summary = `fact polish-diff: ${status.toUpperCase()} - ${counts.blocker} blocker, ${counts.major} major, ${counts.minor} minor`
|
|
98
|
+
return { json: { ...record, out: out === null ? null : resolve(out) }, summary, exitCode }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pairKey = (pair) => [pair.kind, pair.sentence_key, pair.claim_id ?? "", normalizeText(pair.sentence)].join("\u0000")
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The pairs whose sentence text changed since the pre-polish artifact a fill map names: a pair is
|
|
105
|
+
* kept unless the same (kind, sentence key, normalized sentence) occurs in that artifact. The map
|
|
106
|
+
* must carry the artifact's `artifact_sha256` and the bytes must still match it, so the filter is
|
|
107
|
+
* fail-closed on a hand-made or stale map.
|
|
108
|
+
* @param {object[]} pairs extractPairs output on the polished edition
|
|
109
|
+
* @param {string} mapPath the pre-polish fill map (fact fill's `<out>.fill-map.json`)
|
|
110
|
+
*/
|
|
111
|
+
export function pairsChangedSince(pairs, mapPath, ledger, claims) {
|
|
112
|
+
const map = readJsonFile(mapPath)
|
|
113
|
+
if (typeof map.artifact !== "string" || !existsSync(map.artifact)) throw new CliError(`${mapPath} names no readable pre-polish artifact`)
|
|
114
|
+
const text = readText(map.artifact)
|
|
115
|
+
if (typeof map.artifact_sha256 !== "string") throw new CliError(`${mapPath} fill map has no artifact_sha256; --changed-since needs the pre-polish hash`)
|
|
116
|
+
if (sha256Of(text) !== map.artifact_sha256) throw new CliError(`${map.artifact} changed after fill; --changed-since needs the pre-polish bytes`)
|
|
117
|
+
const html = map.format === "md" ? markdownAsHtml(text, map.entries, ledger).html : text
|
|
118
|
+
const before = new Map()
|
|
119
|
+
for (const pair of extractPairs(html, ledger, claims)) before.set(pairKey(pair), (before.get(pairKey(pair)) ?? 0) + 1)
|
|
120
|
+
return pairs.filter((pair) => {
|
|
121
|
+
const left = before.get(pairKey(pair)) ?? 0
|
|
122
|
+
if (left === 0) return true
|
|
123
|
+
before.set(pairKey(pair), left - 1)
|
|
124
|
+
return false
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {string[]} argv the arguments after `fact polish-apply` or `fact polish-diff` */
|
|
129
|
+
export function runFactPolish(sub, argv) {
|
|
130
|
+
const args = parseArgs(argv)
|
|
131
|
+
return sub === "polish-apply" ? runPolishApply(args) : runPolishDiff(args)
|
|
132
|
+
}
|