lazycodex-ai 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.
Files changed (90) hide show
  1. package/dist/cli/index.js +30 -22
  2. package/dist/cli/service-auth/keystore.d.ts +8 -3
  3. package/dist/cli-node/index.js +30 -22
  4. package/package.json +1 -1
  5. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  6. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1 -2
  7. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  8. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  9. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  10. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  11. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  12. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  13. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  14. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  15. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  16. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  17. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  18. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  19. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  20. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  21. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  22. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  23. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  24. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  25. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  26. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  27. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  28. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  29. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  30. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  31. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  32. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  33. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  34. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  35. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  36. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  37. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  38. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  39. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  40. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  41. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  42. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  43. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  44. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  45. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  46. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  47. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  48. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  49. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  50. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  51. package/packages/omo-codex/plugin/package-lock.json +12 -12
  52. package/packages/omo-codex/plugin/package.json +1 -1
  53. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  54. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  55. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +58 -1
  56. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +57 -8
  57. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +2 -1
  58. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +3 -11
  59. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  60. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +21 -44
  61. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
  62. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
  63. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +8 -47
  64. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  65. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
  66. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  67. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +8 -3
  68. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  69. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -5
  70. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  71. package/packages/shared-skills/skills/ulw-research/SKILL.md +59 -2
  72. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +57 -8
  73. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +2 -1
  74. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +3 -11
  75. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +1 -0
  76. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  77. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +209 -0
  78. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  79. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +21 -44
  80. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
  81. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
  82. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +8 -47
  83. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  84. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
  85. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +7 -23
  86. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  87. package/packages/shared-skills/skills/ulw-research/scripts/references-fences.test.ts +25 -0
  88. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +329 -1
  89. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +8 -3
  90. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
