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
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// fact-commands-fill.mjs - `fact fill`, the reader-facing fill of a placeholder source.
|
|
2
|
+
//
|
|
3
|
+
// It refuses an unlocked brief (fidelity_interview_incomplete) and applies the locked `## Fidelity`
|
|
4
|
+
// fields: `derived numbers: none` and the source floor refuse rows (derived_not_allowed,
|
|
5
|
+
// source_outside_floor, evaluated at every fill), `languages` decides whether a translation after a
|
|
6
|
+
// quote binds to it, and `fallback` picks the block quote or omission for --replace-failed. Any
|
|
7
|
+
// error writes no edition, fill map, or state; the one write that survives a refusal is the
|
|
8
|
+
// ledger's cached outside_floor per row (skipped under --view). The fill map records brief_sha256,
|
|
9
|
+
// the locked block fact verify checks it against. node: builtins and sibling imports only.
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync } from "node:fs"
|
|
12
|
+
import { dirname, extname, resolve } from "node:path"
|
|
13
|
+
|
|
14
|
+
import { CliError, sha256Of, writeJsonAtomic } from "./cli-support.mjs"
|
|
15
|
+
import { DEFECT_CODES } from "./contracts.mjs"
|
|
16
|
+
import { FILL_FORMATS, fill } from "./fact-fill.mjs"
|
|
17
|
+
import { readLedger, writeLedger } from "./fact-ledger.mjs"
|
|
18
|
+
import { briefHashOf, fillPolicy, lockedBrief, policyErrors } from "./fidelity-policy.mjs"
|
|
19
|
+
import { readState, writeState } from "./fidelity-state.mjs"
|
|
20
|
+
import { findPlaceholders } from "./placeholder.mjs"
|
|
21
|
+
import { sessionDirOf } from "./source-commands.mjs"
|
|
22
|
+
|
|
23
|
+
/** The CLI's input vocabulary: fill's output formats plus aliases (tex is filled as md), and the extension defaults. */
|
|
24
|
+
export const FILL_INPUT_FORMAT_RULES = Object.freeze({
|
|
25
|
+
aliases: Object.freeze({ tex: "md" }),
|
|
26
|
+
extensions: Object.freeze({ ".md": "md", ".tex": "tex" }),
|
|
27
|
+
})
|
|
28
|
+
export const FILL_INPUT_FORMATS = Object.freeze([...FILL_FORMATS, ...Object.keys(FILL_INPUT_FORMAT_RULES.aliases)])
|
|
29
|
+
const fillFormatOf = (format) => FILL_INPUT_FORMAT_RULES.aliases[format] ?? format
|
|
30
|
+
|
|
31
|
+
function readText(path) {
|
|
32
|
+
try {
|
|
33
|
+
return readFileSync(path, "utf8")
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The inline edition: local stylesheets and scripts inlined, relative image paths made absolute. */
|
|
40
|
+
function inlineAssets(html, baseDir) {
|
|
41
|
+
const local = (ref) => !/^[a-z][a-z0-9+.-]*:|^\/\//i.test(ref)
|
|
42
|
+
return html
|
|
43
|
+
.replace(/<link\b[^>]*\brel=["']stylesheet["'][^>]*>/gi, (tag) => {
|
|
44
|
+
const href = /\bhref=["']([^"']+)["']/i.exec(tag)?.[1]
|
|
45
|
+
return href && local(href) ? `<style>${readText(resolve(baseDir, href))}</style>` : tag
|
|
46
|
+
})
|
|
47
|
+
.replace(/<script\b([^>]*)\bsrc=["']([^"']+)["']([^>]*)><\/script>/gi, (tag, pre, src, post) => (local(src) ? `<script${pre}${post}>${readText(resolve(baseDir, src))}</script>` : tag))
|
|
48
|
+
.replace(/(<img\b[^>]*\bsrc=["'])([^"']+)(["'])/gi, (tag, pre, src, post) => (local(src) && !src.startsWith("/") ? `${pre}${resolve(baseDir, src)}${post}` : tag))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** @param {{ positionals: string[], flags: Record<string, any> }} args parsed by runFact */
|
|
52
|
+
export function runFactFill(args) {
|
|
53
|
+
const [source] = args.positionals
|
|
54
|
+
const { flags } = args
|
|
55
|
+
if (!source || (flags.out === undefined && !flags.view)) throw new CliError("usage: fact fill <src> (--out <path> | --view) [--format html|md|tex] [--inline] [--replace-failed] [--brief brief.md]")
|
|
56
|
+
const format = flags.format ?? FILL_INPUT_FORMAT_RULES.extensions[extname(source).toLowerCase()] ?? "html"
|
|
57
|
+
if (!FILL_INPUT_FORMATS.includes(format)) throw new CliError(`--format must be ${FILL_INPUT_FORMATS.slice(0, -1).join(", ")}, or ${FILL_INPUT_FORMATS.at(-1)}`)
|
|
58
|
+
if (flags.inline && format !== "html") throw new CliError("--inline applies to the html edition only")
|
|
59
|
+
const sessionDir = sessionDirOf(flags)
|
|
60
|
+
const { parsed, refusal } = lockedBrief("fact fill", flags)
|
|
61
|
+
if (refusal) return refusal
|
|
62
|
+
const policy = fillPolicy(parsed)
|
|
63
|
+
const text = readText(source)
|
|
64
|
+
const ledger = readLedger(sessionDir)
|
|
65
|
+
const refused = policyErrors(text, findPlaceholders(text), ledger, sessionDir, policy)
|
|
66
|
+
if (refused.cacheChanged && !flags.view) writeLedger(sessionDir, ledger)
|
|
67
|
+
const state = flags["replace-failed"] ? readState(sessionDir) : null
|
|
68
|
+
const result = fill(text, ledger, { format: fillFormatOf(format), view: flags.view === true, fallback: policy.fallback, ...(state ? { replaceFailed: state } : {}) })
|
|
69
|
+
const warnings = result.warnings
|
|
70
|
+
if (result.out === null || refused.errors.length > 0) {
|
|
71
|
+
const filled = result.errors.map((error) => ({ ...error, severity: DEFECT_CODES[error.code]?.severity ?? "blocker", hint: DEFECT_CODES[error.code]?.hint ?? null }))
|
|
72
|
+
const defects = [...refused.errors, ...filled]
|
|
73
|
+
return { json: { command: "fact fill", status: "fail", exitCode: 1, defects, warnings }, summary: `fact fill: FAIL - ${defects.length} error(s), nothing written`, exitCode: 1 }
|
|
74
|
+
}
|
|
75
|
+
const entries = policy.translations ? result.fillMap : result.fillMap.filter((entry) => entry.translates === undefined)
|
|
76
|
+
const out = flags.inline ? inlineAssets(result.out, dirname(resolve(source))) : result.out
|
|
77
|
+
if (flags.view) return { text: out, summary: `fact fill --view: ${entries.length} pointer(s), ${warnings.length} warning(s)`, exitCode: 0 }
|
|
78
|
+
writeFileSync(flags.out, out)
|
|
79
|
+
if (state) writeState(sessionDir, state)
|
|
80
|
+
const artifact = sha256Of(out)
|
|
81
|
+
const map = `${resolve(flags.out)}.fill-map.json`
|
|
82
|
+
const brief = briefHashOf(parsed)
|
|
83
|
+
writeJsonAtomic(map, { schemaVersion: 1, format, artifact: resolve(flags.out), artifact_sha256: artifact, brief_sha256: brief, entries })
|
|
84
|
+
const data = { out: resolve(flags.out), map, artifact_sha256: artifact, brief_sha256: brief, occurrences: entries.length, warnings }
|
|
85
|
+
return { json: { command: "fact fill", status: "pass", exitCode: 0, defects: [], data }, summary: `fact fill: wrote ${flags.out} (${entries.length} pointer(s))`, exitCode: 0 }
|
|
86
|
+
}
|
|
@@ -1,24 +1,26 @@
|
|
|
1
1
|
// fact-commands-meaning.mjs - `fact pairs` and `fact meaning-check`, the script half of the
|
|
2
2
|
// separate-session meaning verifier. fact pairs writes the (source, report sentence) pairs a
|
|
3
|
-
// reader lane gets and nothing else
|
|
3
|
+
// reader lane gets and nothing else (with --changed-since, only the pairs whose sentence polish
|
|
4
|
+
// rewrote); it honours the brief's `meaning check` field and writes
|
|
4
5
|
// `{ skipped: true }` when it is off. fact meaning-check ingests the readers' verdict files
|
|
5
6
|
// (meaning-check.mjs), writes fidelity-meaning-defects.json (a fidelity record bound to the pairs'
|
|
6
7
|
// artifact_sha256 and report, through writeFidelityRecord), and records the run in
|
|
7
8
|
// fidelity-calibration.jsonl: a header with the promotion criterion on first write, then per run
|
|
8
9
|
// one run line plus one line per flagged pair. The run id hashes the inputs only, so a rerun or a
|
|
9
10
|
// --dispute rerun of the same inputs replaces that run's lines in place (disputes carry forward)
|
|
10
|
-
// instead of counting a second run.
|
|
11
|
+
// instead of counting a second run. Both refuse an unlocked brief (fidelity_interview_incomplete) before
|
|
12
|
+
// reading anything else. Neither command calls a model or the network.
|
|
11
13
|
|
|
12
14
|
import { existsSync, readFileSync } from "node:fs"
|
|
13
15
|
import { dirname, join, resolve } from "node:path"
|
|
14
16
|
|
|
15
17
|
import { CliError, parseArgs, readJsonFile, sha256Of, writeJsonAtomic, writeTextAtomic } from "./cli-support.mjs"
|
|
16
18
|
import { countDefects } from "./contracts.mjs"
|
|
17
|
-
import { markdownAsHtml, readEdition } from "./fact-commands-verify.mjs"
|
|
18
|
-
import {
|
|
19
|
-
import { validateLedger } from "./fact-ledger-schema.mjs"
|
|
19
|
+
import { ledgerFrom, markdownAsHtml, readEdition, sessionOf } from "./fact-commands-verify.mjs"
|
|
20
|
+
import { pairsChangedSince } from "./fact-commands-polish.mjs"
|
|
20
21
|
import { extractPairs, PAIR_KINDS } from "./fact-pairs.mjs"
|
|
21
|
-
import { FIDELITY_INCOMPLETE
|
|
22
|
+
import { FIDELITY_INCOMPLETE } from "./fidelity-brief.mjs"
|
|
23
|
+
import { briefPathOf, lockedBrief, meaningCheckEnabled } from "./fidelity-policy.mjs"
|
|
22
24
|
import { MEANING_RECORD, writeFidelityRecord } from "./fidelity-records.mjs"
|
|
23
25
|
import { blocksDelivery } from "./gates-fidelity.mjs"
|
|
24
26
|
import { ingestVerdicts, unverifiedDefects, VERDICT_SCHEMA } from "./meaning-check.mjs"
|
|
@@ -31,45 +33,20 @@ export const PROMOTION = Object.freeze({ code: "meaning_drift_confirmed", to: "i
|
|
|
31
33
|
/** The command status: fail when any defect blocks delivery under the registry policy. */
|
|
32
34
|
export const meaningStatus = (defects) => (defects.some(blocksDelivery) ? "fail" : "pass")
|
|
33
35
|
|
|
34
|
-
const sessionOf = (flags) => {
|
|
35
|
-
const dir = flags["session-dir"] ?? process.env.SESSION_DIR
|
|
36
|
-
return dir && dir !== true ? resolve(dir) : null
|
|
37
|
-
}
|
|
38
36
|
const inSession = (session, name, flag) => {
|
|
39
37
|
if (session === null) throw new CliError(`${flag} is required without --session-dir (or SESSION_DIR)`)
|
|
40
38
|
return join(session, name)
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
function readLedgerFrom(flags, session) {
|
|
44
|
-
if (flags.ledger === undefined) {
|
|
45
|
-
if (session === null) throw new CliError("--ledger is required without --session-dir (or SESSION_DIR)")
|
|
46
|
-
return readLedger(session)
|
|
47
|
-
}
|
|
48
|
-
const ledger = readJsonFile(flags.ledger)
|
|
49
|
-
const errors = validateLedger(ledger)
|
|
50
|
-
if (errors.length > 0) throw new CliError(`${flags.ledger} is not a valid fact ledger: ${errors[0]}`)
|
|
51
|
-
return ledger
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
function briefState(flags, session) {
|
|
55
|
-
const path = flags.brief ?? inSession(session, "brief.md", "--brief")
|
|
56
|
-
const lock = requireLockedBrief(path)
|
|
57
|
-
if (lock.exitCode === 2) throw new CliError(`${path}: malformed ## Fidelity block: ${lock.errors.join("; ")}`)
|
|
58
|
-
return { path, locked: lock.locked, off: !meaningCheckEnabled(lock.parsed) }
|
|
59
|
-
}
|
|
60
|
-
|
|
61
41
|
function runPairs(args) {
|
|
62
42
|
const [report] = args.positionals
|
|
63
43
|
const { flags } = args
|
|
64
44
|
if (!report) throw new CliError("usage: fact pairs <report> [--format html|md] [--ledger F] [--claims claims.json] [--changed-since <fill-map.json>] [--brief brief.md] [--out pairs.json]")
|
|
65
45
|
const session = sessionOf(flags)
|
|
66
46
|
const out = resolve(flags.out ?? inSession(session, PAIRS_FILE, "--out"))
|
|
67
|
-
const
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
return { json, summary: `fact pairs: ${json.message}`, exitCode: 1 }
|
|
71
|
-
}
|
|
72
|
-
if (brief.off) {
|
|
47
|
+
const { parsed, refusal } = lockedBrief("fact pairs", flags)
|
|
48
|
+
if (refusal) return refusal
|
|
49
|
+
if (!meaningCheckEnabled(parsed)) {
|
|
73
50
|
const record = { schemaVersion: 1, skipped: true, reason: "meaning check: off", pairs: [] }
|
|
74
51
|
writeJsonAtomic(out, record)
|
|
75
52
|
return { json: { command: "fact pairs", status: "skipped", exitCode: 0, data: { out, ...record } }, summary: "fact pairs: skipped (meaning check: off)", exitCode: 0 }
|
|
@@ -77,10 +54,10 @@ function runPairs(args) {
|
|
|
77
54
|
const edition = readEdition(report, flags)
|
|
78
55
|
const claimsPath = flags.claims ?? (session === null ? null : join(session, "claims.json"))
|
|
79
56
|
const claims = claimsPath !== null && (flags.claims !== undefined || existsSync(claimsPath)) ? readJsonFile(claimsPath) : null
|
|
80
|
-
const
|
|
81
|
-
const ledger = readLedgerFrom(flags, session)
|
|
57
|
+
const ledger = ledgerFrom(flags, session)
|
|
82
58
|
const html = edition.format === "md" ? markdownAsHtml(edition.text, edition.entries, ledger).html : edition.text
|
|
83
|
-
const
|
|
59
|
+
const all = extractPairs(html, ledger, claims)
|
|
60
|
+
const pairs = flags["changed-since"] === undefined ? all : pairsChangedSince(all, flags["changed-since"], ledger, claims)
|
|
84
61
|
const counts = { total: pairs.length, force_strong: pairs.filter((pair) => pair.force_strong).length }
|
|
85
62
|
for (const kind of PAIR_KINDS) counts[kind] = pairs.filter((pair) => pair.kind === kind).length
|
|
86
63
|
const record = { schemaVersion: 1, skipped: false, report: resolve(report), format: edition.format, artifact_sha256: sha256Of(edition.text), pairs_sha256: sha256Of(JSON.stringify(pairs)), counts, verdict_schema: VERDICT_SCHEMA, pairs }
|
|
@@ -140,23 +117,23 @@ function recordCalibration(path, lines, runId, pairsSha) {
|
|
|
140
117
|
}
|
|
141
118
|
|
|
142
119
|
/** A `{ skipped: true }` pairs file is accepted only when the locked brief turns the meaning check off. */
|
|
143
|
-
function skippedRefusal(flags,
|
|
144
|
-
|
|
145
|
-
if (!brief.locked) return { error: FIDELITY_INCOMPLETE, message: `${brief.path} has no locked ## Fidelity block, so a skipped meaning check cannot be accepted` }
|
|
146
|
-
if (!brief.off) return { error: "meaning_check_required", message: `${flags.pairs} says skipped but ${brief.path} has meaning check on; rerun fact pairs` }
|
|
120
|
+
function skippedRefusal(flags, parsed) {
|
|
121
|
+
if (meaningCheckEnabled(parsed)) return { error: "meaning_check_required", message: `${flags.pairs} says skipped but ${briefPathOf(flags)} has meaning check on; rerun fact pairs` }
|
|
147
122
|
return null
|
|
148
123
|
}
|
|
149
124
|
|
|
150
125
|
function runMeaningCheck(args) {
|
|
151
126
|
const { flags } = args
|
|
152
|
-
const usage = "usage: fact meaning-check --pairs pairs.json --verdicts cheap.json [--verdicts strong.json] [--dispute P<n> --reason TEXT] [--run-id ID] [--calibration F] [--out F]"
|
|
127
|
+
const usage = "usage: fact meaning-check --pairs pairs.json --verdicts cheap.json [--verdicts strong.json] [--dispute P<n> --reason TEXT] [--run-id ID] [--calibration F] [--brief brief.md] [--out F]"
|
|
153
128
|
if (flags.pairs === undefined || !Array.isArray(flags.verdicts)) throw new CliError(usage)
|
|
154
129
|
const session = sessionOf(flags)
|
|
130
|
+
const { parsed, refusal } = lockedBrief("fact meaning-check", flags)
|
|
131
|
+
if (refusal) return { ...refusal, json: { ...refusal.json, error: FIDELITY_INCOMPLETE } }
|
|
155
132
|
const pairsText = readFileSync(flags.pairs, "utf8")
|
|
156
133
|
const pairsDoc = readJsonFile(flags.pairs)
|
|
157
134
|
if (pairsDoc?.skipped === true) {
|
|
158
|
-
const
|
|
159
|
-
if (
|
|
135
|
+
const skipped = skippedRefusal(flags, parsed)
|
|
136
|
+
if (skipped !== null) return { json: { command: "fact meaning-check", status: "fail", exitCode: 1, ...skipped }, summary: `fact meaning-check: ${skipped.message}`, exitCode: 1 }
|
|
160
137
|
return { json: { command: "fact meaning-check", status: "skipped", exitCode: 0, defects: [] }, summary: JSON.stringify({ command: "fact meaning-check", status: "skipped" }), exitCode: 0 }
|
|
161
138
|
}
|
|
162
139
|
if (typeof pairsDoc?.artifact_sha256 !== "string" || typeof pairsDoc.report !== "string") throw new CliError(`${flags.pairs} names no artifact_sha256 and report, so the meaning record cannot be bound to an edition; rerun fact pairs`)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
// fact-commands-polish.mjs - `fact polish-apply` and `fact polish-diff`, the script half of the
|
|
2
|
+
// proofread gate, plus the --changed-since filter of `fact pairs`.
|
|
3
|
+
//
|
|
4
|
+
// The writing lane returns polish-edits.json ([{before, after, reason}]) and never edits the
|
|
5
|
+
// report. polish-apply applies each edit through polish-apply.mjs and writes the result; a
|
|
6
|
+
// rejected (major) or unlocated (minor) edit leaves its region unchanged and is listed with its
|
|
7
|
+
// index and reason so the orchestrator can re-send it; every rejection reason is printed verbatim, one
|
|
8
|
+
// line per rejected edit (index and the first 60 characters of its before), in the summary and in
|
|
9
|
+
// data.rejected_reasons. It exits 0 whenever it wrote the output.
|
|
10
|
+
// polish-diff is the whole-document backstop (polish-diff.mjs) between the pre-polish artifact
|
|
11
|
+
// and any later copy; it exits 1 on a blocker. pairsChangedSince keeps only the pairs whose
|
|
12
|
+
// sentence text (normalized) is not in the pre-polish artifact that a fill map names, so the
|
|
13
|
+
// meaning readers re-check exactly what polish rewrote. No model, no network.
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
16
|
+
import { join, resolve } from "node:path"
|
|
17
|
+
|
|
18
|
+
import { CliError, parseArgs, readJsonFile, sha256Of, writeJsonAtomic, writeTextAtomic } from "./cli-support.mjs"
|
|
19
|
+
import { countDefects, DEFECT_CODES } from "./contracts.mjs"
|
|
20
|
+
import { editionFormatOf, ledgerFrom, markdownAsHtml, sessionOf } from "./fact-commands-verify.mjs"
|
|
21
|
+
import { extractPairs } from "./fact-pairs.mjs"
|
|
22
|
+
import { lockedBrief } from "./fidelity-policy.mjs"
|
|
23
|
+
import { applyEdits, POLISH_EDIT_FIELDS, validatePolishEdits } from "./polish-apply.mjs"
|
|
24
|
+
import { diffPolish } from "./polish-diff.mjs"
|
|
25
|
+
import { normalizeText } from "./text-normalize.mjs"
|
|
26
|
+
|
|
27
|
+
export { ledgerFrom, POLISH_EDIT_FIELDS, validatePolishEdits }
|
|
28
|
+
|
|
29
|
+
export const POLISH_RECORD = "polish-defects.json"
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The polish-diff exit policy: only a registry blocker fails the command; majors are reported.
|
|
33
|
+
* Fail-closed like the delivery barrier: a code the registry does not know refuses outright,
|
|
34
|
+
* because a caller-claimed severity may not demote it.
|
|
35
|
+
*/
|
|
36
|
+
export const polishDiffFails = (defects) =>
|
|
37
|
+
defects.some((defect) => {
|
|
38
|
+
if (!Object.hasOwn(DEFECT_CODES, defect?.code)) throw new CliError(`unknown defect code ${defect?.code}`)
|
|
39
|
+
return DEFECT_CODES[defect.code].severity === "blocker"
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
function readText(path) {
|
|
43
|
+
try {
|
|
44
|
+
return readFileSync(path, "utf8")
|
|
45
|
+
} catch (error) {
|
|
46
|
+
throw new CliError(`cannot read ${path}: ${error.code ?? error.message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const withSeverity = (entry) => ({ ...entry, severity: DEFECT_CODES[entry.code].severity, hint: DEFECT_CODES[entry.code].hint })
|
|
51
|
+
|
|
52
|
+
/** The first 60 characters of an edit's before, on one line. */
|
|
53
|
+
const beforePreview = (before) => (typeof before === "string" ? before : "").replace(/\s+/gu, " ").slice(0, 60)
|
|
54
|
+
|
|
55
|
+
/** One line per rejected edit: its index, the start of its before, and its reason verbatim. */
|
|
56
|
+
const rejectionLine = (entry, edit) => ` rejected edit ${entry.index} ("${beforePreview(edit?.before)}"): ${entry.reason}`
|
|
57
|
+
|
|
58
|
+
function runPolishApply(args) {
|
|
59
|
+
const [report] = args.positionals
|
|
60
|
+
const { flags } = args
|
|
61
|
+
if (!report || flags.edits === undefined || flags.out === undefined) throw new CliError("usage: fact polish-apply <report.html> --edits polish-edits.json --out <report.polished.html> [--ledger F] [--brief brief.md] [--session-dir D]")
|
|
62
|
+
if (editionFormatOf(report, flags.format) === "md") throw new CliError("fact polish-apply takes the filled HTML edition; polish a Markdown edition through its HTML source")
|
|
63
|
+
const { refusal } = lockedBrief("fact polish-apply", flags)
|
|
64
|
+
if (refusal) return refusal
|
|
65
|
+
const html = readText(report)
|
|
66
|
+
const editsText = readText(flags.edits)
|
|
67
|
+
const edits = readJsonFile(flags.edits)
|
|
68
|
+
try {
|
|
69
|
+
validatePolishEdits(edits)
|
|
70
|
+
} catch (error) {
|
|
71
|
+
throw new CliError(`${flags.edits} must be an array of {${Object.keys(POLISH_EDIT_FIELDS).join(", ")}} objects: ${error.message}`)
|
|
72
|
+
}
|
|
73
|
+
const result = applyEdits(html, edits, ledgerFrom(flags))
|
|
74
|
+
writeTextAtomic(flags.out, result.out)
|
|
75
|
+
const defects = [...result.rejected, ...result.unlocated].sort((a, b) => a.index - b.index).map(withSeverity)
|
|
76
|
+
const counts = { applied: result.applied.length, rejected: result.rejected.length, unlocated: result.unlocated.length }
|
|
77
|
+
const data = { out: resolve(flags.out), ...counts, applied_indexes: result.applied, input_sha256: sha256Of(html), edits_sha256: sha256Of(editsText), out_sha256: sha256Of(result.out) }
|
|
78
|
+
const rejected_reasons = result.rejected.map((entry) => rejectionLine(entry, edits[entry.index]))
|
|
79
|
+
const summary = [`fact polish-apply: ${counts.applied} applied, ${counts.rejected} rejected, ${counts.unlocated} unlocated of ${edits.length} edit(s)`, ...rejected_reasons].join("\n")
|
|
80
|
+
return { json: { command: "fact polish-apply", status: "pass", exitCode: 0, defects, data: { ...data, rejected_reasons } }, summary, exitCode: 0 }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function runPolishDiff(args) {
|
|
84
|
+
const { flags } = args
|
|
85
|
+
if (flags.before === undefined || flags.after === undefined) throw new CliError("usage: fact polish-diff --before <pre-polish.html> --after <polished.html> [--ledger F] [--out polish-defects.json] [--brief brief.md] [--session-dir D]")
|
|
86
|
+
const { refusal } = lockedBrief("fact polish-diff", flags)
|
|
87
|
+
if (refusal) return refusal
|
|
88
|
+
const after = readText(flags.after)
|
|
89
|
+
const defects = diffPolish(readText(flags.before), after, ledgerFrom(flags)).map((defect) => ({ ...defect, hint: DEFECT_CODES[defect.code]?.hint ?? null }))
|
|
90
|
+
const counts = countDefects(defects)
|
|
91
|
+
const status = polishDiffFails(defects) ? "fail" : "pass"
|
|
92
|
+
const exitCode = status === "fail" ? 1 : 0
|
|
93
|
+
const record = { command: "fact polish-diff", status, exitCode, before: resolve(flags.before), artifact: resolve(flags.after), artifact_sha256: sha256Of(after), defects, counts }
|
|
94
|
+
const session = sessionOf(flags)
|
|
95
|
+
const out = flags.out ?? (session === null ? null : join(session, POLISH_RECORD))
|
|
96
|
+
if (out !== null) writeJsonAtomic(resolve(out), record)
|
|
97
|
+
const summary = `fact polish-diff: ${status.toUpperCase()} - ${counts.blocker} blocker, ${counts.major} major, ${counts.minor} minor`
|
|
98
|
+
return { json: { ...record, out: out === null ? null : resolve(out) }, summary, exitCode }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const pairKey = (pair) => [pair.kind, pair.sentence_key, pair.claim_id ?? "", normalizeText(pair.sentence)].join("\u0000")
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The pairs whose sentence text changed since the pre-polish artifact a fill map names: a pair is
|
|
105
|
+
* kept unless the same (kind, sentence key, normalized sentence) occurs in that artifact. The map
|
|
106
|
+
* must carry the artifact's `artifact_sha256` and the bytes must still match it, so the filter is
|
|
107
|
+
* fail-closed on a hand-made or stale map.
|
|
108
|
+
* @param {object[]} pairs extractPairs output on the polished edition
|
|
109
|
+
* @param {string} mapPath the pre-polish fill map (fact fill's `<out>.fill-map.json`)
|
|
110
|
+
*/
|
|
111
|
+
export function pairsChangedSince(pairs, mapPath, ledger, claims) {
|
|
112
|
+
const map = readJsonFile(mapPath)
|
|
113
|
+
if (typeof map.artifact !== "string" || !existsSync(map.artifact)) throw new CliError(`${mapPath} names no readable pre-polish artifact`)
|
|
114
|
+
const text = readText(map.artifact)
|
|
115
|
+
if (typeof map.artifact_sha256 !== "string") throw new CliError(`${mapPath} fill map has no artifact_sha256; --changed-since needs the pre-polish hash`)
|
|
116
|
+
if (sha256Of(text) !== map.artifact_sha256) throw new CliError(`${map.artifact} changed after fill; --changed-since needs the pre-polish bytes`)
|
|
117
|
+
const html = map.format === "md" ? markdownAsHtml(text, map.entries, ledger).html : text
|
|
118
|
+
const before = new Map()
|
|
119
|
+
for (const pair of extractPairs(html, ledger, claims)) before.set(pairKey(pair), (before.get(pairKey(pair)) ?? 0) + 1)
|
|
120
|
+
return pairs.filter((pair) => {
|
|
121
|
+
const left = before.get(pairKey(pair)) ?? 0
|
|
122
|
+
if (left === 0) return true
|
|
123
|
+
before.set(pairKey(pair), left - 1)
|
|
124
|
+
return false
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** @param {string[]} argv the arguments after `fact polish-apply` or `fact polish-diff` */
|
|
129
|
+
export function runFactPolish(sub, argv) {
|
|
130
|
+
const args = parseArgs(argv)
|
|
131
|
+
return sub === "polish-apply" ? runPolishApply(args) : runPolishDiff(args)
|
|
132
|
+
}
|
|
@@ -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
|
}
|