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.
Files changed (124) hide show
  1. package/.agents/command/publish.md +3 -4
  2. package/.agents/skills/publish/SKILL.md +3 -4
  3. package/.opencode/command/publish.md +3 -4
  4. package/dist/agents/atlas/agent.d.ts +3 -2
  5. package/dist/agents/sisyphus/index.d.ts +1 -0
  6. package/dist/agents/sisyphus/kimi-k3-1.d.ts +9 -0
  7. package/dist/agents/sisyphus-agent-factory.d.ts +1 -1
  8. package/dist/agents/sisyphus-junior/agent.d.ts +1 -1
  9. package/dist/agents/sisyphus-junior/index.d.ts +1 -0
  10. package/dist/agents/sisyphus-junior/kimi-k3-1.d.ts +6 -0
  11. package/dist/agents/types.d.ts +2 -2
  12. package/dist/cli/index.js +30 -22
  13. package/dist/cli/service-auth/keystore.d.ts +8 -3
  14. package/dist/cli-node/index.js +30 -22
  15. package/dist/index.js +635 -18
  16. package/dist/skills/browser/runtime/omowright/manifest.json +1 -1
  17. package/dist/skills/ulw-research/SKILL.md +59 -2
  18. package/dist/skills/ulw-research/references/deliverable-phase.md +57 -8
  19. package/dist/skills/ulw-research/references/report-gates.md +2 -1
  20. package/dist/skills/ulw-research/scripts/contracts.mjs +3 -11
  21. package/dist/skills/ulw-research/scripts/contracts.test.ts +1 -0
  22. package/dist/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  23. package/dist/skills/ulw-research/scripts/copy-returns-polish.test.ts +209 -0
  24. package/dist/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  25. package/dist/skills/ulw-research/scripts/fact-commands-meaning.mjs +21 -44
  26. package/dist/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
  27. package/dist/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
  28. package/dist/skills/ulw-research/scripts/fact-commands.mjs +8 -47
  29. package/dist/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  30. package/dist/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
  31. package/dist/skills/ulw-research/scripts/fact-pairs.test.ts +7 -23
  32. package/dist/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  33. package/dist/skills/ulw-research/scripts/references-fences.test.ts +25 -0
  34. package/dist/skills/ulw-research/scripts/report-tools.test.ts +329 -1
  35. package/dist/skills/ulw-research/scripts/source-commands.mjs +8 -3
  36. package/dist/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
  37. package/dist/tui.js +14 -14
  38. package/package.json +14 -14
  39. package/packages/omo-codex/plugin/.codex-plugin/plugin.json +1 -1
  40. package/packages/omo-codex/plugin/components/bootstrap/dist/cli.js +1 -2
  41. package/packages/omo-codex/plugin/components/bootstrap/hooks/hooks.json +1 -1
  42. package/packages/omo-codex/plugin/components/bootstrap/package.json +1 -1
  43. package/packages/omo-codex/plugin/components/comment-checker/hooks/hooks.json +1 -1
  44. package/packages/omo-codex/plugin/components/comment-checker/package.json +1 -1
  45. package/packages/omo-codex/plugin/components/git-bash/hooks/hooks.json +2 -2
  46. package/packages/omo-codex/plugin/components/git-bash/package.json +1 -1
  47. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/hooks/hooks.json +1 -1
  48. package/packages/omo-codex/plugin/components/lazycodex-executor-verify/package.json +1 -1
  49. package/packages/omo-codex/plugin/components/lsp/dist/.omo-runtime-manifest.json +2 -2
  50. package/packages/omo-codex/plugin/components/lsp/hooks/hooks.json +2 -2
  51. package/packages/omo-codex/plugin/components/lsp/package.json +1 -1
  52. package/packages/omo-codex/plugin/components/rules/hooks/hooks.json +4 -4
  53. package/packages/omo-codex/plugin/components/rules/package.json +1 -1
  54. package/packages/omo-codex/plugin/components/teammode/hooks/hooks.json +1 -1
  55. package/packages/omo-codex/plugin/components/teammode/package.json +1 -1
  56. package/packages/omo-codex/plugin/components/telemetry/hooks/hooks.json +1 -1
  57. package/packages/omo-codex/plugin/components/telemetry/package.json +1 -1
  58. package/packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json +1 -1
  59. package/packages/omo-codex/plugin/components/ultrawork/package.json +1 -1
  60. package/packages/omo-codex/plugin/components/ulw-execute-continuation/hooks/hooks.json +1 -1
  61. package/packages/omo-codex/plugin/components/ulw-execute-continuation/package.json +1 -1
  62. package/packages/omo-codex/plugin/components/ulw-loop/hooks/hooks.json +5 -5
  63. package/packages/omo-codex/plugin/components/ulw-loop/package.json +1 -1
  64. package/packages/omo-codex/plugin/hooks/post-compact-resetting-git-bash-mcp-reminder.json +1 -1
  65. package/packages/omo-codex/plugin/hooks/post-compact-resetting-lsp-diagnostics-cache.json +1 -1
  66. package/packages/omo-codex/plugin/hooks/post-compact-resetting-project-rule-cache.json +1 -1
  67. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-comments.json +1 -1
  68. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-lsp-diagnostics.json +1 -1
  69. package/packages/omo-codex/plugin/hooks/post-tool-use-checking-thread-title-hygiene.json +1 -1
  70. package/packages/omo-codex/plugin/hooks/post-tool-use-matching-project-rules.json +1 -1
  71. package/packages/omo-codex/plugin/hooks/post-tool-use-recording-spawn-admission.json +1 -1
  72. package/packages/omo-codex/plugin/hooks/pre-tool-use-enforcing-unlimited-goal-budget.json +1 -1
  73. package/packages/omo-codex/plugin/hooks/pre-tool-use-guarding-ulw-loop-spawns.json +1 -1
  74. package/packages/omo-codex/plugin/hooks/pre-tool-use-recommending-git-bash-mcp.json +1 -1
  75. package/packages/omo-codex/plugin/hooks/session-start-checking-auto-update.json +1 -1
  76. package/packages/omo-codex/plugin/hooks/session-start-checking-bootstrap-provisioning.json +1 -1
  77. package/packages/omo-codex/plugin/hooks/session-start-loading-project-rules.json +1 -1
  78. package/packages/omo-codex/plugin/hooks/session-start-recording-session-telemetry.json +1 -1
  79. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-execute-continuation.json +1 -1
  80. package/packages/omo-codex/plugin/hooks/stop-checking-ulw-loop-resume.json +1 -1
  81. package/packages/omo-codex/plugin/hooks/subagent-stop-verifying-lazycodex-executor-evidence.json +1 -1
  82. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ultrawork-trigger.json +1 -1
  83. package/packages/omo-codex/plugin/hooks/user-prompt-submit-checking-ulw-loop-steering.json +1 -1
  84. package/packages/omo-codex/plugin/hooks/user-prompt-submit-loading-project-rules.json +1 -1
  85. package/packages/omo-codex/plugin/package-lock.json +12 -12
  86. package/packages/omo-codex/plugin/package.json +1 -1
  87. package/packages/omo-codex/plugin/scripts/sync-skills.mjs +1 -1
  88. package/packages/omo-codex/plugin/skills/browser/runtime/omowright/manifest.json +1 -1
  89. package/packages/omo-codex/plugin/skills/ulw-research/SKILL.md +58 -1
  90. package/packages/omo-codex/plugin/skills/ulw-research/references/deliverable-phase.md +57 -8
  91. package/packages/omo-codex/plugin/skills/ulw-research/references/report-gates.md +2 -1
  92. package/packages/omo-codex/plugin/skills/ulw-research/scripts/contracts.mjs +3 -11
  93. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  94. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-meaning.mjs +21 -44
  95. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
  96. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
  97. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-commands.mjs +8 -47
  98. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  99. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
  100. package/packages/omo-codex/plugin/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  101. package/packages/omo-codex/plugin/skills/ulw-research/scripts/source-commands.mjs +8 -3
  102. package/packages/omo-codex/plugin/test/sync-skills-test-support.mjs +1 -1
  103. package/packages/omo-codex/scripts/install-dist/install-local.mjs +4 -5
  104. package/packages/shared-skills/skills/browser/runtime/omowright/manifest.json +1 -1
  105. package/packages/shared-skills/skills/ulw-research/SKILL.md +59 -2
  106. package/packages/shared-skills/skills/ulw-research/references/deliverable-phase.md +57 -8
  107. package/packages/shared-skills/skills/ulw-research/references/report-gates.md +2 -1
  108. package/packages/shared-skills/skills/ulw-research/scripts/contracts.mjs +3 -11
  109. package/packages/shared-skills/skills/ulw-research/scripts/contracts.test.ts +1 -0
  110. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-fidelity-policy.test.ts +156 -0
  111. package/packages/shared-skills/skills/ulw-research/scripts/copy-returns-polish.test.ts +209 -0
  112. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-fill.mjs +86 -0
  113. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-meaning.mjs +21 -44
  114. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-polish.mjs +132 -0
  115. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands-verify.mjs +37 -4
  116. package/packages/shared-skills/skills/ulw-research/scripts/fact-commands.mjs +8 -47
  117. package/packages/shared-skills/skills/ulw-research/scripts/fact-fill.mjs +3 -2
  118. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.mjs +7 -9
  119. package/packages/shared-skills/skills/ulw-research/scripts/fact-pairs.test.ts +7 -23
  120. package/packages/shared-skills/skills/ulw-research/scripts/fidelity-policy.mjs +183 -0
  121. package/packages/shared-skills/skills/ulw-research/scripts/references-fences.test.ts +25 -0
  122. package/packages/shared-skills/skills/ulw-research/scripts/report-tools.test.ts +329 -1
  123. package/packages/shared-skills/skills/ulw-research/scripts/source-commands.mjs +8 -3
  124. package/packages/shared-skills/skills/ulw-research/scripts/tests/fixtures/fidelity/polish-edits.json +12 -0