@@ -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
+ })
@@ -0,0 +1,209 @@
1
+ // Architecture contract for the polish command (#11160 sweep): the edit contract, the ledger
2
+ // loader, the edition-format grammar, and the severity buckets each have one owner export, so a
3
+ // reintroduced local copy in fact-commands-polish.mjs fails one of these tests.
4
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test"
5
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"
6
+ import { tmpdir } from "node:os"
7
+ import { join } from "node:path"
8
+
9
+ import { CliError } from "./cli-support.mjs"
10
+ import { countDefects, DEFECT_CODES, SEVERITIES } from "./contracts.mjs"
11
+ import { runFactMeaning } from "./fact-commands-meaning.mjs"
12
+ import * as polishCommand from "./fact-commands-polish.mjs"
13
+ import { editionFormatOf, ledgerFrom, sessionOf } from "./fact-commands-verify.mjs"
14
+ import { applyEdits, POLISH_EDIT_FIELDS, POLISH_MARKUP_REASON, validatePolishEdits } from "./polish-apply.mjs"
15
+
16
+ const { polishDiffFails, runFactPolish } = polishCommand
17
+ const source = (name: string) => readFileSync(join(import.meta.dir, name), "utf8")
18
+ const LEDGER = { schemaVersion: 1, rows: [], events: [] }
19
+ const doc = (body: string) => `<html><body>\n${body}\n</body></html>`
20
+ const BRIEF = join(import.meta.dir, "tests", "fixtures", "fidelity-brief-locked.md")
21
+
22
+ const dir = mkdtempSync(join(tmpdir(), "polish-copy-returns-"))
23
+ const file = (name: string, content: string) => {
24
+ const path = join(dir, name)
25
+ writeFileSync(path, content)
26
+ return path
27
+ }
28
+ const ledgerPath = file("ledger.json", JSON.stringify(LEDGER))
29
+ const badLedgerPath = file("bad-ledger.json", JSON.stringify({ schemaVersion: 1, rows: "nope" }))
30
+ const reportPath = file("report.html", doc("<p>The span is 320 m [S1].</p>"))
31
+ const savedSession = process.env.SESSION_DIR
32
+ beforeEach(() => {
33
+ delete process.env.SESSION_DIR
34
+ })
35
+ afterEach(() => {
36
+ if (savedSession === undefined) delete process.env.SESSION_DIR
37
+ else process.env.SESSION_DIR = savedSession
38
+ })
39
+ afterAll(() => rmSync(dir, { recursive: true, force: true }))
40
+
41
+ function outcome(run: () => unknown) {
42
+ try {
43
+ run()
44
+ return "ok"
45
+ } catch (error) {
46
+ if (!(error instanceof CliError)) throw error
47
+ return error.message
48
+ }
49
+ }
50
+
51
+ describe("the polish edit contract is polish-apply's", () => {
52
+ const SHAPES = [
53
+ ["not an array", { before: "a", after: "b" }],
54
+ ["a null edit", [null]],
55
+ ["an array edit", [["a", "b"]]],
56
+ ["a missing after", [{ before: "span" }]],
57
+ ["a numeric before", [{ before: 3, after: "b" }]],
58
+ ["a numeric reason", [{ before: "span", after: "span", reason: 5 }]],
59
+ ["a valid edit", [{ before: "The span is", after: "The span is", reason: "none" }]],
60
+ ["a valid edit without reason", [{ before: "The span is", after: "The span is" }]],
61
+ ] as const
62
+
63
+ test("#given the command module #when its edit contract is read #then it is the polish-apply export", () => {
64
+ expect(polishCommand.validatePolishEdits).toBe(validatePolishEdits)
65
+ expect(polishCommand.POLISH_EDIT_FIELDS).toBe(POLISH_EDIT_FIELDS)
66
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/typeof edit\?\.(before|after)/u)
67
+ })
68
+
69
+ for (const [name, edits] of SHAPES) {
70
+ test(`#given ${name} #when applied directly and through the CLI #then both accept or both refuse`, () => {
71
+ const editsPath = file(`edits-${name.replaceAll(" ", "-")}.json`, JSON.stringify(edits))
72
+ const direct = (() => {
73
+ try {
74
+ applyEdits(readFileSync(reportPath, "utf8"), edits as never, LEDGER)
75
+ return "ok"
76
+ } catch (error) {
77
+ expect(error).toBeInstanceOf(TypeError)
78
+ return "refused"
79
+ }
80
+ })()
81
+ const cli = outcome(() => runFactPolish("polish-apply", [reportPath, "--edits", editsPath, "--ledger", ledgerPath, "--brief", BRIEF, "--out", join(dir, "out.html")]))
82
+ expect(cli === "ok" ? "ok" : "refused").toBe(direct)
83
+ if (cli !== "ok") expect(cli).toContain(editsPath)
84
+ })
85
+ }
86
+ })
87
+
88
+ describe("the ledger loader is one shared helper", () => {
89
+ const pairs = (extra: string[]) => () => runFactMeaning("pairs", [reportPath, "--brief", BRIEF, "--out", join(dir, "pairs.json"), ...extra])
90
+ const diff = (extra: string[]) => () => runFactPolish("polish-diff", ["--before", reportPath, "--after", reportPath, "--brief", BRIEF, ...extra])
91
+
92
+ test("#given the command modules #when read #then neither defines its own ledger loader or session resolver", () => {
93
+ expect(polishCommand.ledgerFrom).toBe(ledgerFrom)
94
+ for (const name of ["fact-commands-meaning.mjs", "fact-commands-polish.mjs"]) {
95
+ const text = source(name)
96
+ expect(text, name).not.toMatch(/function (readLedgerFrom|ledgerFrom)\b|validateLedger\(/u)
97
+ expect(text, name).not.toMatch(/const sessionOf\s*=/u)
98
+ }
99
+ })
100
+
101
+ test("#given the same valid, invalid, and missing ledgers #when fact pairs and fact polish-diff load them #then both give the same outcome", () => {
102
+ for (const extra of [["--ledger", ledgerPath], ["--ledger", badLedgerPath], []]) {
103
+ const expected = outcome(() => ledgerFrom({ ...(extra.length > 0 ? { ledger: extra[1] } : {}) }))
104
+ expect(outcome(pairs(extra)), extra.join(" ")).toBe(expected)
105
+ expect(outcome(diff(extra)), extra.join(" ")).toBe(expected)
106
+ }
107
+ expect(outcome(() => ledgerFrom({ ledger: badLedgerPath }))).toContain("is not a valid fact ledger")
108
+ expect(outcome(() => ledgerFrom({}))).toContain("--ledger is required")
109
+ })
110
+
111
+ test("#given a session dir flag or SESSION_DIR #when resolved #then the flag wins and a bare flag is no session", () => {
112
+ expect(sessionOf({ "session-dir": dir })).toBe(dir)
113
+ expect(sessionOf({ "session-dir": true })).toBeNull()
114
+ process.env.SESSION_DIR = dir
115
+ expect(sessionOf({})).toBe(dir)
116
+ })
117
+ })
118
+
119
+ describe("the edition format comes from verify's grammar", () => {
120
+ test("#given report paths of each case #when polish-apply reads them #then it refuses exactly the Markdown classifications", () => {
121
+ const editsPath = file("edits-ok.json", "[]")
122
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/md\|markdown/u)
123
+ for (const name of ["r.md", "R.MD", "r.markdown", "R.Markdown", "r.html", "r.HTML"]) {
124
+ const path = file(name, readFileSync(reportPath, "utf8"))
125
+ const result = outcome(() => runFactPolish("polish-apply", [path, "--edits", editsPath, "--ledger", ledgerPath, "--brief", BRIEF, "--out", join(dir, "out.html")]))
126
+ expect(result.includes("takes the filled HTML edition"), name).toBe(editionFormatOf(path, undefined) === "md")
127
+ }
128
+ })
129
+ })
130
+
131
+ describe("polish-diff counts with contracts and exits on blockers only", () => {
132
+ type DiffRun = { exitCode: number; json: { counts: Record<string, number>; defects: { severity: string }[] } }
133
+ const run = (before: string, after: string) => runFactPolish("polish-diff", ["--before", file("b.html", doc(before)), "--after", file("a.html", doc(after)), "--ledger", ledgerPath, "--brief", BRIEF]) as unknown as DiffRun
134
+
135
+ test("#given every registry code #when polishDiffFails classifies it #then only registry blockers fail", () => {
136
+ for (const [code, entry] of Object.entries(DEFECT_CODES) as [string, { severity: string }][]) expect(polishDiffFails([{ code, severity: "minor" }]), code).toBe(entry.severity === "blocker")
137
+ expect(polishDiffFails([])).toBe(false)
138
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/blocker:\s*0|counts\.blocker > 0/u)
139
+ })
140
+
141
+ test("#given a major-only polish diff #when run #then it is reported with contract buckets and exits 0", () => {
142
+ const result = run("<p>The span may be 320 m [S1].</p>", "<p>The span is 320 m [S1].</p>")
143
+ const { defects } = result.json
144
+ expect(defects.length).toBeGreaterThan(0)
145
+ expect(defects.every((defect) => defect.severity === "major")).toBe(true)
146
+ expect(result.json.counts).toEqual(countDefects(defects))
147
+ expect(Object.keys(result.json.counts)).toEqual([...SEVERITIES, "total"])
148
+ expect(result.exitCode).toBe(0)
149
+ })
150
+
151
+ test("#given a changed numeral #when run #then the blocker exits 1", () => {
152
+ const result = run("<p>Throughput grew 3.5 times [S1].</p>", "<p>Throughput grew 35 times [S1].</p>")
153
+ expect(result.json.counts).toEqual(countDefects(result.json.defects))
154
+ expect(result.exitCode).toBe(1)
155
+ })
156
+ })
157
+
158
+ describe("fact polish-apply prints every rejection reason verbatim", () => {
159
+ type ApplyRun = { summary: string; exitCode: number; json: { defects: { index: number; code: string; reason: string }[]; data: { rejected: number; rejected_reasons: string[] } } }
160
+
161
+ test("#given an edit holding & #when fact polish-apply runs #then the printed line carries polish-apply's own markup reason", () => {
162
+ const before = "Revenue & profit rose across the year"
163
+ const editsPath = file("edits-markup-ampersand.json", JSON.stringify([{ before, after: "Revenue and profit rose across the year", reason: "wording" }]))
164
+ const run = runFactPolish("polish-apply", [reportPath, "--edits", editsPath, "--ledger", ledgerPath, "--brief", BRIEF, "--out", join(dir, "ampersand.html")]) as unknown as ApplyRun
165
+
166
+ expect(run.exitCode).toBe(0)
167
+ expect(run.json.data.rejected).toBe(1)
168
+ expect(run.json.defects.map((defect) => [defect.index, defect.code, defect.reason])).toEqual([[0, "polish_edit_rejected", POLISH_MARKUP_REASON]])
169
+ const line = ` rejected edit 0 ("${before}"): ${POLISH_MARKUP_REASON}`
170
+ expect(run.json.data.rejected_reasons).toEqual([line])
171
+ expect(run.summary.split("\n")[1]).toBe(line)
172
+ })
173
+ })
174
+
175
+ describe("--changed-since stays fail-closed", () => {
176
+ const { pairsChangedSince } = polishCommand as unknown as { pairsChangedSince: (pairs: unknown[], mapPath: string, ledger: unknown, claims: unknown) => unknown[] }
177
+ const artifact = file("pre-polish.html", doc("<p>The span is 320 m [S1].</p>"))
178
+
179
+ test("#given a fill map without artifact_sha256 #when --changed-since reads it #then it refuses", () => {
180
+ const mapPath = file("fill-map-no-sha.json", JSON.stringify({ artifact, format: "html", entries: [] }))
181
+ expect(outcome(() => pairsChangedSince([], mapPath, LEDGER, null))).toContain("fill map has no artifact_sha256")
182
+ })
183
+
184
+ test("#given a fill map whose artifact changed since fill #when read #then it still refuses", () => {
185
+ const mapPath = file("fill-map-stale.json", JSON.stringify({ artifact, artifact_sha256: "0".repeat(64), format: "html", entries: [] }))
186
+ expect(outcome(() => pairsChangedSince([], mapPath, LEDGER, null))).toContain("changed after fill")
187
+ })
188
+ })
189
+
190
+ describe("polish-diff refuses an unknown defect code", () => {
191
+ test("#given a defect whose code is not in the registry #when classified #then it refuses instead of reading its severity", () => {
192
+ expect(outcome(() => polishDiffFails([{ code: "not_a_real_code", severity: "minor" }]))).toContain("unknown defect code not_a_real_code")
193
+ expect(source("fact-commands-polish.mjs")).not.toMatch(/\?\?\s*defect\.severity/u)
194
+ })
195
+ })
196
+
197
+ describe("polish-apply writes atomically and honours --format", () => {
198
+ test("#given the command module #when read #then it writes the edition through writeTextAtomic", () => {
199
+ const text = source("fact-commands-polish.mjs")
200
+ expect(text).not.toMatch(/writeFileSync\s*\(/u)
201
+ expect(text).toMatch(/writeTextAtomic\(flags\.out/u)
202
+ })
203
+
204
+ test("#given --format md on an .html report #when polish-apply runs #then it refuses the Markdown edition", () => {
205
+ const editsPath = file("edits-format.json", "[]")
206
+ const result = outcome(() => runFactPolish("polish-apply", [reportPath, "--edits", editsPath, "--ledger", ledgerPath, "--brief", BRIEF, "--out", join(dir, "fmt.html"), "--format", "md"]))
207
+ expect(result).toContain("takes the filled HTML edition")
208
+ })
209
+ })
@@ -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; it honours the brief's `meaning check` field and writes
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. Neither command calls a model or the network.
11
+ // instead of counting a second run. Both refuse an unlocked brief (fidelity_interview_incomplete) before
12
+ // reading anything else. Neither command calls a model or the network.
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 { readLedger } from "./fact-ledger.mjs"
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, meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
22
+ import { FIDELITY_INCOMPLETE } from "./fidelity-brief.mjs"
23
+ import { briefPathOf, lockedBrief, meaningCheckEnabled } from "./fidelity-policy.mjs"
22
24
  import { MEANING_RECORD, writeFidelityRecord } from "./fidelity-records.mjs"
23
25
  import { blocksDelivery } from "./gates-fidelity.mjs"
24
26
  import { ingestVerdicts, unverifiedDefects, VERDICT_SCHEMA } from "./meaning-check.mjs"
@@ -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 brief = briefState(flags, session)
68
- if (!brief.locked) {
69
- const json = { command: "fact pairs", status: "fail", exitCode: 1, error: "fidelity_interview_incomplete", message: `${brief.path} has no locked ## Fidelity block` }
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 changed = flags["changed-since"] === undefined ? undefined : readJsonFile(flags["changed-since"]).entries
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 pairs = extractPairs(html, ledger, claims, changed)
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, session) {
144
- const brief = briefState(flags, session)
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 refusal = skippedRefusal(flags, session)
159
- if (refusal !== null) return { json: { command: "fact meaning-check", status: "fail", exitCode: 1, ...refusal }, summary: `fact meaning-check: ${refusal.message}`, exitCode: 1 }
135
+ const skipped = skippedRefusal(flags, parsed)
136
+ if (skipped !== null) return { json: { command: "fact meaning-check", status: "fail", exitCode: 1, ...skipped }, summary: `fact meaning-check: ${skipped.message}`, exitCode: 1 }
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`)