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
|
@@ -164,7 +164,64 @@ Never block collection on the shape of the deliverable, and never guess it eithe
|
|
|
164
164
|
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.
|
|
165
165
|
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.
|
|
166
166
|
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.
|
|
167
|
-
4. **
|
|
167
|
+
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`):
|
|
168
|
+
- `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.
|
|
169
|
+
- `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.
|
|
170
|
+
- `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`.
|
|
171
|
+
- `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`.
|
|
172
|
+
- `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`.
|
|
173
|
+
- `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.
|
|
174
|
+
- `fallback` (never asked): `block-quote`, or `omit` on "drop anything you cannot verify"; no other value is accepted.
|
|
175
|
+
|
|
176
|
+
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.
|
|
177
|
+
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.
|
|
178
|
+
|
|
179
|
+
#### The fidelity questions
|
|
180
|
+
|
|
181
|
+
Ask them in the user's language, translating these English texts faithfully and keeping the option order; `<run date>` is the `$SESSION_DIR` date.
|
|
182
|
+
|
|
183
|
+
- **`as of`**, header `As of`: "Which version or date must the facts hold for? Sources are snapshotted and quoted against that anchor."
|
|
184
|
+
1. `Latest as of today, <run date>` - The state published at the run's start; anything older is flagged as stale.
|
|
185
|
+
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.
|
|
186
|
+
3. `Don't care, you decide` - Resolves to option 1.
|
|
187
|
+
- **`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."
|
|
188
|
+
1. `On` - Every sentence is paired with its source and read; confirmed drift is rewritten from the quote or listed in the briefing.
|
|
189
|
+
2. `Off (faster)` - Script checks only: numbers, units, quotes, qualifiers, bindings. A paraphrase that shifts meaning is not read.
|
|
190
|
+
3. `Other: where to focus` - Type it ("the financial sections", "anything about the competitor"); those pairs go to the strongest reader.
|
|
191
|
+
4. `Don't care, you decide` - Resolves to option 1.
|
|
192
|
+
- **`source floor`**, header `Sources`: "Which sources may back a statement of fact?"
|
|
193
|
+
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.
|
|
194
|
+
2. `Primary only` - Every fact traces to a first-party source (standard, filing, dataset, official doc); commentary appears only as attributed opinion.
|
|
195
|
+
3. `Only the sources I pointed at` - Ledger rows come from the named files and URLs only; anything else is context, not evidence.
|
|
196
|
+
4. `Other: name the source classes` - Type it ("peer-reviewed only", "no vendor blogs").
|
|
197
|
+
5. `Don't care, you decide` - Resolves to option 1.
|
|
198
|
+
- **`derived numbers`**, header `Derived`: "May the report state numbers no source states (sums, growth rates, shares, unit conversions, estimates)?"
|
|
199
|
+
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.
|
|
200
|
+
2. `No, sourced numbers only` - Every number is a MEASURED ledger value; comparisons are stated in words or shown side by side.
|
|
201
|
+
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.
|
|
202
|
+
4. `Don't care, you decide` - Resolves to option 1.
|
|
203
|
+
|
|
204
|
+
#### The `## Fidelity` block
|
|
205
|
+
|
|
206
|
+
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:
|
|
207
|
+
|
|
208
|
+
```text
|
|
209
|
+
## Fidelity
|
|
210
|
+
as of: latest at run start, 2026-10-23 (answered_by: default)
|
|
211
|
+
meaning check: on (answered_by: default)
|
|
212
|
+
source floor: standard (answered_by: default)
|
|
213
|
+
derived numbers: pending (answered_by: pending)
|
|
214
|
+
languages: report en; sources en; quotes original (answered_by: default)
|
|
215
|
+
identifier classes: version strings, package names, commit ids (answered_by: default)
|
|
216
|
+
fallback: block-quote (answered_by: default)
|
|
217
|
+
asked: derived numbers
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
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.
|
|
221
|
+
|
|
222
|
+
**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.
|
|
223
|
+
|
|
224
|
+
**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.
|
|
168
225
|
|
|
169
226
|
Phase 5 opens by turning the recorded fields into `design-spec.md`.
|
|
170
227
|
|
|
@@ -373,7 +430,7 @@ Next, write `$SESSION_DIR/pairs-strong.json` (the same file, keeping only the pa
|
|
|
373
430
|
|
|
374
431
|
3. **Layout gates.** Print the probe with `node "$SKILL_DIR/scripts/report-tools.mjs" layout-probe --json`, evaluate its `source` in the rendered page through the `browser` skill's owned headless engine, save the result as `$SESSION_DIR/boxes.json`, and re-run `check` with `--layout "$SESSION_DIR/boxes.json"` (overflow, clipped text, distorted images, overlapping siblings).
|
|
375
432
|
4. **Visual QA.** Render the produced artifact back to images — PDF pages to PNG, the HTML through the browser skill at desktop and phone widths — and look at them: missing or broken figures, images stretched or spilling their containers, diagram or chart text rendered off the spec's font or palette, clipped tables, overflowing CJK text, blank pages, unlabeled chart values, wrong page breaks. Reading the source markup is not visual QA; inspect the pixels.
|
|
376
|
-
5. **Proofread gate: `task(category="writing", ...)`.** Hand the final text to a dedicated `writing` worker whose only job is language: grammar, spelling, punctuation, terminology consistency, and whether the prose reads NATIVELY in the report's own language. It proofreads only and never composes
|
|
433
|
+
5. **Proofread gate: `task(category="writing", ...)`.** Hand the final text to a dedicated `writing` worker whose only job is language: grammar, spelling, punctuation, terminology consistency, and whether the prose reads NATIVELY in the report's own language. It proofreads only and never composes, and it never edits the file: it returns `$SESSION_DIR/polish-edits.json`, an array of `{"before": ..., "after": ..., "reason": ...}` where `before` is the exact current text of one or more adjacent sentences. 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. Apply it with `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"`: each edit is applied alone, and an edit that changes a fact span, a numeral, a link, or a claim id, or drops a hedge or qualifier the source carries, is `polish_edit_rejected` (a `before` not found exactly once is `polish_edit_unlocated`) and leaves its text unchanged. Send the rejected and unlocated edits back to the same worker with their reasons, plus `$SESSION_DIR/polish-defects.json` and the pre-polish `report.html` the backstop compared against, so the worker sees why each edit was refused; it returns either a rewritten sentence or a new edit list, and the repair round re-runs `fact polish-apply` with `"$SESSION_DIR/report.polished.html"` as both input and output, within the repair budget. Then run the backstop `fact polish-diff --before "$SESSION_DIR/report.html" --after "$SESSION_DIR/report.polished.html" --session-dir "$SESSION_DIR"` (exit `1` on `polish_diff_fail`), `fact verify` on `report.polished.html`, and `fact pairs "$SESSION_DIR/report.polished.html" --changed-since "$SESSION_DIR/report.html.fill-map.json" --session-dir "$SESSION_DIR"`, which keeps only the quote and claim pairs whose sentence text changed during polish; run the meaning readers above (cheap, then strong on the flagged pairs) on exactly those pairs and `fact meaning-check` before recording `outcome gate fidelity`. A rewording that keeps every number, hedge, qualifier, and link but swaps who did what to whom therefore still reaches a meaning reader on the delivered text. `report.polished.html` is the report that is delivered. Deliver only on a clean pass. This gate runs BEFORE the first delivery, not after the user finds the typo. 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.
|
|
377
434
|
|
|
378
435
|
**Repair is bounded.** After every gate run that found defects, ask `node "$SKILL_DIR/scripts/report-tools.mjs" repair decide --state "$SESSION_DIR/repair-state.json" --defects "$SESSION_DIR/defects.json" --defects "$SESSION_DIR/fidelity-defects.json" --artifact-bytes <bytes> --renders <pages> --session-dir "$SESSION_DIR"` and obey it: `repair` means fix and re-run; `deliver` means stop and ship with the residual defects the manifest now lists; `block` means a content-integrity defect remains (or there is no usable artifact), so do not deliver: tell the user what blocks it and record the row as `failed` with the reason. Never repair past a stop decision (3 attempts, a 2-attempt plateau, an oscillation, or 15 minutes; the reference's section 8). Fidelity defects ride the same loop (`--defects "$SESSION_DIR/defects.json" --defects "$SESSION_DIR/fidelity-defects.json"`), but a `deliver` decision does not release them: while `fidelity-defects.json` lists `untraceable_number`, `value_mismatch`, `unit_mismatch`, `quote_not_verbatim`, `quote_unbound`, `unbound_sentence`, or `chart_value_untraceable`, `outcome verify` fails with `fidelity_unresolved` and `outcome finish` refuses, even with the repair budget spent. The only ways out are a ledger row, an `assumed` row with its rationale, or removing the sentence.
|
|
379
436
|
|
|
@@ -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,156 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs"
|
|
3
|
+
import { tmpdir } from "node:os"
|
|
4
|
+
import { join } from "node:path"
|
|
5
|
+
|
|
6
|
+
import { FILL_INPUT_FORMAT_RULES, FILL_INPUT_FORMATS } from "./fact-commands-fill.mjs"
|
|
7
|
+
import { KIND_RULES } from "./fact-ledger-kinds.mjs"
|
|
8
|
+
import { FILL_FORMATS } from "./fact-fill.mjs"
|
|
9
|
+
import { meaningCheckEnabled as briefMeaningCheckEnabled } from "./fidelity-brief.mjs"
|
|
10
|
+
import { FIDELITY_VALUE_RULES, meaningCheckEnabled, parseSourceFloor, policyErrors, SOURCE_FLOOR_RULES, sourceInsideFloor } from "./fidelity-policy.mjs"
|
|
11
|
+
|
|
12
|
+
type PolicyRow = { fact_id: string; kind: string; status: string; source_id: string | null; operands: string[]; derived_from: string[] }
|
|
13
|
+
const policyRow = (fact_id: string, kind: string, source_id: string | null, operands: string[] = []): PolicyRow => ({ fact_id, kind, status: "active", source_id, operands, derived_from: [] })
|
|
14
|
+
|
|
15
|
+
/** policyErrors over one placeholder per row id under a parsed floor, as code and message pairs. */
|
|
16
|
+
function floorRefusals(rows: PolicyRow[], ids: string[], floor: ReturnType<typeof parseSourceFloor>) {
|
|
17
|
+
const found = ids.map((id, index) => ({ token: `{{${id}}}`, start: index, parsed: { id } }))
|
|
18
|
+
const refused = policyErrors(ids.map((id) => `{{${id}}}`).join(""), found, { rows }, tmpdir(), { derivedNone: false, floor })
|
|
19
|
+
return refused.errors.map((error: { code: string; message: string }) => [error.code, error.message])
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const referenceValues = async (field: string) => {
|
|
23
|
+
const row = (await Bun.file(REFERENCE).text()).split("\n").find((line) => line.startsWith(`| \`${field}\` |`)) ?? ""
|
|
24
|
+
return [...(row.split("|")[2] ?? "").matchAll(/`([^`]+)`/gu)].map((match) => match[1])
|
|
25
|
+
}
|
|
26
|
+
import { pointedFlag } from "./source-commands.mjs"
|
|
27
|
+
|
|
28
|
+
const REFERENCE = join(import.meta.dir, "..", "references", "deliverable-phase.md")
|
|
29
|
+
|
|
30
|
+
function briefWithFloor(floor: string) {
|
|
31
|
+
const dir = mkdtempSync(join(tmpdir(), "policy-registry-"))
|
|
32
|
+
const lines = ["as of: latest at run start, 2026-10-23", "meaning check: on", `source floor: ${floor}`, "derived numbers: allowed", "languages: report en; sources en; quotes original", "identifier classes: none", "fallback: block-quote"]
|
|
33
|
+
writeFileSync(join(dir, "brief.md"), `## Fidelity\n${lines.map((line) => `${line} (answered_by: default)`).join("\n")}\n`)
|
|
34
|
+
return dir
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const floorOf = (text: string) => {
|
|
38
|
+
const parsed = parseSourceFloor(text)
|
|
39
|
+
if (parsed === null) throw new Error(`parseSourceFloor(${text}) read no floor`)
|
|
40
|
+
return parsed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const derivedRefusal = (kind: string) => {
|
|
44
|
+
const row = { fact_id: "F1", kind, status: "active", source_id: null, operands: [], derived_from: [] }
|
|
45
|
+
const found = [{ token: "{{F1}}", start: 0, parsed: { id: "F1" } }]
|
|
46
|
+
const refused = policyErrors("{{F1}}", found, { rows: [row] }, tmpdir(), { derivedNone: true, floor: null })
|
|
47
|
+
return refused.errors.some((error: { code: string }) => error.code === "derived_not_allowed")
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("fidelity-policy registry: copies must not return", () => {
|
|
51
|
+
test("#given every ledger kind #when derived numbers none refuses #then policyErrors agrees with FIDELITY_VALUE_RULES and assumed is refused", () => {
|
|
52
|
+
const kinds = Object.keys(KIND_RULES)
|
|
53
|
+
for (const kind of kinds) expect({ kind, refused: derivedRefusal(kind) }).toEqual({ kind, refused: FIDELITY_VALUE_RULES.derivedRefused(kind) })
|
|
54
|
+
for (const kind of FIDELITY_VALUE_RULES.derivedKinds) expect(kinds).toContain(kind)
|
|
55
|
+
expect(FIDELITY_VALUE_RULES.derivedRefused("assumed")).toBe(true)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("#given each source floor form #when parsed #then the rule matches the registry and an unknown form is a usage error", () => {
|
|
59
|
+
expect(parseSourceFloor("standard")).toBeNull()
|
|
60
|
+
expect(parseSourceFloor("Primary Only")).toEqual({ kind: "primary" })
|
|
61
|
+
expect(parseSourceFloor("pointed only: S4, https://a.example/x")).toEqual({ kind: "pointed", list: ["S4", "https://a.example/x"] })
|
|
62
|
+
expect(parseSourceFloor("classes: example.com, gov.uk")).toEqual({ kind: "classes", list: ["example.com", "gov.uk"] })
|
|
63
|
+
for (const rule of SOURCE_FLOOR_RULES) expect(Object.isFrozen(rule)).toBe(true)
|
|
64
|
+
expect(() => parseSourceFloor("only nice sources")).toThrow(/source floor/)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test("#given pointed and classes floors #when evaluated #then ids, urls, hosts, and suffixes match and notexample.com does not", () => {
|
|
68
|
+
const pointed = floorOf("pointed only: S4, https://a.example/x")
|
|
69
|
+
expect(sourceInsideFloor(pointed, "S4", { url: null })).toBe(true)
|
|
70
|
+
expect(sourceInsideFloor(pointed, "S9", { url: "https://a.example/x" })).toBe(true)
|
|
71
|
+
expect(sourceInsideFloor(pointed, "S9", { url: "https://a.example/y" })).toBe(false)
|
|
72
|
+
const classes = floorOf("classes: example.com")
|
|
73
|
+
for (const [url, inside] of [["https://example.com/a", true], ["https://docs.example.com/a", true], ["https://notexample.com/a", false], [null, false]] as const) {
|
|
74
|
+
expect({ url, inside: sourceInsideFloor(classes, "S1", { url }) }).toEqual({ url, inside })
|
|
75
|
+
}
|
|
76
|
+
expect(sourceInsideFloor({ kind: "primary" }, "S1", { provenance: "primary" })).toBe(true)
|
|
77
|
+
expect(sourceInsideFloor({ kind: "primary" }, "S1", { provenance: "secondary" })).toBe(false)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
test("#given a brief floor #when pointedFlag reads it #then it agrees with parseSourceFloor for every form", () => {
|
|
81
|
+
const url = "https://a.example/x"
|
|
82
|
+
for (const floor of ["pointed only: S4, https://a.example/x", "pointed only: S4", "standard", "primary only", "classes: a.example"]) {
|
|
83
|
+
const dir = briefWithFloor(floor)
|
|
84
|
+
try {
|
|
85
|
+
const rule = (() => {
|
|
86
|
+
try {
|
|
87
|
+
return parseSourceFloor(floor)
|
|
88
|
+
} catch {
|
|
89
|
+
return null
|
|
90
|
+
}
|
|
91
|
+
})()
|
|
92
|
+
expect({ floor, flag: pointedFlag(dir, url) }).toEqual({ floor, flag: rule?.kind === "pointed" && rule.list.includes(url) })
|
|
93
|
+
} finally {
|
|
94
|
+
rmSync(dir, { recursive: true, force: true })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("#given a malformed or pending floor #when pointedFlag reads it #then malformed is a usage error and pending is not pointed", () => {
|
|
100
|
+
const malformed = briefWithFloor("not a floor")
|
|
101
|
+
const pending = mkdtempSync(join(tmpdir(), "policy-registry-"))
|
|
102
|
+
writeFileSync(join(pending, "brief.md"), "## Fidelity\nsource floor: pending (answered_by: pending)\n")
|
|
103
|
+
try {
|
|
104
|
+
expect(() => pointedFlag(malformed, "https://a.example/x")).toThrow(/source floor "not a floor"/)
|
|
105
|
+
expect(pointedFlag(pending, "https://a.example/x")).toBe(false)
|
|
106
|
+
} finally {
|
|
107
|
+
for (const dir of [malformed, pending]) rmSync(dir, { recursive: true, force: true })
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test("#given derived numbers and fallback values #when read #then only their table values pass, any case, and anything else names field and value", () => {
|
|
112
|
+
expect(["none", "None", " none "].map((value) => FIDELITY_VALUE_RULES.derivedNone(value))).toEqual([true, true, true])
|
|
113
|
+
expect(["allowed", "Allowed"].map((value) => FIDELITY_VALUE_RULES.derivedNone(value))).toEqual([false, false])
|
|
114
|
+
for (const value of ["nope", "no", "n/a", "none.", "measured only"]) expect(() => FIDELITY_VALUE_RULES.derivedNone(value)).toThrow(`derived numbers "${value}" is not allowed, none`)
|
|
115
|
+
expect(["block-quote", "Omit"].map((value) => FIDELITY_VALUE_RULES.fallback(value))).toEqual(["quote", "omit"])
|
|
116
|
+
for (const value of ["drop", "omit sentence", "quote"]) expect(() => FIDELITY_VALUE_RULES.fallback(value)).toThrow(`fallback "${value}" is not block-quote, omit`)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("#given the deliverable reference #when its derived numbers and fallback rows are read #then they name exactly the registry tables", async () => {
|
|
120
|
+
for (const [field, table] of Object.entries(FIDELITY_VALUE_RULES.tables)) expect({ field, values: await referenceValues(field) }).toEqual({ field, values: Object.keys(table) })
|
|
121
|
+
expect(Object.keys(FIDELITY_VALUE_RULES.tables)).toEqual(["derived numbers", "fallback"])
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
test("#given sourceless rows #when judged under a strict floor and under standard #then strict refuses each, a derived row over one included, and standard refuses none", () => {
|
|
125
|
+
const rows = [policyRow("F1", "measured", "S1"), policyRow("A1", "assumed", null), policyRow("D1", "derived", null, ["F1", "A1"]), policyRow("D2", "derived", null, ["F9"])]
|
|
126
|
+
const ids = ["F1", "A1", "D1", "D2"]
|
|
127
|
+
expect(floorRefusals(rows, ids, floorOf("pointed only: S1"))).toEqual([
|
|
128
|
+
["source_outside_floor", "A1 rests on no source, outside the source floor"],
|
|
129
|
+
["source_outside_floor", "D1 rests on no source, outside the source floor"],
|
|
130
|
+
["source_outside_floor", "D2 rests on no source, outside the source floor"],
|
|
131
|
+
])
|
|
132
|
+
expect(floorRefusals(rows, ids, parseSourceFloor("standard"))).toEqual([])
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
test("#given the meaning check value rule #when read through the policy or the brief #then both are the same function", () => {
|
|
136
|
+
expect(meaningCheckEnabled).toBe(briefMeaningCheckEnabled)
|
|
137
|
+
for (const value of ["on", "off", "Off", "on; force strong: money", "offline"]) {
|
|
138
|
+
expect({ value, on: FIDELITY_VALUE_RULES.meaningCheck(value) }).toEqual({ value, on: meaningCheckEnabled({ fields: { "meaning check": value } }) })
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
test("#given the fill input vocabulary #when derived #then every input maps to a FILL_FORMATS member and every output format is reachable", () => {
|
|
143
|
+
const target = (format: string) => (FILL_INPUT_FORMAT_RULES.aliases as Record<string, string>)[format] ?? format
|
|
144
|
+
for (const format of FILL_INPUT_FORMATS) expect(FILL_FORMATS as readonly string[]).toContain(target(format))
|
|
145
|
+
for (const format of FILL_FORMATS) expect(FILL_INPUT_FORMATS as readonly string[]).toContain(format)
|
|
146
|
+
for (const format of Object.values(FILL_INPUT_FORMAT_RULES.extensions)) expect(FILL_INPUT_FORMATS as readonly string[]).toContain(format)
|
|
147
|
+
expect(FILL_INPUT_FORMAT_RULES.aliases).toEqual({ tex: "md" })
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test("#given the deliverable reference #when its source floor row is read #then it names exactly the SOURCE_FLOOR_RULES forms", async () => {
|
|
151
|
+
const row = (await Bun.file(REFERENCE).text()).split("\n").find((line) => line.startsWith("| `source floor` |")) ?? ""
|
|
152
|
+
const values = row.split("|")[2] ?? ""
|
|
153
|
+
const forms = [...values.matchAll(/`([^`]+)`/gu)].map((match) => match[1])
|
|
154
|
+
expect(forms).toEqual(SOURCE_FLOOR_RULES.map((rule) => rule.form))
|
|
155
|
+
})
|
|
156
|
+
})
|