@@ -1,21 +1,19 @@
1
- // fact-commands.mjs - the `fact` subcommands of report-tools: add, quote, retract, export, fill,
2
- // derive, verify (fact-commands-verify.mjs), pairs and meaning-check (fact-commands-meaning.mjs). Every value reaches the ledger from a source
1
+ // fact-commands.mjs - the `fact` subcommands of report-tools: add, quote, retract, export, derive,
2
+ // fill (fact-commands-fill.mjs), verify (fact-commands-verify.mjs), pairs and meaning-check
3
+ // (fact-commands-meaning.mjs), polish-apply and polish-diff (fact-commands-polish.mjs). Every value reaches the ledger from a source
3
4
  // snapshot through a deterministic locator or from a declared derivation; nothing here accepts a
4
5
  // typed value. add and quote print the row with 60 chars of source context on each side (JSON
5
6
  // with --json). Exit 0 ok, 1 semantic failure (fact_not_located, fill errors, a refused
6
7
  // derivation), 2 usage. node: builtins and sibling imports only.
7
8
 
8
9
  import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs"
9
- import { dirname, extname, join, resolve } from "node:path"
10
+ import { join, resolve } from "node:path"
10
11
 
11
- import { CliError, parseArgs, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
12
- import { DEFECT_CODES } from "./contracts.mjs"
12
+ import { CliError, parseArgs, sha256Of } from "./cli-support.mjs"
13
13
  import { derive } from "./fact-derive.mjs"
14
- import { fill } from "./fact-fill.mjs"
15
14
  import { addRow, readLedger, retractRow, supersedeRow, writeLedger } from "./fact-ledger.mjs"
16
15
  import { locateFind, locateMatch, locatePageLine, locateTable } from "./fact-locate.mjs"
17
16
  import { QUALIFIER_FORMS } from "./fidelity-lexicon.mjs"
18
- import { readState, writeState } from "./fidelity-state.mjs"
19
17
  import { exportRows } from "./fidelity-values.mjs"
20
18
  import { pointedFlag, sessionDirOf, snapshotOf } from "./source-commands.mjs"
21
19
 
@@ -156,44 +154,6 @@ function runExport(args) {
156
154
  return { json: { command: "fact export", status: "pass", exitCode: 0, data: { out: resolve(args.flags.out), format: args.flags.csv ? "csv" : "json", sha256: sha256Of(text), coverage } }, summary: coverage, exitCode: 0 }
157
155
  }
158
156
 
159
- /** The inline edition: local stylesheets and scripts inlined, relative image paths made absolute. */
160
- function inlineAssets(html, baseDir) {
161
- const local = (ref) => !/^[a-z][a-z0-9+.-]*:|^\/\//i.test(ref)
162
- return html
163
- .replace(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi, (tag) => {
164
- const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1]
165
- return href && local(href) ? `<style>${readText(resolve(baseDir, href))}</style>` : tag
166
- })
167
- .replace(/<script\b([^>]*)\bsrc=["']([^"']+)["']([^>]*)><\/script>/gi, (tag, pre, src, post) => (local(src) ? `<script${pre}${post}>${readText(resolve(baseDir, src))}</script>` : tag))
168
- .replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (tag, pre, src, post) => (local(src) && !src.startsWith("/") ? `${pre}${resolve(baseDir, src)}${post}` : tag))
169
- }
170
-
171
- function runFill(args) {
172
- const [source] = args.positionals
173
- const { flags } = args
174
- if (!source || (flags.out === undefined && !flags.view)) throw new CliError("usage: fact fill <src> (--out <path> | --view) [--format html|md|tex] [--inline] [--replace-failed]")
175
- const format = flags.format ?? ({ ".md": "md", ".tex": "tex" }[extname(source).toLowerCase()] ?? "html")
176
- if (!["html", "md", "tex"].includes(format)) throw new CliError("--format must be html, md, or tex")
177
- if (flags.inline && format !== "html") throw new CliError("--inline applies to the html edition only")
178
- const sessionDir = sessionDirOf(flags)
179
- const state = flags["replace-failed"] ? readState(sessionDir) : null
180
- const result = fill(readText(source), readLedger(sessionDir), { format: format === "tex" ? "md" : format, view: flags.view === true, ...(state ? { replaceFailed: state } : {}) })
181
- const warnings = result.warnings
182
- if (result.out === null) {
183
- const defects = result.errors.map((error) => ({ ...error, severity: DEFECT_CODES[error.code]?.severity ?? "blocker", hint: DEFECT_CODES[error.code]?.hint ?? null }))
184
- return { json: { command: "fact fill", status: "fail", exitCode: 1, defects, warnings }, summary: `fact fill: FAIL - ${defects.length} error(s), nothing written`, exitCode: 1 }
185
- }
186
- const out = flags.inline ? inlineAssets(result.out, dirname(resolve(source))) : result.out
187
- if (flags.view) return { text: out, summary: `fact fill --view: ${result.fillMap.length} pointer(s), ${warnings.length} warning(s)`, exitCode: 0 }
188
- writeFileSync(flags.out, out)
189
- if (state) writeState(sessionDir, state)
190
- const artifact = sha256Of(out)
191
- const map = `${resolve(flags.out)}.fill-map.json`
192
- writeJsonAtomic(map, { schemaVersion: 1, format, artifact: resolve(flags.out), artifact_sha256: artifact, entries: result.fillMap })
193
- const data = { out: resolve(flags.out), map, artifact_sha256: artifact, occurrences: result.fillMap.length, warnings }
194
- return { json: { command: "fact fill", status: "pass", exitCode: 0, defects: [], data }, summary: `fact fill: wrote ${flags.out} (${result.fillMap.length} pointer(s))`, exitCode: 0 }
195
- }
196
-
197
157
  function runDerive(args) {
198
158
  const { flags } = args
199
159
  const op = args.required("op")
@@ -217,6 +177,7 @@ function runDerive(args) {
217
177
  export async function runFact(argv) {
218
178
  const [sub, ...rest] = argv
219
179
  if (sub === "pairs" || sub === "meaning-check") return (await import("./fact-commands-meaning.mjs")).runFactMeaning(sub, rest)
180
+ if (sub === "polish-apply" || sub === "polish-diff") return (await import("./fact-commands-polish.mjs")).runFactPolish(sub, rest)
220
181
  const args = parseArgs(rest, { booleans: BOOLEANS })
221
182
  switch (sub) {
222
183
  case "add":
@@ -228,12 +189,12 @@ export async function runFact(argv) {
228
189
  case "export":
229
190
  return runExport(args)
230
191
  case "fill":
231
- return runFill(args)
192
+ return (await import("./fact-commands-fill.mjs")).runFactFill(args)
232
193
  case "derive":
233
194
  return runDerive(args)
234
195
  case "verify":
235
196
  return (await import("./fact-commands-verify.mjs")).runFactVerify(args)
236
197
  default:
237
- throw new CliError("usage: fact <add|quote|retract|export|fill|verify|derive|pairs|meaning-check> ...")
198
+ throw new CliError("usage: fact <add|quote|retract|export|fill|verify|derive|pairs|meaning-check|polish-apply|polish-diff> ...")
238
199
  }
239
200
  }
@@ -10,7 +10,8 @@ import { failureCount, markReplaced, sentenceKey } from "./fidelity-state.mjs"
10
10
  import { findPlaceholders, lead, qualifiedText, renderBlockquote, renderQuote, renderValueHtml, subSpan, valueParts } from "./placeholder.mjs"
11
11
  import { splitSentences } from "./sentence-split.mjs"
12
12
 
13
- const FORMATS = Object.freeze(["html", "md"])
13
+ /** The output formats fill renders. */
14
+ export const FILL_FORMATS = Object.freeze(["html", "md"])
14
15
  const TRANSLATION = Object.freeze({
15
16
  html: /^\s*<span data-translates="(Q\d+)">[\s\S]*?<\/span>/u,
16
17
  md: /^\s*<!-- translates: (Q\d+) -->[^\n]*/u,
@@ -166,7 +167,7 @@ function assemble(text, parts, context) {
166
167
  */
167
168
  export function fill(sourceText, ledger, options = {}) {
168
169
  const format = options.format ?? "html"
169
- if (!FORMATS.includes(format)) throw new TypeError(`format must be one of ${FORMATS.join(", ")}`)
170
+ if (!FILL_FORMATS.includes(format)) throw new TypeError(`format must be one of ${FILL_FORMATS.join(", ")}`)
170
171
  const rows = new Map(ledger.rows.map((row) => [row.fact_id, row]))
171
172
  const found = findPlaceholders(sourceText)
172
173
  const errors = []
@@ -96,11 +96,11 @@ function lexiconRouted(group, sentence) {
96
96
  return lexiconChecks(probes).forceStrong.length > 0
97
97
  }
98
98
 
99
- function quotePairs(sentence, rows, filled, slug, reportLang) {
99
+ function quotePairs(sentence, rows, slug, reportLang) {
100
100
  const groups = new Map()
101
101
  for (const fact of sentence.facts) {
102
102
  const row = rows.get(fact.id)
103
- if (!row?.quote || (filled && !filled.has(fact.id))) continue
103
+ if (!row?.quote) continue
104
104
  const group = groups.get(row.quote) ?? { quote: row.quote, row, rows, facts: [], ids: [] }
105
105
  group.facts.push(fact)
106
106
  if (!group.ids.includes(fact.id)) group.ids.push(fact.id)
@@ -136,11 +136,11 @@ function claimPairs(ctx, block, sentence, byClaim, slug, reportLang) {
136
136
  }))
137
137
  }
138
138
 
139
- function translationPair(node, rows, filled, slug, reportLang) {
139
+ function translationPair(node, rows, slug, reportLang) {
140
140
  const id = node.attrs["data-translates"]
141
141
  const row = rows.get(id)
142
142
  const text = proseText(node)
143
- if (!row?.quote || text === "" || (filled && !filled.has(id))) return null
143
+ if (!row?.quote || text === "") return null
144
144
  return {
145
145
  kind: "translation",
146
146
  sentence_key: sentenceKey(slug, [id]),
@@ -157,13 +157,11 @@ function translationPair(node, rows, filled, slug, reportLang) {
157
157
  * @param {string} html the filled report
158
158
  * @param {{ rows: object[] }} ledger
159
159
  * @param {{ claims: object[] } | object[] | null} claims indexClaims(...) output or its claims array
160
- * @param {{ fact_id: string }[]} [fillMap] fact fill's map; when given, only facts it filled are paired
161
160
  * @returns {object[]} pairs with pair_id P1, P2, ... in document order
162
161
  */
163
- export function extractPairs(html, ledger, claims, fillMap) {
162
+ export function extractPairs(html, ledger, claims) {
164
163
  const ctx = buildContext(parseHtml(html), {})
165
164
  const rows = new Map((ledger?.rows ?? []).map((row) => [row.fact_id, row]))
166
- const filled = Array.isArray(fillMap) ? new Set(fillMap.map((entry) => entry.fact_id)) : null
167
165
  const byClaim = claimMap(claims)
168
166
  const htmlLang = ctx.all.find((node) => node.tag === "html")?.attrs.lang
169
167
  const reportLang = htmlLang ? htmlLang.toLowerCase().split("-")[0] : null
@@ -174,12 +172,12 @@ export function extractPairs(html, ledger, claims, fillMap) {
174
172
  if (!findings) continue
175
173
  const slug = slugOf(block.scope)
176
174
  for (const sentence of block.sentences) {
177
- out.push(...quotePairs(sentence, rows, filled, slug, reportLang), ...claimPairs(ctx, block, sentence, byClaim, slug, reportLang))
175
+ out.push(...quotePairs(sentence, rows, slug, reportLang), ...claimPairs(ctx, block, sentence, byClaim, slug, reportLang))
178
176
  }
179
177
  for (const node of ctx.util.descendants(block.owner)) {
180
178
  if (node.attrs?.["data-translates"] === undefined || seenSpans.has(node)) continue
181
179
  seenSpans.add(node)
182
- const pair = translationPair(node, rows, filled, slug, reportLang)
180
+ const pair = translationPair(node, rows, slug, reportLang)
183
181
  if (pair) out.push(pair)
184
182
  }
185
183
  }
@@ -39,8 +39,8 @@ function filled(): { out: string; fillMap: { fact_id: string }[] } {
39
39
  }
40
40
 
41
41
  function pairs(): Pair[] {
42
- const { out, fillMap } = filled()
43
- return extractPairs(out, LEDGER, CLAIMS, fillMap) as Pair[]
42
+ const { out } = filled()
43
+ return extractPairs(out, LEDGER, CLAIMS) as Pair[]
44
44
  }
45
45
 
46
46
  describe("extractPairs: a filled report", () => {
@@ -110,14 +110,7 @@ describe("extractPairs: a filled report", () => {
110
110
  expect(sentences.some((text) => text.includes("Bridge authority"))).toBe(false)
111
111
  })
112
112
 
113
- test("#given a fill map that omits a fact #when extracted #then that hand-typed fact yields no pair", () => {
114
- const { out, fillMap } = filled()
115
- const kept = fillMap.filter((entry) => entry.fact_id !== "F13")
116
- const result = extractPairs(out, LEDGER, CLAIMS, kept) as Pair[]
117
- expect(result.some((pair) => Array.isArray(pair.fact_ids) && (pair.fact_ids as string[]).includes("F13"))).toBe(false)
118
- })
119
-
120
- test("#given no fill map #when extracted #then every data-fact element counts as filled", () => {
113
+ test("#given the filled fixture #when extracted #then every data-fact element yields its pair", () => {
121
114
  const { out } = filled()
122
115
  expect((extractPairs(out, LEDGER, CLAIMS) as Pair[]).length).toBe(6)
123
116
  })
@@ -125,20 +118,11 @@ describe("extractPairs: a filled report", () => {
125
118
  test("#given a Hangul quote #when extracted #then source_lang is ko", () => {
126
119
  const html = "<html><body><h2>Findings</h2><p>HBM revenue reached {{F21:hbm_rev}} [S5].</p></body></html>"
127
120
  const result = fill(html, LEDGER, { format: "html" }) as { out: string; fillMap: { fact_id: string }[] }
128
- const [pair] = extractPairs(result.out, LEDGER, CLAIMS, result.fillMap) as Pair[]
121
+ const [pair] = extractPairs(result.out, LEDGER, CLAIMS) as Pair[]
129
122
  expect(pair).toMatchObject({ fact_ids: ["F21"], source_lang: "ko", report_lang: "en" })
130
123
  })
131
124
  })
132
125
 
133
- describe("extractPairs: a fill map over translations", () => {
134
- test("#given a fill map that omits the translated fact #when extracted #then no translation pair is emitted", () => {
135
- const { out, fillMap } = filled()
136
- const kept = fillMap.filter((entry) => entry.fact_id !== "Q7")
137
- const result = extractPairs(out, LEDGER, CLAIMS, kept) as Pair[]
138
- expect(result.some((pair) => pair.kind === "translation")).toBe(false)
139
- })
140
- })
141
-
142
126
  describe("hasHedge: a month is not a hedge", () => {
143
127
  test("#given a text that starts with the month May #when checked #then it does not count as hedged", () => {
144
128
  expect(hasHedge("May 2024 sales rose.")).toBe(false)
@@ -152,9 +136,9 @@ describe("hasHedge: a month is not a hedge", () => {
152
136
 
153
137
  describe("extractPairs: determinism", () => {
154
138
  test("#given the same filled report twice #when extracted #then both results are byte-for-byte equal", () => {
155
- const { out, fillMap } = filled()
156
- const first = JSON.stringify(extractPairs(out, LEDGER, CLAIMS, fillMap))
157
- const second = JSON.stringify(extractPairs(out, LEDGER, CLAIMS, fillMap))
139
+ const { out } = filled()
140
+ const first = JSON.stringify(extractPairs(out, LEDGER, CLAIMS))
141
+ const second = JSON.stringify(extractPairs(out, LEDGER, CLAIMS))
158
142
  expect(second).toBe(first)
159
143
  })
160
144
  })
@@ -0,0 +1,183 @@
1
+ // fidelity-policy.mjs - what the locked `## Fidelity` block of brief.md means to the CLI stages.
2
+ //
3
+ // lockedBrief is the gate the six reader-facing stages (fact fill, verify, pairs, meaning-check,
4
+ // polish-apply, polish-diff) run before reading or writing anything: an unlocked brief returns
5
+ // one fidelity_interview_incomplete defect (exit 1), a malformed block is a usage error (exit 2).
6
+ // fillPolicy reads the fields fact fill consumes (a value off its table is a usage error, exit 2),
7
+ // and policyErrors refuses a placeholder whose row the brief forbids: derived_not_allowed
8
+ // (`derived numbers: none` against a derived, converted, or assumed row) and source_outside_floor
9
+ // (the source floor is evaluated live against each source's meta.json at every fill and verify;
10
+ // under any floor but `standard` a row resting on no source is refused too; a row's outside_floor
11
+ // is only the cached result). briefHashOf binds the locked block into the fill map, and
12
+ // briefBindingErrors refuses an edition whose map was filled under another brief (brief_changed).
13
+ // Collection stages never call this module. node: builtins and sibling imports only.
14
+
15
+ import { existsSync, readFileSync } from "node:fs"
16
+ import { join, resolve } from "node:path"
17
+
18
+ import { CliError, sha256Of } from "./cli-support.mjs"
19
+ import { DEFECT_CODES } from "./contracts.mjs"
20
+ import { KIND_RULES } from "./fact-ledger-kinds.mjs"
21
+ import { FIDELITY_FIELDS, meaningCheckEnabled, requireLockedBrief } from "./fidelity-brief.mjs"
22
+
23
+ export { meaningCheckEnabled }
24
+
25
+ /**
26
+ * Ledger kinds a brief with `derived numbers: none` refuses. A policy list, not the ledger's
27
+ * derivation set: it also names assumed. Every name must be a KIND_RULES kind (checked at load).
28
+ */
29
+ const DERIVED_KINDS = Object.freeze(["derived", "converted", "assumed"])
30
+ for (const kind of DERIVED_KINDS) {
31
+ if (!Object.hasOwn(KIND_RULES, kind)) throw new Error(`fidelity-policy: ${kind} is not a KIND_RULES kind`)
32
+ }
33
+
34
+ /** Fields whose only legal values are a closed table: value (any case) -> its reading. */
35
+ const VALUE_TABLES = Object.freeze({
36
+ "derived numbers": Object.freeze({ allowed: false, none: true }),
37
+ fallback: Object.freeze({ "block-quote": "quote", omit: "omit" }),
38
+ })
39
+
40
+ /** The reading of a table value; any other text is a usage error naming the field and the value. */
41
+ function tableValue(field, value) {
42
+ const table = VALUE_TABLES[field]
43
+ const key = value.trim().toLowerCase()
44
+ if (Object.hasOwn(table, key)) return table[key]
45
+ throw new CliError(`${field} "${value}" is not ${Object.keys(table).join(", ")}`)
46
+ }
47
+
48
+ /** How each field value the fill stages consume is read; the single owner of those readings. */
49
+ export const FIDELITY_VALUE_RULES = Object.freeze({
50
+ tables: VALUE_TABLES,
51
+ derivedKinds: DERIVED_KINDS,
52
+ derivedRefused: (kind) => DERIVED_KINDS.includes(kind),
53
+ derivedNone: (value) => tableValue("derived numbers", value),
54
+ fallback: (value) => tableValue("fallback", value),
55
+ translations: (value) => /with translation beside/iu.test(value),
56
+ meaningCheck: (value) => meaningCheckEnabled({ fields: { "meaning check": value } }),
57
+ })
58
+
59
+ /** The `source floor` forms: `pattern` reads one, `listed` forms carry a comma list. */
60
+ export const SOURCE_FLOOR_RULES = Object.freeze([
61
+ Object.freeze({ kind: "standard", form: "standard", pattern: /^standard$/iu, listed: false }),
62
+ Object.freeze({ kind: "primary", form: "primary only", pattern: /^primary only$/iu, listed: false }),
63
+ Object.freeze({ kind: "pointed", form: "pointed only: <list>", pattern: /^pointed only:\s*(.+)$/iu, listed: true }),
64
+ Object.freeze({ kind: "classes", form: "classes: <list>", pattern: /^classes:\s*(.+)$/iu, listed: true }),
65
+ ])
66
+ const lineOf = (text, at) => text.slice(0, at).split("\n").length
67
+ const listOf = (text) => text.split(",").map((entry) => entry.trim()).filter(Boolean)
68
+
69
+ /** brief.md from --brief, else the session's; a usage error when neither is known. */
70
+ export function briefPathOf(flags) {
71
+ if (flags.brief !== undefined && flags.brief !== true) return resolve(flags.brief)
72
+ const dir = flags["session-dir"] ?? process.env.SESSION_DIR
73
+ if (!dir || dir === true) throw new CliError("--brief is required without --session-dir (or SESSION_DIR)")
74
+ return join(resolve(dir), "brief.md")
75
+ }
76
+
77
+ /**
78
+ * @returns {{ parsed: object, refusal: null } | { parsed: null, refusal: { json: object, summary: string, exitCode: 1 } }}
79
+ */
80
+ export function lockedBrief(command, flags) {
81
+ const path = briefPathOf(flags)
82
+ const lock = requireLockedBrief(path)
83
+ if (lock.exitCode === 2) throw new CliError(`${path}: ${lock.errors.join("; ")}`)
84
+ if (lock.locked) return { parsed: lock.parsed, refusal: null }
85
+ const json = { command, status: "fail", exitCode: 1, defects: lock.defects }
86
+ return { parsed: null, refusal: { json, summary: `${command}: ${lock.defects[0].message}`, exitCode: 1 } }
87
+ }
88
+
89
+ /**
90
+ * The source floor as a rule, or null for `standard`; a usage error for any other text.
91
+ * @returns {null | { kind: "primary" } | { kind: "pointed" | "classes", list: string[] }}
92
+ */
93
+ export function parseSourceFloor(value) {
94
+ const text = value.trim()
95
+ for (const rule of SOURCE_FLOOR_RULES) {
96
+ const match = rule.pattern.exec(text)
97
+ if (!match) continue
98
+ if (rule.kind === "standard") return null
99
+ return rule.listed ? { kind: rule.kind, list: listOf(match[1]) } : { kind: rule.kind }
100
+ }
101
+ throw new CliError(`source floor "${value}" is not ${SOURCE_FLOOR_RULES.map((rule) => rule.form).join(", ")}`)
102
+ }
103
+
104
+ function hostOf(url) {
105
+ return typeof url === "string" && URL.canParse(url) ? new URL(url).hostname.toLowerCase() : null
106
+ }
107
+
108
+ /** Whether one source (id and meta.json) satisfies a parsed floor rule. */
109
+ export function sourceInsideFloor(rule, sourceId, meta) {
110
+ if (rule.kind === "primary") return meta.provenance === "primary"
111
+ if (rule.kind === "pointed") return rule.list.includes(sourceId) || rule.list.includes(meta.url)
112
+ const host = hostOf(meta.url)
113
+ return host !== null && rule.list.some((entry) => host === entry.toLowerCase() || host.endsWith(`.${entry.toLowerCase()}`))
114
+ }
115
+
116
+ /** The fields fact fill consumes: the fallback form and whether a translation binds to its quote. */
117
+ export function fillPolicy(parsed) {
118
+ const field = (name) => parsed.fields[name]
119
+ return {
120
+ fallback: FIDELITY_VALUE_RULES.fallback(field("fallback")),
121
+ translations: FIDELITY_VALUE_RULES.translations(field("languages")),
122
+ derivedNone: FIDELITY_VALUE_RULES.derivedNone(field("derived numbers")),
123
+ floor: parseSourceFloor(field("source floor")),
124
+ }
125
+ }
126
+
127
+ /** Source ids behind a row: its own, or for a derived row its operands' sources; null marks a leaf with no source. */
128
+ function sourcesOf(row, rows, seen = new Set()) {
129
+ if (seen.has(row.fact_id)) return []
130
+ seen.add(row.fact_id)
131
+ if (row.source_id) return [row.source_id]
132
+ const inputs = [...(row.operands ?? []), ...(row.derived_from ?? [])]
133
+ if (inputs.length === 0) return [null]
134
+ return inputs.flatMap((id) => (rows.has(id) ? sourcesOf(rows.get(id), rows, seen) : [null]))
135
+ }
136
+
137
+ /** sha256 of the locked `## Fidelity` block normalized: the seven fields in FIDELITY_FIELDS order, `field: value` per line. */
138
+ export const briefHashOf = (parsed) => sha256Of(FIDELITY_FIELDS.map((field) => `${field}: ${parsed.fields[field]}`).join("\n"))
139
+
140
+ /** brief_changed when a fill map was not written under the current brief (or predates the binding). */
141
+ export function briefBindingErrors(map, mapPath, parsed) {
142
+ if (map.brief_sha256 === briefHashOf(parsed)) return []
143
+ const why = typeof map.brief_sha256 === "string" ? "was filled under a different ## Fidelity block" : "records no brief_sha256"
144
+ return [{ code: "brief_changed", token: null, line: null, message: `${mapPath} ${why}; re-run fact fill under the current brief`, severity: DEFECT_CODES.brief_changed.severity, integrity: DEFECT_CODES.brief_changed.integrity, hint: DEFECT_CODES.brief_changed.hint }]
145
+ }
146
+
147
+ function metaOf(sessionDir, sourceId, cache) {
148
+ if (!cache.has(sourceId)) {
149
+ const file = join(sessionDir, "sources", sourceId, "meta.json")
150
+ cache.set(sourceId, existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : { url: null, provenance: "unknown" })
151
+ }
152
+ return cache.get(sourceId)
153
+ }
154
+
155
+ const defect = (code, pointer, text, message) => ({ code, token: pointer.token, line: lineOf(text, pointer.start), message, severity: DEFECT_CODES[code].severity, hint: DEFECT_CODES[code].hint })
156
+
157
+ /**
158
+ * Refusals for the placeholders of one fill source (or the data-fact elements of one edition at
159
+ * verify). Sets each referenced row's outside_floor to this evaluation and reports whether any
160
+ * cached value changed.
161
+ * @param {{ token: string, start: number, parsed: { id?: string } }[]} found findPlaceholders output
162
+ * @returns {{ errors: object[], cacheChanged: boolean }}
163
+ */
164
+ export function policyErrors(text, found, ledger, sessionDir, policy) {
165
+ const rows = new Map(ledger.rows.map((row) => [row.fact_id, row]))
166
+ const metas = new Map()
167
+ const errors = []
168
+ let cacheChanged = false
169
+ for (const pointer of found) {
170
+ const row = rows.get(pointer.parsed?.id)
171
+ if (row === undefined || row.status !== "active") continue
172
+ if (policy.derivedNone && FIDELITY_VALUE_RULES.derivedRefused(row.kind)) {
173
+ errors.push(defect("derived_not_allowed", pointer, text, `${row.fact_id} is ${row.kind}; the brief records derived numbers: none`))
174
+ }
175
+ const sources = sourcesOf(row, rows)
176
+ const outside = policy.floor === null ? [] : [...new Set(sources.filter((id) => id === null || !sourceInsideFloor(policy.floor, id, metaOf(sessionDir, id, metas))))]
177
+ if (row.outside_floor !== outside.length > 0) cacheChanged = true
178
+ row.outside_floor = outside.length > 0
179
+ const named = outside.map((id) => id ?? "no source").join(", ")
180
+ if (outside.length > 0) errors.push(defect("source_outside_floor", pointer, text, `${row.fact_id} rests on ${named}, outside the source floor`))
181
+ }
182
+ return { errors, cacheChanged }
183
+ }
@@ -0,0 +1,25 @@
1
+ // Every references/*.md must have balanced code fences: an unclosed fence renders prose as code
2
+ // and flips every later fence in the file. md-lite is the parser the skill's own tools use.
3
+ import { describe, expect, test } from "bun:test"
4
+ import { readdirSync, readFileSync } from "node:fs"
5
+ import { join } from "node:path"
6
+
7
+ import { parseMarkdown } from "./md-lite.mjs"
8
+
9
+ const REFERENCES = join(import.meta.dir, "..", "references")
10
+ const FILES = readdirSync(REFERENCES).filter((name) => name.endsWith(".md")).sort()
11
+
12
+ describe("references/*.md fence parity", () => {
13
+ test("#given the references directory #when listed #then it holds the reference pages", () => {
14
+ expect(FILES.length).toBeGreaterThan(0)
15
+ })
16
+
17
+ for (const name of FILES) {
18
+ test(`#given ${name} #when its fences are counted #then the count is even and md-lite reports no problem`, () => {
19
+ const text = readFileSync(join(REFERENCES, name), "utf8")
20
+ const fences = text.split("\n").filter((line) => /^ {0,3}(`{3,}|~{3,})/u.test(line))
21
+ expect(fences.length % 2, `${name} has ${fences.length} fence lines`).toBe(0)
22
+ expect(parseMarkdown(text).problems).toEqual([])
23
+ })
24
+ }
25
+ })