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
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
// escaped HTML projection (same line numbers) and checked with its fill map, which is the only
|
|
9
9
|
// record of where its values sit; LaTeX is verified through its Markdown source.
|
|
10
10
|
//
|
|
11
|
+
// The locked brief is re-judged here, not only at fill: the edition's fill map (--fill-map, else
|
|
12
|
+
// `<report>.fill-map.json` when present) must carry the current brief's brief_sha256
|
|
13
|
+
// (brief_changed), and policyErrors re-runs over every data-fact the edition carries, so an
|
|
14
|
+
// edition that violates the current derived-numbers or source-floor rule fails here too.
|
|
15
|
+
//
|
|
11
16
|
// The delivery barrier that reads these records (and every other fidelity record) is
|
|
12
17
|
// fidelity-records.mjs.
|
|
13
18
|
|
|
@@ -18,6 +23,8 @@ import { CliError, readJsonFile, sha256Of } from "./cli-support.mjs"
|
|
|
18
23
|
import { countDefects } from "./contracts.mjs"
|
|
19
24
|
import { recompute } from "./fact-derive.mjs"
|
|
20
25
|
import { readLedger } from "./fact-ledger.mjs"
|
|
26
|
+
import { validateLedger } from "./fact-ledger-schema.mjs"
|
|
27
|
+
import { briefBindingErrors, fillPolicy, lockedBrief, policyErrors } from "./fidelity-policy.mjs"
|
|
21
28
|
import { FIDELITY_RECORD, writeFidelityRecord } from "./fidelity-records.mjs"
|
|
22
29
|
import { blocksDelivery, checkFidelity } from "./gates-fidelity.mjs"
|
|
23
30
|
import { parseMarkdown } from "./md-lite.mjs"
|
|
@@ -120,6 +127,24 @@ export function markdownAsHtml(text, entries, ledger) {
|
|
|
120
127
|
return { html: `${html}${open ? MD_HTML_BLOCK_RULES[open].group[1] : ""}</body></html>\n`, stale: [...stale] }
|
|
121
128
|
}
|
|
122
129
|
|
|
130
|
+
/** The session dir from --session-dir or SESSION_DIR, or null; a bare flag names no session. */
|
|
131
|
+
export const sessionOf = (flags) => {
|
|
132
|
+
const dir = flags["session-dir"] ?? process.env.SESSION_DIR
|
|
133
|
+
return dir && dir !== true ? resolve(dir) : null
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The ledger from --ledger (validated) or from the session dir; every fact command loads it here. */
|
|
137
|
+
export function ledgerFrom(flags, session = sessionOf(flags)) {
|
|
138
|
+
if (flags.ledger === undefined) {
|
|
139
|
+
if (session === null) throw new CliError("--ledger is required without --session-dir (or SESSION_DIR)")
|
|
140
|
+
return readLedger(session)
|
|
141
|
+
}
|
|
142
|
+
const ledger = readJsonFile(flags.ledger)
|
|
143
|
+
const errors = validateLedger(ledger)
|
|
144
|
+
if (errors.length > 0) throw new CliError(`${flags.ledger} is not a valid fact ledger: ${errors[0]}`)
|
|
145
|
+
return ledger
|
|
146
|
+
}
|
|
147
|
+
|
|
123
148
|
const MARKDOWN_EXTENSION = /\.(md|markdown)$/iu
|
|
124
149
|
/** The edition format: the explicit flag, else md for a .md or .markdown path (any case), else html. */
|
|
125
150
|
export const editionFormatOf = (path, flag) => flag ?? (MARKDOWN_EXTENSION.test(path) ? "md" : "html")
|
|
@@ -140,8 +165,11 @@ export function runFactVerify(args) {
|
|
|
140
165
|
const [report] = args.positionals
|
|
141
166
|
const { flags } = args
|
|
142
167
|
if (!report) throw new CliError("usage: fact verify <report> [--format html|md] [--sources D] [--claims claims.json] [--fill-map F] [--brief brief.md] [--out fidelity-defects.json]")
|
|
143
|
-
const edition = readEdition(report, flags)
|
|
144
168
|
const sessionDir = sessionDirOf(flags)
|
|
169
|
+
const { parsed, refusal } = lockedBrief("fact verify", flags)
|
|
170
|
+
if (refusal) return refusal
|
|
171
|
+
const policy = fillPolicy(parsed)
|
|
172
|
+
const edition = readEdition(report, flags)
|
|
145
173
|
const claimsPath = flags.claims ?? join(sessionDir, "claims.json")
|
|
146
174
|
const briefPath = flags.brief ?? join(sessionDir, "brief.md")
|
|
147
175
|
const ledger = readLedger(sessionDir)
|
|
@@ -155,10 +183,15 @@ export function runFactVerify(args) {
|
|
|
155
183
|
baseDir: dirname(resolve(report)),
|
|
156
184
|
recompute,
|
|
157
185
|
})
|
|
158
|
-
const
|
|
159
|
-
const
|
|
186
|
+
const checked = projected?.html ?? edition.text
|
|
187
|
+
const pointers = [...checked.matchAll(/\bdata-fact="([^"]+)"/gu)].map((match) => ({ token: match[0], start: match.index, parsed: { id: match[1] } }))
|
|
188
|
+
const mapPath = flags["fill-map"] ?? `${report}.fill-map.json`
|
|
189
|
+
const bound = existsSync(mapPath) ? briefBindingErrors(readJsonFile(mapPath), mapPath, parsed) : []
|
|
190
|
+
const defects = [...bound, ...policyErrors(checked, pointers, ledger, sessionDir, policy).errors, ...result.defects]
|
|
191
|
+
const counts = countDefects(defects)
|
|
192
|
+
const status = defects.some(blocksDelivery) ? "fail" : "pass"
|
|
160
193
|
const exitCode = status === "fail" ? 1 : 0
|
|
161
194
|
const out = resolve(flags.out ?? join(sessionDir, FIDELITY_RECORD))
|
|
162
|
-
const record = writeFidelityRecord(out, "fidelity", { command: "fact verify", status, exitCode, format: edition.format, report: resolve(report), artifact_sha256: sha256Of(edition.text), defects
|
|
195
|
+
const record = writeFidelityRecord(out, "fidelity", { command: "fact verify", status, exitCode, format: edition.format, report: resolve(report), artifact_sha256: sha256Of(edition.text), defects, counts, gatesRun: result.gatesRun })
|
|
163
196
|
return { json: { ...record, out }, summary: `fact verify: ${status.toUpperCase()} - ${counts.blocker} blocker, ${counts.major} major, ${counts.minor} minor`, exitCode }
|
|
164
197
|
}
|
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
// fact-commands.mjs - the `fact` subcommands of report-tools: add, quote, retract, export,
|
|
2
|
-
//
|
|
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 {
|
|
10
|
+
import { join, resolve } from "node:path"
|
|
10
11
|
|
|
11
|
-
import { CliError, parseArgs, sha256Of
|
|
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
|
|
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
|
-
|
|
13
|
+
/** The output formats fill renders. */
|
|
14
|
+
export const FILL_FORMATS = Object.freeze(["html", "md"])
|
|
14
15
|
const TRANSLATION = Object.freeze({
|
|
15
16
|
html: /^\s*<span data-translates="(Q\d+)">[\s\S]*?<\/span>/u,
|
|
16
17
|
md: /^\s*<!-- translates: (Q\d+) -->[^\n]*/u,
|
|
@@ -166,7 +167,7 @@ function assemble(text, parts, context) {
|
|
|
166
167
|
*/
|
|
167
168
|
export function fill(sourceText, ledger, options = {}) {
|
|
168
169
|
const format = options.format ?? "html"
|
|
169
|
-
if (!
|
|
170
|
+
if (!FILL_FORMATS.includes(format)) throw new TypeError(`format must be one of ${FILL_FORMATS.join(", ")}`)
|
|
170
171
|
const rows = new Map(ledger.rows.map((row) => [row.fact_id, row]))
|
|
171
172
|
const found = findPlaceholders(sourceText)
|
|
172
173
|
const errors = []
|
|
@@ -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,
|
|
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
|
|
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,
|
|
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 === ""
|
|
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
|
|
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,
|
|
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,
|
|
180
|
+
const pair = translationPair(node, rows, slug, reportLang)
|
|
183
181
|
if (pair) out.push(pair)
|
|
184
182
|
}
|
|
185
183
|
}
|
|
@@ -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
|
+
}
|
|
@@ -14,6 +14,7 @@ import { join, resolve } from "node:path"
|
|
|
14
14
|
import { indexClaims } from "./claims-index.mjs"
|
|
15
15
|
import { CliError, parseArgs, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
|
|
16
16
|
import { parseFidelityBlock } from "./fidelity-brief.mjs"
|
|
17
|
+
import { parseSourceFloor } from "./fidelity-policy.mjs"
|
|
17
18
|
import { writeSnapshot } from "./source-snapshot.mjs"
|
|
18
19
|
|
|
19
20
|
const FETCH_TIMEOUT_MS = 30_000
|
|
@@ -44,12 +45,16 @@ export function snapshotOf(sessionDir, id) {
|
|
|
44
45
|
return { meta: JSON.parse(readInput(join(dir, "meta.json"))), text: readInput(join(dir, "text.txt")), html: existsSync(html) ? readInput(html) : null }
|
|
45
46
|
}
|
|
46
47
|
|
|
47
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Whether the brief's source floor lists this source as pointed at (`pointed only: a, b`). An absent
|
|
50
|
+
* or pending floor points at nothing; a malformed floor is a usage error (parseSourceFloor's CliError).
|
|
51
|
+
*/
|
|
48
52
|
export function pointedFlag(sessionDir, url) {
|
|
49
53
|
const brief = join(sessionDir, "brief.md")
|
|
50
54
|
const floor = existsSync(brief) ? (parseFidelityBlock(readInput(brief)).fields?.["source floor"] ?? "") : ""
|
|
51
|
-
|
|
52
|
-
|
|
55
|
+
if (floor === "" || floor === "pending") return false
|
|
56
|
+
const rule = parseSourceFloor(floor)
|
|
57
|
+
return rule?.kind === "pointed" && rule.list.includes(url)
|
|
53
58
|
}
|
|
54
59
|
|
|
55
60
|
async function fetchSource(url) {
|
|
@@ -158,7 +158,7 @@ prefixes when identity is needed.
|
|
|
158
158
|
`;
|
|
159
159
|
const reviewWorkCodexGatePattern = new RegExp(reviewWorkCodexGate.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"));
|
|
160
160
|
|
|
161
|
-
const ulwResearchProofreadGate = "**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
|
|
161
|
+
const ulwResearchProofreadGate = "**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.";
|
|
162
162
|
// The shared skill numbers the proofread gate right after the Visual QA item the Codex edition ends on.
|
|
163
163
|
const ulwResearchVisualQaItemPattern = /^(\d+)\. \*\*Visual QA\.\*\*[^\n]*inspect the pixels\.$/m;
|
|
164
164
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// omo-codex-install:
|
|
2
|
+
// omo-codex-install:59bc5768b3b3ef6732c97bec7583a523952e5e1d9fdf5d8e5fb245658043bfca:fadb66139b4d0de34630f6b4bddc87783374d2be2659a207a4bb01c2c2f852d0
|
|
3
3
|
var __esm = (fn, res, err) => () => {
|
|
4
4
|
if (fn)
|
|
5
5
|
try {
|
|
@@ -10123,7 +10123,7 @@ var package_default;
|
|
|
10123
10123
|
var init_package = __esm(() => {
|
|
10124
10124
|
package_default = {
|
|
10125
10125
|
name: "@oh-my-opencode/omo-codex",
|
|
10126
|
-
version: "5.1.
|
|
10126
|
+
version: "5.1.48",
|
|
10127
10127
|
type: "module",
|
|
10128
10128
|
private: true,
|
|
10129
10129
|
description: "Codex harness adapter for oh-my-openagent. Vendored Codex plugin namespace (omo) + TypeScript installer + telemetry.",
|
|
@@ -10380,7 +10380,7 @@ function resolveStdio(options) {
|
|
|
10380
10380
|
return [options.stdin ?? "ignore", options.stdout ?? "pipe", options.stderr ?? "inherit"];
|
|
10381
10381
|
}
|
|
10382
10382
|
function createBunSpawnOptions(options) {
|
|
10383
|
-
return { ...options, windowsHide: true };
|
|
10383
|
+
return { ...options, env: options.env ?? process.env, windowsHide: true };
|
|
10384
10384
|
}
|
|
10385
10385
|
function createNodeSpawnOptions(options, platform = process.platform) {
|
|
10386
10386
|
const nodeOptions = {
|
|
@@ -10612,8 +10612,7 @@ function windowsNodeDiscoveryLines() {
|
|
|
10612
10612
|
' for /f "tokens=* delims= " %%N in ("!OMO_NODE_BINARY!") do set "OMO_NODE_BINARY=%%N"',
|
|
10613
10613
|
` if "!OMO_NODE_BINARY:~0,1!"=="'" set "OMO_NODE_BINARY=!OMO_NODE_BINARY:~1!"`,
|
|
10614
10614
|
` if "!OMO_NODE_BINARY:~-1!"=="'" set "OMO_NODE_BINARY=!OMO_NODE_BINARY:~0,-1!"`,
|
|
10615
|
-
'
|
|
10616
|
-
' if "!OMO_NODE_BINARY:~-1!"=="^"" set "OMO_NODE_BINARY=!OMO_NODE_BINARY:~0,-1!"',
|
|
10615
|
+
' set "OMO_NODE_BINARY=!OMO_NODE_BINARY:"=!"',
|
|
10617
10616
|
' if defined OMO_NODE_BINARY if not exist "!OMO_NODE_BINARY!" set "OMO_NODE_BINARY="',
|
|
10618
10617
|
")",
|
|
10619
10618
|
'if not defined OMO_NODE_BINARY where node >nul 2>nul && set "OMO_NODE_BINARY=node"'
|
|
@@ -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
|
|