onto-mcp 0.4.12 → 0.4.13
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/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +137 -0
- package/.onto/domains/accounting-kr/competency_qs.md +153 -153
- package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
- package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
- package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +169 -8
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
- package/.onto/processes/review/productized-live-path.md +9 -5
- package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
- package/.onto/processes/review/review-target-profile-contract.md +47 -4
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
- package/.onto/processes/shared/target-material-kind-contract.md +1 -1
- package/README.md +64 -1
- package/dist/cli.js +11 -1
- package/dist/core-api/reconstruct-api.js +575 -24
- package/dist/core-api/review-api.js +70 -5
- package/dist/core-api/review-progress.js +7 -0
- package/dist/core-api/runtime-observation.js +7 -0
- package/dist/core-runtime/artifact-io.js +75 -3
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
- package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
- package/dist/core-runtime/cli/prepare-review-session.js +34 -1
- package/dist/core-runtime/cli/render-review-final-output.js +2 -1
- package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
- package/dist/core-runtime/cli/run-seats.js +18 -0
- package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
- package/dist/core-runtime/cli/unit-resubmit.js +269 -0
- package/dist/core-runtime/cli/worker-structured-output.js +80 -2
- package/dist/core-runtime/discovery/host-detection.js +71 -22
- package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
- package/dist/core-runtime/discovery/review-cert-record.js +442 -0
- package/dist/core-runtime/discovery/review-execution-units.js +22 -0
- package/dist/core-runtime/discovery/seat-inventory.js +167 -0
- package/dist/core-runtime/discovery/settings-chain.js +311 -52
- package/dist/core-runtime/discovery/supported-models.js +282 -29
- package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
- package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
- package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
- package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
- package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
- package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
- package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
- package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
- package/dist/core-runtime/effort-calibration-ingest.js +135 -0
- package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
- package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
- package/dist/core-runtime/llm/llm-caller.js +188 -47
- package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
- package/dist/core-runtime/llm/model-switcher.js +1 -1
- package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
- package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
- package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
- package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
- package/dist/core-runtime/onboard/cli-host.js +5 -2
- package/dist/core-runtime/onboard/configure-provider.js +33 -1
- package/dist/core-runtime/onboard/host-target.js +28 -3
- package/dist/core-runtime/onboard/register.js +54 -12
- package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
- package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
- package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
- package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
- package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
- package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
- package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
- package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
- package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
- package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
- package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
- package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
- package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
- package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
- package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
- package/dist/core-runtime/reconstruct/record.js +42 -0
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
- package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
- package/dist/core-runtime/reconstruct/run.js +5198 -536
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
- package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
- package/dist/core-runtime/review/continuation-plan.js +12 -5
- package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
- package/dist/core-runtime/review/materializers.js +100 -11
- package/dist/core-runtime/review/nesting-batch.js +6 -2
- package/dist/core-runtime/review/obligation-shardability.js +138 -0
- package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
- package/dist/core-runtime/review/review-artifact-utils.js +217 -5
- package/dist/core-runtime/review/review-execution-steps.js +32 -8
- package/dist/core-runtime/review/review-progress-contract.js +8 -0
- package/dist/core-runtime/review/review-prompt-budget.js +97 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
- package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
- package/dist/core-runtime/route-identity.js +182 -0
- package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
- package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
- package/dist/core-runtime/target-material-kind.js +58 -0
- package/dist/mcp/server.js +12 -5
- package/dist/tui/app.js +167 -0
- package/dist/tui/data/event-follower.js +116 -0
- package/dist/tui/data/node-detail.js +42 -0
- package/dist/tui/data/projection-poll.js +26 -0
- package/dist/tui/data/session-discovery.js +65 -0
- package/dist/tui/index.js +108 -0
- package/dist/tui/screens/log.js +25 -0
- package/dist/tui/screens/node-detail.js +17 -0
- package/dist/tui/screens/session-selector.js +26 -0
- package/dist/tui/screens/workflow-tree.js +90 -0
- package/dist/tui/view-model/reconstruct-adapter.js +162 -0
- package/dist/tui/view-model/review-adapter.js +270 -0
- package/dist/tui/view-model/tree-view-model.js +19 -0
- package/package.json +18 -3
- package/settings.example.json +77 -16
|
@@ -0,0 +1,771 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { reduceNodeGroundHash, reduceNodeKey, } from "./comprehension-reduce.js";
|
|
3
|
+
// ── N1/N2 two-sided reconciliation ───────────────────────────────────────────
|
|
4
|
+
const VALUE_SHAPE = "value_shape";
|
|
5
|
+
/** ±1: a value-shape seam spans last_prev_format_row / first_new_format_row (2 rows); ±1 covers that
|
|
6
|
+
* span without widening it (design §13.3). Exact-row match is preferred; tolerance is the fallback. */
|
|
7
|
+
export const ANCHOR_ROW_TOLERANCE = 1;
|
|
8
|
+
const cmpStr = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
|
|
9
|
+
/** Legal enum surfaces — the honesty validator is TOTAL over these (a value outside them is malformed
|
|
10
|
+
* and fails closed; review F2 / onto issue-001/007/009/014). */
|
|
11
|
+
const VALID_ANCHOR_STATUS = new Set(["anchored", "unanchored"]);
|
|
12
|
+
const VALID_VERIFICATION = new Set([
|
|
13
|
+
"structural_location_only",
|
|
14
|
+
"unverified",
|
|
15
|
+
"adversarial_confirmed",
|
|
16
|
+
"adversarial_refuted",
|
|
17
|
+
]);
|
|
18
|
+
/** Single literal source for the adversarial-verdict vocabulary (wiring design 20260702 §15.1): BOTH
|
|
19
|
+
* the exported verdict type and the runtime allowlist derive from this tuple, so the type and the
|
|
20
|
+
* Set cannot drift apart (a separate union + Set pair stays compile-green when only one is edited;
|
|
21
|
+
* R3 W1-01 / onto issue-001·003). Exported so the drift-guard test proves the same-source derivation. */
|
|
22
|
+
export const ADVERSARIAL_RESULTS = ["adversarial_confirmed", "adversarial_refuted"];
|
|
23
|
+
const _verdictExact = [true, true];
|
|
24
|
+
void _verdictExact;
|
|
25
|
+
// ReadonlySet<union> (not Set<string>): a member added to the SET literal that is not in the tuple
|
|
26
|
+
// now fails the build too (the runtime .has() guard against JS-level bogus values is unchanged).
|
|
27
|
+
const VALID_ADVERSARIAL_RESULT = new Set(ADVERSARIAL_RESULTS);
|
|
28
|
+
/** Canonical TOTAL order over value-shape seams (full witness tuple so equal-row seams from different
|
|
29
|
+
* provenance never tie by input order; review F7 — mirrors comprehension-reduce canonicalBoundaries). */
|
|
30
|
+
function seamCmp(a, b) {
|
|
31
|
+
return (cmpStr(a.sheet, b.sheet) ||
|
|
32
|
+
a.column_index - b.column_index ||
|
|
33
|
+
a.first_new_format_row - b.first_new_format_row ||
|
|
34
|
+
a.last_prev_format_row - b.last_prev_format_row ||
|
|
35
|
+
cmpStr(a.boundary_kind, b.boundary_kind) ||
|
|
36
|
+
cmpStr(a.prev_shape, b.prev_shape) ||
|
|
37
|
+
cmpStr(a.new_shape, b.new_shape));
|
|
38
|
+
}
|
|
39
|
+
/** Two-sided deterministic reconciliation (design §13.3 / N1 / N2 / codex-F3). For a node's LLM
|
|
40
|
+
* boundaries and its Layer-1 value-shape seams, assign each boundary anchored|unanchored and record a
|
|
41
|
+
* covered|missed_by_llm status for EVERY seam. Matching is a 1:1 assignment (a seam anchors at most
|
|
42
|
+
* one boundary), exact-row first then nearest-within-tolerance, canonical-order tie-break — so dense
|
|
43
|
+
* seams cannot blanket-anchor every boundary (L2H-2) and the result is order-stable (codex-F3). */
|
|
44
|
+
export function reconcileBoundaries(raw, reduceNode) {
|
|
45
|
+
const seams = reduceNode.boundaries.filter((b) => b.boundary_kind === VALUE_SHAPE).slice().sort(seamCmp);
|
|
46
|
+
const seamMatched = seams.map(() => false);
|
|
47
|
+
const matchedSeamOf = new Map(); // raw index -> seam index
|
|
48
|
+
// Canonical LLM-boundary order (row, then character tuple, then input index) so passes are stable.
|
|
49
|
+
const order = raw
|
|
50
|
+
.map((b, i) => ({ b, i }))
|
|
51
|
+
.sort((x, y) => x.b.row - y.b.row ||
|
|
52
|
+
cmpStr(x.b.character_before, y.b.character_before) ||
|
|
53
|
+
cmpStr(x.b.character_after, y.b.character_after) ||
|
|
54
|
+
x.i - y.i);
|
|
55
|
+
// Pass 1 — exact row (LLM row === seam.first_new_format_row). 1:1, canonical order.
|
|
56
|
+
for (const { b, i } of order) {
|
|
57
|
+
for (let si = 0; si < seams.length; si += 1) {
|
|
58
|
+
const s = seams[si];
|
|
59
|
+
if (!s || seamMatched[si])
|
|
60
|
+
continue;
|
|
61
|
+
if (s.first_new_format_row === b.row) {
|
|
62
|
+
seamMatched[si] = true;
|
|
63
|
+
matchedSeamOf.set(i, si);
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Pass 2 — nearest unmatched seam within tolerance; strict-less keeps the canonically-first seam on a
|
|
69
|
+
// distance tie (seams are canonically sorted), so the outcome is order-stable.
|
|
70
|
+
for (const { b, i } of order) {
|
|
71
|
+
if (matchedSeamOf.has(i))
|
|
72
|
+
continue;
|
|
73
|
+
let bestSi = -1;
|
|
74
|
+
let bestDist = Number.POSITIVE_INFINITY;
|
|
75
|
+
for (let si = 0; si < seams.length; si += 1) {
|
|
76
|
+
const s = seams[si];
|
|
77
|
+
if (!s || seamMatched[si])
|
|
78
|
+
continue;
|
|
79
|
+
const dist = Math.abs(s.first_new_format_row - b.row);
|
|
80
|
+
if (dist <= ANCHOR_ROW_TOLERANCE && dist < bestDist) {
|
|
81
|
+
bestDist = dist;
|
|
82
|
+
bestSi = si;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (bestSi >= 0) {
|
|
86
|
+
seamMatched[bestSi] = true;
|
|
87
|
+
matchedSeamOf.set(i, bestSi);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const boundaries = raw.map((b, i) => {
|
|
91
|
+
const anchored = matchedSeamOf.has(i);
|
|
92
|
+
return {
|
|
93
|
+
row: b.row,
|
|
94
|
+
character_before: b.character_before,
|
|
95
|
+
character_after: b.character_after,
|
|
96
|
+
anchor_status: anchored ? "anchored" : "unanchored",
|
|
97
|
+
verification: anchored ? "structural_location_only" : "unverified",
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
const coverage = seams.map((s, si) => ({
|
|
101
|
+
// Reconstruct from the DECLARED fields only (round-4): a bare spread would clone extra nested own
|
|
102
|
+
// props by reference (aliasing the source boundary); the explicit projection drops them and clones.
|
|
103
|
+
boundary_ref: {
|
|
104
|
+
sheet: s.sheet,
|
|
105
|
+
column_index: s.column_index,
|
|
106
|
+
boundary_kind: s.boundary_kind,
|
|
107
|
+
prev_shape: s.prev_shape,
|
|
108
|
+
new_shape: s.new_shape,
|
|
109
|
+
last_prev_format_row: s.last_prev_format_row,
|
|
110
|
+
first_new_format_row: s.first_new_format_row,
|
|
111
|
+
},
|
|
112
|
+
status: seamMatched[si] ? "covered" : "missed_by_llm",
|
|
113
|
+
}));
|
|
114
|
+
return { boundaries, coverage };
|
|
115
|
+
}
|
|
116
|
+
// ── N4 recursive epoch contribution (allowlisted, non-circular) ──────────────
|
|
117
|
+
export const SEMANTIC_EPOCH_PREIMAGE_ALLOWLIST = [
|
|
118
|
+
"layer1_ground_hash",
|
|
119
|
+
"child_contributions",
|
|
120
|
+
"reduce_reader_model_identity",
|
|
121
|
+
"reduce_prompt_sha256",
|
|
122
|
+
"reduce_schema_tool_version",
|
|
123
|
+
"comprehension_version",
|
|
124
|
+
"over_context_gate_config_sha256",
|
|
125
|
+
"over_context_gate_logic_sha256",
|
|
126
|
+
];
|
|
127
|
+
const ALLOWLIST_SET = new Set(SEMANTIC_EPOCH_PREIMAGE_ALLOWLIST);
|
|
128
|
+
/** Fail-closed ALLOWLIST guard (design §13.4 / N7 / C8). Unlike the sibling denylist
|
|
129
|
+
* (assertGatingKeyExcludesInEpochOutput, which must enumerate every forbidden LLM field), an
|
|
130
|
+
* allowlist rejects an UNKNOWN key by construction, so a newly-added LLM output cannot silently reach
|
|
131
|
+
* the gating key. Also rejects a NESTED object smuggling a field in (codex residual): every value must
|
|
132
|
+
* be a string, except child_contributions which must be a string[]. */
|
|
133
|
+
export function assertPreImageKeysAllowlisted(pre) {
|
|
134
|
+
for (const k of Object.keys(pre)) {
|
|
135
|
+
if (!ALLOWLIST_SET.has(k)) {
|
|
136
|
+
throw new Error(`comprehension-semantic-map: epoch pre-image has non-allowlisted key '${k}' (§13.4 allowlist — a non-deterministic/LLM field could gate its own reuse). Permitted: ${SEMANTIC_EPOCH_PREIMAGE_ALLOWLIST.join(", ")}.`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
for (const k of SEMANTIC_EPOCH_PREIMAGE_ALLOWLIST) {
|
|
140
|
+
// OWN key only (review F3 / onto issue-002/005/006/…): `k in pre` accepts an INHERITED field, but
|
|
141
|
+
// the canonical spread/stableJson hashes OWN keys only — so a prototype-backed pre-image would pass
|
|
142
|
+
// validation while its required deterministic fields silently drop from the contribution key.
|
|
143
|
+
if (!Object.hasOwn(pre, k)) {
|
|
144
|
+
throw new Error(`comprehension-semantic-map: epoch pre-image missing OWN required key '${k}' (§13.4 — inherited keys are not hashed).`);
|
|
145
|
+
}
|
|
146
|
+
const v = pre[k];
|
|
147
|
+
if (k === "child_contributions") {
|
|
148
|
+
if (!Array.isArray(v) || v.some((x) => typeof x !== "string")) {
|
|
149
|
+
throw new Error("comprehension-semantic-map: child_contributions must be string[] (no nested objects — §13.4 nested-injection guard).");
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (typeof v !== "string") {
|
|
153
|
+
throw new Error(`comprehension-semantic-map: epoch pre-image field '${k}' must be a string (no nested objects — §13.4 nested-injection guard).`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function stableJson(value) {
|
|
158
|
+
if (Array.isArray(value))
|
|
159
|
+
return `[${value.map(stableJson).join(",")}]`;
|
|
160
|
+
if (value && typeof value === "object") {
|
|
161
|
+
return `{${Object.keys(value)
|
|
162
|
+
.sort()
|
|
163
|
+
.map((k) => `${JSON.stringify(k)}:${stableJson(value[k])}`)
|
|
164
|
+
.join(",")}}`;
|
|
165
|
+
}
|
|
166
|
+
return JSON.stringify(value) ?? "null";
|
|
167
|
+
}
|
|
168
|
+
/** The recursive per-node epoch contribution (= subtree_epoch_contribution). NON-CIRCULAR by
|
|
169
|
+
* construction (allowlist admits only deterministic fields). Folding child_contributions makes a
|
|
170
|
+
* parent re-derive on ANY descendant structural change EVEN WHEN the parent's own reduceNodeGroundHash
|
|
171
|
+
* is byte-identical (e.g. a non-lowest child's limiting_witness change — comprehension-reduce.ts keeps
|
|
172
|
+
* only witnessCandidates[0], and parent edges are first/last only — so the parent ground is stable
|
|
173
|
+
* while a child's ground hash rotates; codex tsx probe confirmed cardinality > 0). child_contributions
|
|
174
|
+
* is sorted so SIBLING ORDER within a grouping does not perturb the key. NOTE (review F4): unlike the
|
|
175
|
+
* Layer-1 GROUND (which is fanin/grouping-INVARIANT), this contribution is TOPOLOGY-DEPENDENT by
|
|
176
|
+
* design — a node synthesizes ITS children, so a different fan-in gives genuinely different judgments,
|
|
177
|
+
* and the contribution correctly rotates with it (a fan-in change is a re-derive, caught via
|
|
178
|
+
* child_contributions, never silent). Production pins the topology with the deterministic over-context
|
|
179
|
+
* frontier, whose config + logic sha are folded into the pre-image. */
|
|
180
|
+
export function reduceNodeEpochContribution(pre) {
|
|
181
|
+
assertPreImageKeysAllowlisted(pre);
|
|
182
|
+
const canonical = { ...pre, child_contributions: [...pre.child_contributions].sort() };
|
|
183
|
+
return createHash("sha256").update(stableJson(canonical)).digest("hex");
|
|
184
|
+
}
|
|
185
|
+
// ── N3/N5/N6 fail-closed validators ──────────────────────────────────────────
|
|
186
|
+
/** N5 / codex-F1: child-judgment completeness, subsumed-aware. A `subsumed` node (below the
|
|
187
|
+
* accumulation frontier) has no synthesized judgment, so it MUST carry consumed=[]. An accumulating
|
|
188
|
+
* node's consumed_child_judgment_keys MUST equal the frontier-direct child set the caller computed.
|
|
189
|
+
* Symmetric with comprehension-reduce assertContiguousChildren. */
|
|
190
|
+
export function assertChildJudgmentCoverage(node, expectedConsumedKeys) {
|
|
191
|
+
if (node.reduce_read_attempt === "subsumed") {
|
|
192
|
+
if (node.consumed_child_judgment_keys.length > 0) {
|
|
193
|
+
throw new Error(`comprehension-semantic-map: subsumed node ${reduceNodeKey(node.node_ref)} must consume no child judgments, has ${node.consumed_child_judgment_keys.length} (§13.5 codex-F1).`);
|
|
194
|
+
}
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
// Reject duplicate consumed keys BEFORE the set comparison (review F8 / onto issue-004): a Set
|
|
198
|
+
// collapses ["k1","k1"] to {k1}, so a node consuming the same child twice would falsely satisfy
|
|
199
|
+
// coverage against {k1}. A node consumes each child at most once.
|
|
200
|
+
if (new Set(node.consumed_child_judgment_keys).size !== node.consumed_child_judgment_keys.length) {
|
|
201
|
+
throw new Error(`comprehension-semantic-map: duplicate consumed child-judgment key at ${reduceNodeKey(node.node_ref)} (§13.5 N5 — each child is consumed once).`);
|
|
202
|
+
}
|
|
203
|
+
const have = new Set(node.consumed_child_judgment_keys);
|
|
204
|
+
const want = new Set(expectedConsumedKeys);
|
|
205
|
+
const equal = have.size === want.size && [...want].every((k) => have.has(k));
|
|
206
|
+
if (!equal) {
|
|
207
|
+
throw new Error(`comprehension-semantic-map: child-judgment coverage mismatch at ${reduceNodeKey(node.node_ref)} — consumed {${[...have].sort().join(",")}} != expected {${[...want].sort().join(",")}} (§13.5 N5 completeness).`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/** N3 / L2H-5 / codex-F2: the fail-closed verification state machine. Legal (anchor_status,
|
|
211
|
+
* verification) combinations, plus the seed-projection rule. `seedBound` = these boundaries are about
|
|
212
|
+
* to flow to the seed authoring input. Throws on the first illegal state. */
|
|
213
|
+
export function assertSemanticBoundaryHonesty(node, seedBound) {
|
|
214
|
+
const at = (b) => `${reduceNodeKey(node.node_ref)}@row${b.row}`;
|
|
215
|
+
for (const b of node.semantic_boundaries) {
|
|
216
|
+
// TOTAL over the enum surface (review F2 / onto issue-001/007/009/014): an anchor_status or
|
|
217
|
+
// verification value OUTSIDE the known set is malformed and fails closed — otherwise a bogus
|
|
218
|
+
// caller-injected verifier output (e.g. "bogus_status") would slip through the combo checks below.
|
|
219
|
+
if (!VALID_ANCHOR_STATUS.has(b.anchor_status)) {
|
|
220
|
+
throw new Error(`comprehension-semantic-map: ${at(b)} unknown anchor_status '${b.anchor_status}' (§13.5 fail-closed enum).`);
|
|
221
|
+
}
|
|
222
|
+
if (!VALID_VERIFICATION.has(b.verification)) {
|
|
223
|
+
throw new Error(`comprehension-semantic-map: ${at(b)} unknown verification '${b.verification}' (§13.5 fail-closed enum).`);
|
|
224
|
+
}
|
|
225
|
+
if (b.anchor_status === "anchored" && b.verification !== "structural_location_only") {
|
|
226
|
+
throw new Error(`comprehension-semantic-map: ${at(b)} anchored boundary must be 'structural_location_only' (anchored corroborates LOCATION only, never content; got '${b.verification}') — §13.5 codex-F2.`);
|
|
227
|
+
}
|
|
228
|
+
if (b.anchor_status === "unanchored" && b.verification === "structural_location_only") {
|
|
229
|
+
throw new Error(`comprehension-semantic-map: ${at(b)} unanchored boundary cannot be 'structural_location_only' (no seam corroborates it) — §13.5 codex-F2.`);
|
|
230
|
+
}
|
|
231
|
+
if (seedBound && b.anchor_status === "unanchored" && b.verification === "unverified") {
|
|
232
|
+
throw new Error(`comprehension-semantic-map: ${at(b)} seed-bound unanchored boundary must be adversarially processed before seed (N3 all-unanchored) — §13.5.`);
|
|
233
|
+
}
|
|
234
|
+
if (seedBound && b.verification === "adversarial_refuted") {
|
|
235
|
+
throw new Error(`comprehension-semantic-map: ${at(b)} adversarial_refuted boundary must be EXCLUDED from seed boundaries (recorded as a refuted disclosure + counted in taint), not carried — §13.5 codex-F2.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
/** This node's OWN unverified taint: unanchored boundaries not adversarially-confirmed (unverified or
|
|
240
|
+
* refuted) + a failed/unread read outcome. */
|
|
241
|
+
export function ownUnverifiedCount(node) {
|
|
242
|
+
const boundaryUnverified = node.semantic_boundaries.filter((b) => b.anchor_status === "unanchored" && b.verification !== "adversarial_confirmed").length;
|
|
243
|
+
const attemptUnverified = node.reduce_read_attempt === "failed" || node.reduce_read_attempt === "unread" ? 1 : 0;
|
|
244
|
+
return boundaryUnverified + attemptUnverified;
|
|
245
|
+
}
|
|
246
|
+
/** The canonical taint census value a node SHOULD carry (own + children). Callers set
|
|
247
|
+
* unanchored_unverified_count from this; the validator enforces the fail-closed direction. */
|
|
248
|
+
export function computeUnanchoredUnverifiedCount(node, children) {
|
|
249
|
+
return children.reduce((acc, c) => acc + c.unanchored_unverified_count, 0) + ownUnverifiedCount(node);
|
|
250
|
+
}
|
|
251
|
+
/** N6: taint monotone. A parent may never UNDERSTATE accumulated unverified taint (fail-closed,
|
|
252
|
+
* symmetric with assertHonestyFold). Over-reporting is allowed; understating throws. */
|
|
253
|
+
function assertSafeCount(label, n) {
|
|
254
|
+
// A non-finite / negative / non-integer count would defeat the monotone comparison (review F6: NaN <
|
|
255
|
+
// x is false, so a NaN taint would pass silently). Require a safe non-negative integer.
|
|
256
|
+
if (!Number.isSafeInteger(n) || n < 0) {
|
|
257
|
+
throw new Error(`comprehension-semantic-map: ${label} must be a non-negative safe integer, got ${n} (§13.5 N6 fail-closed).`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
export function assertTaintCensusMonotone(node, children) {
|
|
261
|
+
assertSafeCount(`taint count at ${reduceNodeKey(node.node_ref)}`, node.unanchored_unverified_count);
|
|
262
|
+
for (const c of children)
|
|
263
|
+
assertSafeCount(`child taint count at ${reduceNodeKey(c.node_ref)}`, c.unanchored_unverified_count);
|
|
264
|
+
const expectedMin = computeUnanchoredUnverifiedCount(node, children);
|
|
265
|
+
if (node.unanchored_unverified_count < expectedMin) {
|
|
266
|
+
throw new Error(`comprehension-semantic-map: taint census understated at ${reduceNodeKey(node.node_ref)} — ${node.unanchored_unverified_count} < ${expectedMin} (parent may not understate children + own unverified; §13.5 N6).`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const VALUE_SHAPE_KIND = "value_shape";
|
|
270
|
+
/** Deterministic subtree LEAF count per node (the single over-context metric — NOT row-span or ground
|
|
271
|
+
* bytes; §13.6 codex-F5). Throws on a cyclic trace. */
|
|
272
|
+
export function computeSubtreeLeafCounts(trace) {
|
|
273
|
+
const counts = new Map();
|
|
274
|
+
const compute = (key, visiting) => {
|
|
275
|
+
const cached = counts.get(key);
|
|
276
|
+
if (cached !== undefined)
|
|
277
|
+
return cached;
|
|
278
|
+
if (visiting.has(key))
|
|
279
|
+
throw new Error(`comprehension-semantic-map: cycle at ${key} while counting leaves (§13.6).`);
|
|
280
|
+
const tnode = trace.nodes.get(key);
|
|
281
|
+
if (!tnode)
|
|
282
|
+
throw new Error(`comprehension-semantic-map: trace node missing for ${key} (leaf count).`);
|
|
283
|
+
visiting.add(key);
|
|
284
|
+
const n = tnode.child_keys.length === 0 ? 1 : tnode.child_keys.reduce((s, c) => s + compute(c, visiting), 0);
|
|
285
|
+
visiting.delete(key);
|
|
286
|
+
counts.set(key, n);
|
|
287
|
+
return n;
|
|
288
|
+
};
|
|
289
|
+
for (const key of trace.nodes.keys())
|
|
290
|
+
compute(key, new Set());
|
|
291
|
+
return counts;
|
|
292
|
+
}
|
|
293
|
+
/** Tautological over-context gate LOGIC digest (design §13.4 L2R-2 / ultracode audit F — mirrors
|
|
294
|
+
* leaf-reader structureLeafTriggerLogicSha256): hashes the SOURCE of the frontier predicate and its
|
|
295
|
+
* metric, so editing either rotates every semantic-map fingerprint without a hand-bumped knob. */
|
|
296
|
+
export function semanticMapGateLogicSha256() {
|
|
297
|
+
return createHash("sha256")
|
|
298
|
+
.update(classifyFrontier.toString())
|
|
299
|
+
.update(" ")
|
|
300
|
+
.update(computeSubtreeLeafCounts.toString())
|
|
301
|
+
.digest("hex");
|
|
302
|
+
}
|
|
303
|
+
/** Classify every REACHABLE node (from root) into its frontier role, top-down (§13.6). A leaf can never
|
|
304
|
+
* accumulate (no children), so it is a frontier (or subsumed under one). Deterministic. */
|
|
305
|
+
export function classifyFrontier(trace, overContextBudget) {
|
|
306
|
+
// A leaf-count budget must be a real non-negative integer (review F4 / onto issue-001/005/007/…):
|
|
307
|
+
// NaN/Infinity would silently collapse the frontier to the root instead of failing closed.
|
|
308
|
+
if (!Number.isSafeInteger(overContextBudget) || overContextBudget < 0) {
|
|
309
|
+
throw new Error(`comprehension-semantic-map: overContextBudget must be a non-negative safe integer (leaf count), got ${overContextBudget} (§13.6 fail-closed).`);
|
|
310
|
+
}
|
|
311
|
+
const leafCounts = computeSubtreeLeafCounts(trace);
|
|
312
|
+
const mode = new Map();
|
|
313
|
+
const shouldAccumulate = (key) => {
|
|
314
|
+
const tnode = trace.nodes.get(key);
|
|
315
|
+
if (!tnode || tnode.child_keys.length === 0)
|
|
316
|
+
return false; // a leaf never accumulates.
|
|
317
|
+
const count = leafCounts.get(key); // every node was counted above; a miss is an impossible state.
|
|
318
|
+
if (count === undefined)
|
|
319
|
+
throw new Error(`comprehension-semantic-map: no leaf count for ${key} (impossible; §13.6).`);
|
|
320
|
+
return count > overContextBudget;
|
|
321
|
+
};
|
|
322
|
+
const classify = (key, underFrontier) => {
|
|
323
|
+
const tnode = trace.nodes.get(key);
|
|
324
|
+
if (!tnode)
|
|
325
|
+
throw new Error(`comprehension-semantic-map: trace node missing for ${key} (classify).`);
|
|
326
|
+
const m = underFrontier ? "subsumed" : shouldAccumulate(key) ? "accumulating" : "frontier";
|
|
327
|
+
mode.set(key, m);
|
|
328
|
+
const childUnder = m !== "accumulating"; // frontier/subsumed → descendants subsumed.
|
|
329
|
+
for (const c of tnode.child_keys)
|
|
330
|
+
classify(c, childUnder);
|
|
331
|
+
};
|
|
332
|
+
classify(trace.root_key, false);
|
|
333
|
+
return mode;
|
|
334
|
+
}
|
|
335
|
+
/** Fail-closed: the trace MUST be a rooted TREE (review F2 / onto issue-002/004). A DAG / diamond /
|
|
336
|
+
* duplicate child edge would double-count leaves, classify a shared node order-dependently, or let one
|
|
337
|
+
* semantic node be reused through multiple parents. Checks: every child key exists; no duplicate child
|
|
338
|
+
* edge within a node; every node has indegree ≤ 1; the root has indegree 0; every node is reachable
|
|
339
|
+
* from the root (no orphans; no cycle, since a cycle leaves a node either unreachable or indegree > 1). */
|
|
340
|
+
export function assertReduceTopologyIsTree(trace) {
|
|
341
|
+
// The declared root MUST name a real node (round-2: a missing root_key could pass when the node count
|
|
342
|
+
// coincidentally matched the empty reached set).
|
|
343
|
+
if (!trace.nodes.has(trace.root_key)) {
|
|
344
|
+
throw new Error(`comprehension-semantic-map: root_key '${trace.root_key}' is not in the trace (§13.6 tree).`);
|
|
345
|
+
}
|
|
346
|
+
const indegree = new Map();
|
|
347
|
+
for (const [key, tnode] of trace.nodes) {
|
|
348
|
+
// Each map key MUST equal reduceNodeKey(node_ref) (round-2: an alias key would let
|
|
349
|
+
// consumed_child_judgment_keys diverge from the canonical child-summary keys).
|
|
350
|
+
if (key !== reduceNodeKey(tnode.node_ref)) {
|
|
351
|
+
throw new Error(`comprehension-semantic-map: trace key '${key}' != reduceNodeKey(node_ref) '${reduceNodeKey(tnode.node_ref)}' (§13.6 tree — keys must be canonical).`);
|
|
352
|
+
}
|
|
353
|
+
const seenChild = new Set();
|
|
354
|
+
for (const c of tnode.child_keys) {
|
|
355
|
+
if (!trace.nodes.has(c))
|
|
356
|
+
throw new Error(`comprehension-semantic-map: child '${c}' of '${key}' is not in the trace (§13.6 tree).`);
|
|
357
|
+
if (seenChild.has(c))
|
|
358
|
+
throw new Error(`comprehension-semantic-map: duplicate child edge '${c}' under '${key}' (§13.6 tree).`);
|
|
359
|
+
seenChild.add(c);
|
|
360
|
+
const d = (indegree.get(c) ?? 0) + 1;
|
|
361
|
+
indegree.set(c, d);
|
|
362
|
+
if (d > 1)
|
|
363
|
+
throw new Error(`comprehension-semantic-map: node '${c}' has multiple parents — trace is a DAG, not a tree (§13.6).`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if ((indegree.get(trace.root_key) ?? 0) !== 0) {
|
|
367
|
+
throw new Error(`comprehension-semantic-map: root '${trace.root_key}' has a parent (§13.6 tree).`);
|
|
368
|
+
}
|
|
369
|
+
const reached = new Set();
|
|
370
|
+
const stack = [trace.root_key];
|
|
371
|
+
while (stack.length > 0) {
|
|
372
|
+
const k = stack.pop();
|
|
373
|
+
if (k === undefined || reached.has(k))
|
|
374
|
+
continue;
|
|
375
|
+
reached.add(k);
|
|
376
|
+
for (const c of trace.nodes.get(k)?.child_keys ?? [])
|
|
377
|
+
stack.push(c);
|
|
378
|
+
}
|
|
379
|
+
if (reached.size !== trace.nodes.size) {
|
|
380
|
+
throw new Error(`comprehension-semantic-map: ${trace.nodes.size - reached.size} trace node(s) unreachable from root '${trace.root_key}' (§13.6 tree).`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
/** Fail-closed: a SUBSUMED node carries no judgment (its frontier ancestor absorbed it) and no taint
|
|
384
|
+
* (review onto issue-009: a subsumed node has no unverified judgment to count). */
|
|
385
|
+
export function assertSubsumedNodeEmpty(node) {
|
|
386
|
+
if (node.reduce_read_attempt !== "subsumed")
|
|
387
|
+
return;
|
|
388
|
+
if (node.semantic_summary !== "" ||
|
|
389
|
+
node.semantic_boundaries.length > 0 ||
|
|
390
|
+
node.structure_boundary_coverage.length > 0 ||
|
|
391
|
+
node.unanchored_unverified_count !== 0) {
|
|
392
|
+
throw new Error(`comprehension-semantic-map: subsumed node ${reduceNodeKey(node.node_ref)} must carry no judgment or taint (§13.6 — its frontier ancestor's read covers it).`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
/** SOURCE-SAFETY guard (§13.6 / C6): the synthesis input must carry only bounded deterministic facts +
|
|
396
|
+
* child summary prose — never a raw cell value / formatCode / example. By construction the builder
|
|
397
|
+
* only pulls safe fields; this asserts the shape so an accidental enrichment fails closed. */
|
|
398
|
+
const SYNTHESIS_INPUT_KEYS = ["node_ref", "format_clusters", "value_shape_seams", "child_summaries"];
|
|
399
|
+
const SEAM_KEYS = ["row", "prev_shape", "new_shape"];
|
|
400
|
+
const CHILD_SUMMARY_KEYS = ["key", "summary"];
|
|
401
|
+
const REGION_KEYS = ["sheet", "column_index", "row_start", "row_end"];
|
|
402
|
+
const SYNTHESIS_OUTPUT_KEYS = ["semantic_summary", "boundaries"];
|
|
403
|
+
const RAW_BOUNDARY_KEYS = ["row", "character_before", "character_after"];
|
|
404
|
+
function assertExactKeys(label, obj, keys) {
|
|
405
|
+
if (!obj || typeof obj !== "object" || Array.isArray(obj)) {
|
|
406
|
+
throw new Error(`comprehension-semantic-map: ${label} must be a plain object (§13.6 source-safe envelope).`);
|
|
407
|
+
}
|
|
408
|
+
const own = Object.keys(obj);
|
|
409
|
+
const want = new Set(keys);
|
|
410
|
+
for (const k of own) {
|
|
411
|
+
if (!want.has(k)) {
|
|
412
|
+
throw new Error(`comprehension-semantic-map: ${label} has unexpected field '${k}' — only ${keys.join(", ")} allowed (§13.6 source-safe envelope; an extra field could smuggle a raw value into synthesis).`);
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
for (const k of keys) {
|
|
416
|
+
if (!Object.hasOwn(obj, k)) {
|
|
417
|
+
throw new Error(`comprehension-semantic-map: ${label} missing required field '${k}' (§13.6).`);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
/** SOURCE-SAFETY guard (§13.6 / C6 / review F1 / onto issue-012/016) — an EXACT own-key schema, not a
|
|
422
|
+
* partial type spot-check. The input may carry ONLY the bounded deterministic facts + child summary
|
|
423
|
+
* prose; an extra field on the input, a non-string format cluster, a seam with extra keys, or a raw
|
|
424
|
+
* child-summary value fails closed — so an accidental "enrichment" that smuggles a raw cell value /
|
|
425
|
+
* formatCode / example into the synthesis prompt cannot pass. */
|
|
426
|
+
export function assertSynthesisInputBounded(input) {
|
|
427
|
+
assertExactKeys("synthesis input", input, SYNTHESIS_INPUT_KEYS);
|
|
428
|
+
assertExactKeys("synthesis input.node_ref", input.node_ref, REGION_KEYS);
|
|
429
|
+
if (!Array.isArray(input.format_clusters) || input.format_clusters.some((c) => typeof c !== "string")) {
|
|
430
|
+
throw new Error("comprehension-semantic-map: synthesis format_clusters must be string[] — no raw values (§13.6).");
|
|
431
|
+
}
|
|
432
|
+
if (!Array.isArray(input.value_shape_seams)) {
|
|
433
|
+
throw new Error("comprehension-semantic-map: synthesis value_shape_seams must be an array (§13.6).");
|
|
434
|
+
}
|
|
435
|
+
for (const s of input.value_shape_seams) {
|
|
436
|
+
assertExactKeys("synthesis seam", s, SEAM_KEYS);
|
|
437
|
+
if (typeof s.row !== "number" || typeof s.prev_shape !== "string" || typeof s.new_shape !== "string") {
|
|
438
|
+
throw new Error("comprehension-semantic-map: synthesis seam must be {row:number, prev_shape:string, new_shape:string} — no raw values (§13.6).");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (!Array.isArray(input.child_summaries)) {
|
|
442
|
+
throw new Error("comprehension-semantic-map: synthesis child_summaries must be an array (§13.6).");
|
|
443
|
+
}
|
|
444
|
+
for (const c of input.child_summaries) {
|
|
445
|
+
assertExactKeys("synthesis child summary", c, CHILD_SUMMARY_KEYS);
|
|
446
|
+
if (typeof c.key !== "string" || typeof c.summary !== "string") {
|
|
447
|
+
throw new Error("comprehension-semantic-map: synthesis child summary must be {key:string, summary:string} (§13.6 source-safe envelope).");
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
/** Fail-closed on the caller-injected synthesize's OUTPUT (round-2 review: the INPUT was validated but
|
|
452
|
+
* the OUTPUT was not, so a malformed boundary — a string row that coerces in Math.abs, an object
|
|
453
|
+
* character field — flowed through reconcile and anchored). Exact own-key schema; a boundary row must
|
|
454
|
+
* be a safe integer and its character fields strings. */
|
|
455
|
+
export function assertSynthesisOutputBounded(out) {
|
|
456
|
+
assertExactKeys("synthesis output", out, SYNTHESIS_OUTPUT_KEYS);
|
|
457
|
+
if (typeof out.semantic_summary !== "string") {
|
|
458
|
+
throw new Error("comprehension-semantic-map: synthesis output semantic_summary must be a string (§13.5 fail-closed).");
|
|
459
|
+
}
|
|
460
|
+
if (!Array.isArray(out.boundaries)) {
|
|
461
|
+
throw new Error("comprehension-semantic-map: synthesis output boundaries must be an array (§13.5 fail-closed).");
|
|
462
|
+
}
|
|
463
|
+
for (const b of out.boundaries) {
|
|
464
|
+
assertExactKeys("synthesis output boundary", b, RAW_BOUNDARY_KEYS);
|
|
465
|
+
if (!Number.isSafeInteger(b.row)) {
|
|
466
|
+
throw new Error(`comprehension-semantic-map: synthesis output boundary row must be a safe integer, got ${JSON.stringify(b.row)} (§13.5 fail-closed).`);
|
|
467
|
+
}
|
|
468
|
+
if (typeof b.character_before !== "string" || typeof b.character_after !== "string") {
|
|
469
|
+
throw new Error("comprehension-semantic-map: synthesis output boundary character fields must be strings (§13.5 fail-closed).");
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
/** Canonical value-shape seams for the synthesis input (round-4 / onto issue-001): the ground hash
|
|
474
|
+
* canonicalizes (sorts + dedups) format_clusters/boundaries, but the synthesis input consumed RAW
|
|
475
|
+
* order, so a ground-equivalent but raw-divergent reduce node could change the LLM input. Deriving the
|
|
476
|
+
* synthesis facts canonically makes the input a pure function of the ground identity. For a real
|
|
477
|
+
* reduceColumnLeavesWithTrace node (already canonical) this is a no-op. */
|
|
478
|
+
function canonicalValueShapeSeams(boundaries) {
|
|
479
|
+
const seen = new Set();
|
|
480
|
+
const out = [];
|
|
481
|
+
for (const b of boundaries) {
|
|
482
|
+
if (b.boundary_kind !== VALUE_SHAPE_KIND)
|
|
483
|
+
continue;
|
|
484
|
+
const key = `${b.first_new_format_row}|${b.prev_shape}|${b.new_shape}`;
|
|
485
|
+
if (seen.has(key))
|
|
486
|
+
continue;
|
|
487
|
+
seen.add(key);
|
|
488
|
+
out.push({ row: b.first_new_format_row, prev_shape: b.prev_shape, new_shape: b.new_shape });
|
|
489
|
+
}
|
|
490
|
+
out.sort((x, y) => x.row - y.row || cmpStr(x.prev_shape, y.prev_shape) || cmpStr(x.new_shape, y.new_shape));
|
|
491
|
+
return out;
|
|
492
|
+
}
|
|
493
|
+
/** SINGLE-SOURCE synthesis-input builder (W2 §3(a) / X2): the module's internal walk AND the stage
|
|
494
|
+
* bridge both construct the LLM-facing input through this function, so the input the LLM actually
|
|
495
|
+
* saw cannot drift from the input the module validates (the bridge additionally compares stableJson
|
|
496
|
+
* of both — §3(b)). Child summaries are MODULE-owned outputs (a produced child's semantic_summary),
|
|
497
|
+
* unbuildable from topology alone (X2: codex-F2 ≡ onto-004) — the caller supplies them via
|
|
498
|
+
* `childSummaryByKey` (available bottom-up). Fail-closed on an unknown/subsumed key or a missing
|
|
499
|
+
* consumed-child summary. The returned node_ref is a clone (review F3 — a caller-injected
|
|
500
|
+
* synthesize cannot mutate the trace). */
|
|
501
|
+
export function buildSynthesisInputForNode(trace, nodesByKey, modes, key, childSummaryByKey) {
|
|
502
|
+
const tnode = trace.nodes.get(key);
|
|
503
|
+
const reduceNode = nodesByKey.get(key);
|
|
504
|
+
if (!tnode || !reduceNode) {
|
|
505
|
+
throw new Error(`comprehension-semantic-map: trace/node missing for key ${key} (synthesis input).`);
|
|
506
|
+
}
|
|
507
|
+
const mode = modes.get(key);
|
|
508
|
+
if (!mode) {
|
|
509
|
+
throw new Error(`comprehension-semantic-map: no frontier mode for ${key} (synthesis input).`);
|
|
510
|
+
}
|
|
511
|
+
if (mode === "subsumed") {
|
|
512
|
+
throw new Error(`comprehension-semantic-map: subsumed node ${key} takes no synthesis input (§13.6 — its frontier ancestor's read covers it).`);
|
|
513
|
+
}
|
|
514
|
+
const r = tnode.node_ref;
|
|
515
|
+
const isFrontier = mode === "frontier";
|
|
516
|
+
const consumedChildKeys = tnode.child_keys.filter((k) => modes.get(k) !== "subsumed");
|
|
517
|
+
return {
|
|
518
|
+
node_ref: { sheet: r.sheet, column_index: r.column_index, row_start: r.row_start, row_end: r.row_end },
|
|
519
|
+
format_clusters: [...reduceNode.format_clusters].sort(), // canonical (round-4): input = fn(ground identity), not raw order.
|
|
520
|
+
value_shape_seams: canonicalValueShapeSeams(reduceNode.boundaries),
|
|
521
|
+
child_summaries: isFrontier
|
|
522
|
+
? []
|
|
523
|
+
: consumedChildKeys.map((k) => {
|
|
524
|
+
const summary = childSummaryByKey.get(k);
|
|
525
|
+
if (summary === undefined) {
|
|
526
|
+
throw new Error(`comprehension-semantic-map: missing consumed-child summary for ${k} (synthesis input — children must be produced bottom-up first).`);
|
|
527
|
+
}
|
|
528
|
+
return { key: k, summary };
|
|
529
|
+
}),
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
/** Walk the trace bottom-up, producing one validated ComprehensionSemanticNode per skeleton node. Each
|
|
533
|
+
* node: synthesize (caller LLM) → reconcileBoundaries (deterministic anchor/coverage) → verify EVERY
|
|
534
|
+
* unanchored boundary (N3) → recursive epoch contribution → taint census → assemble, with all three
|
|
535
|
+
* fail-closed validators enforced. Deterministic given a deterministic synthesize/verify (the mock). */
|
|
536
|
+
export function accumulateSemanticMap(trace, nodesByKey, opts) {
|
|
537
|
+
const result = new Map();
|
|
538
|
+
const seedBound = opts.seedBound ?? false;
|
|
539
|
+
const visiting = new Set(); // cycle detection (review F5 / onto issue-003).
|
|
540
|
+
assertReduceTopologyIsTree(trace); // reject DAG / diamond / duplicate edges before counting (review F2).
|
|
541
|
+
const modes = classifyFrontier(trace, opts.overContextBudget); // S3 over-context partition (§13.6).
|
|
542
|
+
// Round-4: validate the config sha is a string BEFORE the template fold (a non-string would coerce to
|
|
543
|
+
// "[object Object]" and silently pass; the folded value is later allowlist-checked but by then the
|
|
544
|
+
// original is lost).
|
|
545
|
+
if (typeof opts.preImageBase.over_context_gate_config_sha256 !== "string") {
|
|
546
|
+
throw new Error("comprehension-semantic-map: preImageBase.over_context_gate_config_sha256 must be a string (§13.4 fail-closed).");
|
|
547
|
+
}
|
|
548
|
+
// Round-3 F2: the RUNTIME owns the budget→epoch binding (do not trust the caller to fold the budget
|
|
549
|
+
// into over_context_gate_config_sha256). A budget change reshapes the frontier partition and thus the
|
|
550
|
+
// judgments, so fold the actual budget into the config sha here — a budget change always rotates the key.
|
|
551
|
+
const preImageBase = {
|
|
552
|
+
...opts.preImageBase,
|
|
553
|
+
over_context_gate_config_sha256: createHash("sha256")
|
|
554
|
+
.update(`${opts.preImageBase.over_context_gate_config_sha256}|over_context_budget=${opts.overContextBudget}`)
|
|
555
|
+
.digest("hex"),
|
|
556
|
+
};
|
|
557
|
+
const visit = (key) => {
|
|
558
|
+
const cached = result.get(key);
|
|
559
|
+
if (cached)
|
|
560
|
+
return cached;
|
|
561
|
+
if (visiting.has(key)) {
|
|
562
|
+
throw new Error(`comprehension-semantic-map: cycle in reduce topology at ${key} — a well-formed trace is a tree (§13.5 fail-closed; review F5).`);
|
|
563
|
+
}
|
|
564
|
+
const tnode = trace.nodes.get(key);
|
|
565
|
+
const reduceNode = nodesByKey.get(key);
|
|
566
|
+
if (!tnode || !reduceNode) {
|
|
567
|
+
throw new Error(`comprehension-semantic-map: trace/node missing for key ${key} (accumulate walk).`);
|
|
568
|
+
}
|
|
569
|
+
// Round-3 F1: nodesByKey MUST agree with the validated trace — a caller could pass a mismatched
|
|
570
|
+
// reduce node whose deterministic facts then feed synthesis/reconcile. reduceNodeGroundHash is the
|
|
571
|
+
// node's full canonical identity (region + sorted clusters + canonical boundaries + honesty), and
|
|
572
|
+
// the synthesis input is derived canonically (canonicalValueShapeSeams), so a ground-equivalent node
|
|
573
|
+
// yields an identical input. NOTE (round-4, deferred): a FULL structural re-derivation — proving each
|
|
574
|
+
// parent's ground == merge(children's reduce nodes) — is not re-run here (it would re-execute the
|
|
575
|
+
// Layer-1 fold per node). The module trusts the trace/nodesByKey to be produced by
|
|
576
|
+
// reduceColumnLeavesWithTrace (which guarantees that by construction); production wiring provides it.
|
|
577
|
+
if (reduceNodeKey(reduceNode.region) !== key || reduceNodeGroundHash(reduceNode) !== tnode.ground_hash) {
|
|
578
|
+
throw new Error(`comprehension-semantic-map: nodesByKey['${key}'] disagrees with the trace node (region/ground mismatch) (§13.6 fail-closed).`);
|
|
579
|
+
}
|
|
580
|
+
const mode = modes.get(key);
|
|
581
|
+
if (!mode)
|
|
582
|
+
throw new Error(`comprehension-semantic-map: no frontier mode for ${key} (unreachable from root?).`);
|
|
583
|
+
visiting.add(key);
|
|
584
|
+
const children = tnode.child_keys.map(visit); // recurse ALWAYS — a subsumed subtree still gets 1:1 placeholder nodes.
|
|
585
|
+
visiting.delete(key);
|
|
586
|
+
// Children whose judgment this node CONSUMES = the non-subsumed direct children. For an accumulating
|
|
587
|
+
// node that is all its children; for a frontier node that is none (children are subsumed).
|
|
588
|
+
const consumedChildKeys = tnode.child_keys.filter((k) => modes.get(k) !== "subsumed");
|
|
589
|
+
const consumedChildren = children.filter((c) => modes.get(reduceNodeKey(c.node_ref)) !== "subsumed");
|
|
590
|
+
// SUBSUMED: no judgment — a 1:1 placeholder (its frontier ancestor's flat read covers it). Its
|
|
591
|
+
// epoch contribution STILL folds its children's contributions (review F1 / onto issue-003/006/011):
|
|
592
|
+
// the epoch recursion is decoupled from judgment consumption, so a non-propagating descendant change
|
|
593
|
+
// (that leaves this node's ground byte-identical) still rotates the key up to the frontier ancestor.
|
|
594
|
+
if (mode === "subsumed") {
|
|
595
|
+
const preImage = {
|
|
596
|
+
...preImageBase,
|
|
597
|
+
layer1_ground_hash: tnode.ground_hash,
|
|
598
|
+
child_contributions: children.map((c) => c.subtree_epoch_contribution),
|
|
599
|
+
};
|
|
600
|
+
const rr = tnode.node_ref;
|
|
601
|
+
const node = {
|
|
602
|
+
node_ref: { sheet: rr.sheet, column_index: rr.column_index, row_start: rr.row_start, row_end: rr.row_end },
|
|
603
|
+
layer1_ground_hash: tnode.ground_hash,
|
|
604
|
+
subtree_epoch_contribution: reduceNodeEpochContribution(preImage),
|
|
605
|
+
authority: "non_authoritative",
|
|
606
|
+
provisional: true,
|
|
607
|
+
reduce_read_attempt: "subsumed",
|
|
608
|
+
semantic_summary: "",
|
|
609
|
+
semantic_boundaries: [],
|
|
610
|
+
structure_boundary_coverage: [],
|
|
611
|
+
topology_child_keys: [...tnode.child_keys], // clone so the returned node does not alias the trace array (round-3 F3).
|
|
612
|
+
consumed_child_judgment_keys: [],
|
|
613
|
+
unanchored_unverified_count: 0,
|
|
614
|
+
};
|
|
615
|
+
assertSubsumedNodeEmpty(node);
|
|
616
|
+
assertChildJudgmentCoverage(node, []);
|
|
617
|
+
result.set(key, node);
|
|
618
|
+
return node;
|
|
619
|
+
}
|
|
620
|
+
// FRONTIER = one flat read over the whole subtree (child_summaries omitted; children are subsumed).
|
|
621
|
+
// ACCUMULATING = synthesize the (non-subsumed) children's judgments. Both are 'produced'.
|
|
622
|
+
// W2 §3(a): the input is constructed through the SAME exported single-source builder the stage
|
|
623
|
+
// bridge uses (node_ref cloned inside — review F3), so the LLM-facing input cannot drift from
|
|
624
|
+
// the module-validated input by construction.
|
|
625
|
+
const r = tnode.node_ref;
|
|
626
|
+
const input = buildSynthesisInputForNode(trace, nodesByKey, modes, key, new Map(consumedChildren.map((c) => [reduceNodeKey(c.node_ref), c.semantic_summary])));
|
|
627
|
+
assertSynthesisInputBounded(input);
|
|
628
|
+
const out = opts.synthesize(input);
|
|
629
|
+
assertSynthesisOutputBounded(out); // round-2: validate the caller's OUTPUT, not just the input.
|
|
630
|
+
const { boundaries: classified, coverage } = reconcileBoundaries(out.boundaries, reduceNode);
|
|
631
|
+
// N3: verify EVERY unanchored boundary (structure is blind there). Anchored stay location-only.
|
|
632
|
+
// The injected verifier's return is validated (review F2): a bogus status must fail closed here,
|
|
633
|
+
// not slip into a seed-bound node.
|
|
634
|
+
const verified = classified.map((b) => {
|
|
635
|
+
if (b.anchor_status !== "unanchored")
|
|
636
|
+
return b;
|
|
637
|
+
// Clone node_ref + boundary for the verifier too (round-2: F3 only cloned the synthesize input;
|
|
638
|
+
// the verifier could mutate the live trace node_ref and corrupt later child-summary keys).
|
|
639
|
+
const v = opts.verifyUnanchored({
|
|
640
|
+
node_ref: { sheet: r.sheet, column_index: r.column_index, row_start: r.row_start, row_end: r.row_end },
|
|
641
|
+
boundary: { ...b },
|
|
642
|
+
summary: out.semantic_summary,
|
|
643
|
+
});
|
|
644
|
+
if (!VALID_ADVERSARIAL_RESULT.has(v)) {
|
|
645
|
+
throw new Error(`comprehension-semantic-map: verifyUnanchored returned invalid result '${v}' at ${key} — must be adversarial_confirmed | adversarial_refuted (§13.5 fail-closed).`);
|
|
646
|
+
}
|
|
647
|
+
return { ...b, verification: v };
|
|
648
|
+
});
|
|
649
|
+
const refutedCount = verified.filter((b) => b.verification === "adversarial_refuted").length;
|
|
650
|
+
// Seed-bound: refuted boundaries are EXCLUDED from the seed boundary list (codex-F2) — counted in
|
|
651
|
+
// taint below. Non-seed: kept for inspection (still counted in taint via ownUnverifiedCount).
|
|
652
|
+
const keptBoundaries = seedBound ? verified.filter((b) => b.verification !== "adversarial_refuted") : verified;
|
|
653
|
+
const preImage = {
|
|
654
|
+
...preImageBase,
|
|
655
|
+
layer1_ground_hash: tnode.ground_hash,
|
|
656
|
+
// EPOCH recursion is decoupled from judgment CONSUMPTION (review F1 / onto issue-003/006/011): a
|
|
657
|
+
// frontier consumes no child SUMMARIES (flat read) but STILL folds ALL children's contributions,
|
|
658
|
+
// so a non-propagating descendant change (parent ground byte-identical) rotates the frontier key —
|
|
659
|
+
// matching the accumulating path (fail-safe: over-rotate, never stale-reuse).
|
|
660
|
+
child_contributions: children.map((c) => c.subtree_epoch_contribution),
|
|
661
|
+
};
|
|
662
|
+
const node = {
|
|
663
|
+
node_ref: { sheet: r.sheet, column_index: r.column_index, row_start: r.row_start, row_end: r.row_end },
|
|
664
|
+
layer1_ground_hash: tnode.ground_hash,
|
|
665
|
+
subtree_epoch_contribution: reduceNodeEpochContribution(preImage),
|
|
666
|
+
authority: "non_authoritative",
|
|
667
|
+
provisional: true,
|
|
668
|
+
reduce_read_attempt: "produced",
|
|
669
|
+
semantic_summary: out.semantic_summary,
|
|
670
|
+
semantic_boundaries: keptBoundaries,
|
|
671
|
+
structure_boundary_coverage: coverage,
|
|
672
|
+
topology_child_keys: [...tnode.child_keys], // clone so the returned node does not alias the trace array (round-3 F3).
|
|
673
|
+
consumed_child_judgment_keys: consumedChildKeys, // [] for a frontier; all children for accumulating.
|
|
674
|
+
unanchored_unverified_count: 0,
|
|
675
|
+
};
|
|
676
|
+
// Taint = CONSUMED children + own remaining unverified boundaries + any refuted removed for seed.
|
|
677
|
+
node.unanchored_unverified_count =
|
|
678
|
+
computeUnanchoredUnverifiedCount(node, consumedChildren) + (seedBound ? refutedCount : 0);
|
|
679
|
+
// Fail-closed validators (§13.5). expectedConsumedKeys = the non-subsumed children.
|
|
680
|
+
assertChildJudgmentCoverage(node, consumedChildKeys);
|
|
681
|
+
assertSemanticBoundaryHonesty(node, seedBound);
|
|
682
|
+
assertTaintCensusMonotone(node, consumedChildren);
|
|
683
|
+
result.set(key, node);
|
|
684
|
+
return node;
|
|
685
|
+
};
|
|
686
|
+
visit(trace.root_key);
|
|
687
|
+
// Reachability (review F5 / onto issue-003): every trace node MUST be reached from the root, else an
|
|
688
|
+
// orphan node would be silently dropped from the accumulated map (a completeness violation).
|
|
689
|
+
if (result.size !== trace.nodes.size) {
|
|
690
|
+
throw new Error(`comprehension-semantic-map: ${trace.nodes.size - result.size} trace node(s) unreachable from root ${trace.root_key} — orphan nodes would be silently dropped (§13.5 fail-closed).`);
|
|
691
|
+
}
|
|
692
|
+
return result;
|
|
693
|
+
}
|
|
694
|
+
/** Project the accumulated semantic map into a bounded, honest seed input (§6). Pure / deterministic.
|
|
695
|
+
* ★INPUT CONTRACT (S4 code review): the map MUST be accumulated with seedBound=FALSE — refuted
|
|
696
|
+
* boundaries are RETAINED in semantic_boundaries so this projection is the sole place that excludes
|
|
697
|
+
* them (into refuted_disclosure). A seedBound=true map would hide refuted from the disclosure. The
|
|
698
|
+
* taint census is DERIVED from what the projection actually sees (refuted boundaries + failed/unread
|
|
699
|
+
* regions) — self-consistent with the disclosure, never trusting the map's own
|
|
700
|
+
* unanchored_unverified_count (which a caller could set to NaN / non-monotone). */
|
|
701
|
+
export function projectSemanticMapToSeed(map, opts = {}) {
|
|
702
|
+
const checkBound = (name, v) => {
|
|
703
|
+
if (v !== undefined && (!Number.isSafeInteger(v) || v < 0)) {
|
|
704
|
+
throw new Error(`comprehension-semantic-map: seed projection ${name} must be a non-negative safe integer or absent (fail-closed; -1/NaN would silently show nothing).`);
|
|
705
|
+
}
|
|
706
|
+
};
|
|
707
|
+
checkBound("maxNodes", opts.maxNodes);
|
|
708
|
+
checkBound("maxDisclosure", opts.maxDisclosure);
|
|
709
|
+
const nodesAll = [];
|
|
710
|
+
const refuted = [];
|
|
711
|
+
let failedOrUnread = 0;
|
|
712
|
+
// Deterministic CANONICAL order — by reduceNodeKey(node_ref), NOT the caller's map key.
|
|
713
|
+
const nodes = [...map.values()].sort((a, b) => cmpStr(reduceNodeKey(a.node_ref), reduceNodeKey(b.node_ref)));
|
|
714
|
+
for (const node of nodes) {
|
|
715
|
+
// Honesty precondition (fail-closed): legal (anchor_status, verification) states only. seedBound=false
|
|
716
|
+
// (the map still carries refuted boundaries; THIS projection is what excludes them from seed).
|
|
717
|
+
assertSemanticBoundaryHonesty(node, false);
|
|
718
|
+
// The map key MUST be the canonical node key (else consumed/child keys diverge; S3 F4 class).
|
|
719
|
+
const canonicalKey = reduceNodeKey(node.node_ref);
|
|
720
|
+
if (map.get(canonicalKey) !== node) {
|
|
721
|
+
throw new Error(`comprehension-semantic-map: seed projection — a node is stored under a non-canonical map key (expected '${canonicalKey}') (§13.6 fail-closed).`);
|
|
722
|
+
}
|
|
723
|
+
if (node.reduce_read_attempt === "subsumed") {
|
|
724
|
+
assertSubsumedNodeEmpty(node); // a subsumed node must carry no judgment/taint (fail-closed).
|
|
725
|
+
continue; // no judgment (frontier ancestor covers it).
|
|
726
|
+
}
|
|
727
|
+
if (node.reduce_read_attempt === "failed" || node.reduce_read_attempt === "unread") {
|
|
728
|
+
failedOrUnread += 1; // an unverified region (the read failed) — counted in taint, no seed node.
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
const region = node.node_ref;
|
|
732
|
+
const boundaries = [];
|
|
733
|
+
for (const b of node.semantic_boundaries) {
|
|
734
|
+
if (b.anchor_status === "anchored") {
|
|
735
|
+
boundaries.push({ row: b.row, character_before: b.character_before, character_after: b.character_after, disposition: "structural_location_only" });
|
|
736
|
+
}
|
|
737
|
+
else if (b.verification === "adversarial_confirmed") {
|
|
738
|
+
boundaries.push({ row: b.row, character_before: b.character_before, character_after: b.character_after, disposition: "adversarial_confirmed" });
|
|
739
|
+
}
|
|
740
|
+
else if (b.verification === "adversarial_refuted") {
|
|
741
|
+
refuted.push({
|
|
742
|
+
node_ref: { sheet: region.sheet, column_index: region.column_index, row_start: region.row_start, row_end: region.row_end },
|
|
743
|
+
row: b.row,
|
|
744
|
+
character_before: b.character_before,
|
|
745
|
+
character_after: b.character_after,
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
// unanchored + unverified: accumulate must have adversarially processed it before seed (N3).
|
|
750
|
+
throw new Error(`comprehension-semantic-map: unverified unanchored boundary at ${reduceNodeKey(region)}@row${b.row} reached seed projection — every unanchored boundary must be adversarially verified first (§13.5 N3 fail-closed).`);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
nodesAll.push({
|
|
754
|
+
node_ref: { sheet: region.sheet, column_index: region.column_index, row_start: region.row_start, row_end: region.row_end },
|
|
755
|
+
semantic_summary: node.semantic_summary,
|
|
756
|
+
boundaries,
|
|
757
|
+
});
|
|
758
|
+
}
|
|
759
|
+
// Taint DERIVED from what the projection sees (refuted boundaries + failed/unread regions) — self-
|
|
760
|
+
// consistent with refuted_disclosure, never trusting the caller's count.
|
|
761
|
+
const taint = refuted.length + failedOrUnread;
|
|
762
|
+
return {
|
|
763
|
+
authority: "non_authoritative",
|
|
764
|
+
provisional: true,
|
|
765
|
+
nodes: nodesAll.slice(0, opts.maxNodes ?? nodesAll.length),
|
|
766
|
+
nodes_total: nodesAll.length,
|
|
767
|
+
refuted_disclosure: refuted.slice(0, opts.maxDisclosure ?? refuted.length),
|
|
768
|
+
refuted_disclosure_total: refuted.length,
|
|
769
|
+
unanchored_unverified_total: taint,
|
|
770
|
+
};
|
|
771
|
+
}
|