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,367 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
// ── canonical projection (resume-key subject; prose-free, byte-stable) ────────
|
|
3
|
+
/** Re-key a boundary witness into a FIXED field order so JSON.stringify is byte-stable regardless of
|
|
4
|
+
* how a producer happened to construct the object (nested-object canonicalization; review F2). */
|
|
5
|
+
function canonicalWitness(b) {
|
|
6
|
+
return {
|
|
7
|
+
sheet: b.sheet,
|
|
8
|
+
column_index: b.column_index,
|
|
9
|
+
boundary_kind: b.boundary_kind,
|
|
10
|
+
prev_shape: b.prev_shape,
|
|
11
|
+
new_shape: b.new_shape,
|
|
12
|
+
last_prev_format_row: b.last_prev_format_row,
|
|
13
|
+
first_new_format_row: b.first_new_format_row,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
const cmpStr = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
|
|
17
|
+
function canonicalBoundaries(boundaries) {
|
|
18
|
+
const seen = new Set();
|
|
19
|
+
const out = [];
|
|
20
|
+
for (const b of boundaries) {
|
|
21
|
+
const k = `${b.sheet}|${b.column_index}|${b.boundary_kind}|${b.first_new_format_row}|${b.last_prev_format_row}|${b.prev_shape}|${b.new_shape}`;
|
|
22
|
+
if (seen.has(k))
|
|
23
|
+
continue;
|
|
24
|
+
seen.add(k);
|
|
25
|
+
out.push(canonicalWitness(b));
|
|
26
|
+
}
|
|
27
|
+
// The sort key MUST be a superset of the dedup key (review R8-CANON-BOUND-SORT / F1-det): dedup keeps
|
|
28
|
+
// 7 fields, so ties on a shorter sort key fall back to input order = grouping-VARIANT hash. Ordering
|
|
29
|
+
// by all 7 dedup fields is a TOTAL order (dedup guarantees the tuple is unique).
|
|
30
|
+
out.sort((a, b) => cmpStr(a.sheet, b.sheet) ||
|
|
31
|
+
a.column_index - b.column_index ||
|
|
32
|
+
a.first_new_format_row - b.first_new_format_row ||
|
|
33
|
+
a.last_prev_format_row - b.last_prev_format_row ||
|
|
34
|
+
cmpStr(a.boundary_kind, b.boundary_kind) ||
|
|
35
|
+
cmpStr(a.prev_shape, b.prev_shape) ||
|
|
36
|
+
cmpStr(a.new_shape, b.new_shape));
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
/** The canonical GROUND of a node — the byte-stable subject a resume key hashes. Contains only
|
|
40
|
+
* deterministic structure (no prose, no LLM field), sorted/normalized so equal structure → equal bytes. */
|
|
41
|
+
export function reduceNodeGround(node) {
|
|
42
|
+
// Every nested object is re-keyed into a FIXED field order (region, witnesses via canonicalBoundaries,
|
|
43
|
+
// limiting_witness) so byte-stability does not silently rest on producers sharing one key-insertion
|
|
44
|
+
// order (review F2).
|
|
45
|
+
const r = node.region;
|
|
46
|
+
const w = node.limiting_witness;
|
|
47
|
+
return {
|
|
48
|
+
region: { sheet: r.sheet, column_index: r.column_index, row_start: r.row_start, row_end: r.row_end },
|
|
49
|
+
format_clusters: [...node.format_clusters].sort(),
|
|
50
|
+
boundaries: canonicalBoundaries(node.boundaries),
|
|
51
|
+
edge_first_shape: node.edge_first_shape,
|
|
52
|
+
edge_last_shape: node.edge_last_shape,
|
|
53
|
+
distinct_is_lower_bound: node.distinct_is_lower_bound,
|
|
54
|
+
boundaries_are_lower_bound: node.boundaries_are_lower_bound,
|
|
55
|
+
segments_capped: node.segments_capped,
|
|
56
|
+
limiting_witness: w
|
|
57
|
+
? { sheet: w.sheet, column_index: w.column_index, row: w.row, reason: w.reason }
|
|
58
|
+
: null,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
export function reduceNodeGroundHash(node) {
|
|
62
|
+
return createHash("sha256").update(JSON.stringify(reduceNodeGround(node))).digest("hex");
|
|
63
|
+
}
|
|
64
|
+
// ── canonical child-partition (contiguous only) + fail-closed validator ───────
|
|
65
|
+
function sortCanonical(children) {
|
|
66
|
+
return [...children].sort((a, b) => a.region.row_start - b.region.row_start);
|
|
67
|
+
}
|
|
68
|
+
/** Fail-closed: the canonical child-partition MUST be a single column and contiguous (no overlap /
|
|
69
|
+
* interleave; a row gap is allowed). Returns [] when valid. `mergeReduceNodes` throws on any
|
|
70
|
+
* violation — the invalid partition is made INVALID, not merely forbidden in prose (design §4/§8 F1,
|
|
71
|
+
* symmetric with the R9 honesty validator). */
|
|
72
|
+
export function assertContiguousChildren(children) {
|
|
73
|
+
const firstNode = children[0];
|
|
74
|
+
if (!firstNode)
|
|
75
|
+
return [{ code: "empty", message: "reduce requires ≥1 child" }];
|
|
76
|
+
const first = firstNode.region;
|
|
77
|
+
for (const c of children) {
|
|
78
|
+
if (c.region.row_start > c.region.row_end) {
|
|
79
|
+
return [
|
|
80
|
+
{
|
|
81
|
+
code: "inverted_range",
|
|
82
|
+
message: `inverted range [${c.region.row_start},${c.region.row_end}] (row_start > row_end)`,
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
if (c.region.sheet !== first.sheet || c.region.column_index !== first.column_index) {
|
|
87
|
+
return [
|
|
88
|
+
{
|
|
89
|
+
code: "mixed_region",
|
|
90
|
+
message: `all children must share sheet+column_index (got ${c.region.sheet}#${c.region.column_index} vs ${first.sheet}#${first.column_index}) — per-column reduce`,
|
|
91
|
+
},
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const sorted = sortCanonical(children);
|
|
96
|
+
const violations = [];
|
|
97
|
+
for (let i = 0; i + 1 < sorted.length; i += 1) {
|
|
98
|
+
const an = sorted[i];
|
|
99
|
+
const bn = sorted[i + 1];
|
|
100
|
+
if (!an || !bn)
|
|
101
|
+
continue;
|
|
102
|
+
const a = an.region;
|
|
103
|
+
const b = bn.region;
|
|
104
|
+
// b must start strictly after a ends (gap allowed). b.row_start <= a.row_end ⇒ overlap/interleave.
|
|
105
|
+
if (b.row_start <= a.row_end) {
|
|
106
|
+
violations.push({
|
|
107
|
+
code: "overlap_or_interleave",
|
|
108
|
+
message: `children overlap/interleave: [${a.row_start},${a.row_end}] then [${b.row_start},${b.row_end}] — a non-contiguous partition silently drops seams (R8)`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return violations;
|
|
113
|
+
}
|
|
114
|
+
/** Fail-closed honesty (R9 §5.4): a parent may never UNDERSTATE a child's lower bound. Returns []
|
|
115
|
+
* when the parent correctly ORs its children's flags. */
|
|
116
|
+
export function assertHonestyFold(parent, children) {
|
|
117
|
+
const anyDistinctLB = children.some((c) => c.distinct_is_lower_bound);
|
|
118
|
+
const anyBoundsLB = children.some((c) => c.boundaries_are_lower_bound);
|
|
119
|
+
const anyCapped = children.some((c) => c.segments_capped);
|
|
120
|
+
const bad = [];
|
|
121
|
+
if (anyDistinctLB && !parent.distinct_is_lower_bound)
|
|
122
|
+
bad.push({ code: "overlap_or_interleave", message: "parent dropped a child's distinct_is_lower_bound (R9)" });
|
|
123
|
+
if (anyBoundsLB && !parent.boundaries_are_lower_bound)
|
|
124
|
+
bad.push({ code: "overlap_or_interleave", message: "parent dropped a child's boundaries_are_lower_bound (R9)" });
|
|
125
|
+
if (anyCapped && !parent.segments_capped)
|
|
126
|
+
bad.push({ code: "overlap_or_interleave", message: "parent dropped a child's segments_capped (R9)" });
|
|
127
|
+
return bad;
|
|
128
|
+
}
|
|
129
|
+
// ── the UNION monoid ──────────────────────────────────────────────────────────
|
|
130
|
+
/** Fold k contiguous same-column children into one parent node. Deterministic, associative &
|
|
131
|
+
* commutative on the GROUND (canonical sort + set-union + adjacency-gated seam + OR honesty), so any
|
|
132
|
+
* canonical grouping yields a byte-identical root ground (grouping-invariance, R8). Throws fail-closed
|
|
133
|
+
* on a non-contiguous / mixed-region partition. */
|
|
134
|
+
export function mergeReduceNodes(children) {
|
|
135
|
+
const violations = assertContiguousChildren(children);
|
|
136
|
+
if (violations.length > 0) {
|
|
137
|
+
throw new Error(`comprehension-reduce: invalid child-partition — ${violations.map((v) => v.message).join("; ")}`);
|
|
138
|
+
}
|
|
139
|
+
const sorted = sortCanonical(children);
|
|
140
|
+
const first = sorted[0];
|
|
141
|
+
const last = sorted[sorted.length - 1];
|
|
142
|
+
if (!first || !last)
|
|
143
|
+
throw new Error("comprehension-reduce: empty partition after validation (unreachable)");
|
|
144
|
+
const clusters = new Set();
|
|
145
|
+
const boundaries = [];
|
|
146
|
+
for (const c of sorted) {
|
|
147
|
+
for (const f of c.format_clusters)
|
|
148
|
+
clusters.add(f);
|
|
149
|
+
for (const b of c.boundaries)
|
|
150
|
+
boundaries.push(b);
|
|
151
|
+
}
|
|
152
|
+
// Seam: a NEW value-shape boundary at a child junction, ONLY when the two children are row-adjacent
|
|
153
|
+
// AND their touching edge shapes differ. A gap ⇒ no seam (the contiguity validator already rejected
|
|
154
|
+
// overlap/interleave, so only adjacency vs gap remains here).
|
|
155
|
+
for (let i = 0; i + 1 < sorted.length; i += 1) {
|
|
156
|
+
const a = sorted[i];
|
|
157
|
+
const b = sorted[i + 1];
|
|
158
|
+
if (!a || !b)
|
|
159
|
+
continue;
|
|
160
|
+
const adjacent = a.region.row_end + 1 === b.region.row_start;
|
|
161
|
+
if (adjacent &&
|
|
162
|
+
a.edge_last_shape !== null &&
|
|
163
|
+
b.edge_first_shape !== null &&
|
|
164
|
+
a.edge_last_shape !== b.edge_first_shape) {
|
|
165
|
+
boundaries.push({
|
|
166
|
+
sheet: a.region.sheet,
|
|
167
|
+
column_index: a.region.column_index,
|
|
168
|
+
boundary_kind: "value_shape",
|
|
169
|
+
prev_shape: a.edge_last_shape,
|
|
170
|
+
new_shape: b.edge_first_shape,
|
|
171
|
+
last_prev_format_row: a.region.row_end,
|
|
172
|
+
first_new_format_row: b.region.row_start,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
// Honesty fold (R9): OR the lower-bound / capped flags; localize the limiting witness to the
|
|
177
|
+
// lowest-row capped child.
|
|
178
|
+
const distinctLB = sorted.some((c) => c.distinct_is_lower_bound);
|
|
179
|
+
const boundsLB = sorted.some((c) => c.boundaries_are_lower_bound);
|
|
180
|
+
const capped = sorted.some((c) => c.segments_capped);
|
|
181
|
+
const witnessCandidates = sorted
|
|
182
|
+
.map((c) => c.limiting_witness)
|
|
183
|
+
.filter((w) => w !== null)
|
|
184
|
+
.sort((x, y) => x.row - y.row);
|
|
185
|
+
const parent = {
|
|
186
|
+
region: {
|
|
187
|
+
sheet: first.region.sheet,
|
|
188
|
+
column_index: first.region.column_index,
|
|
189
|
+
row_start: first.region.row_start,
|
|
190
|
+
row_end: last.region.row_end,
|
|
191
|
+
},
|
|
192
|
+
format_clusters: [...clusters].sort(),
|
|
193
|
+
boundaries: canonicalBoundaries(boundaries),
|
|
194
|
+
edge_first_shape: first.edge_first_shape,
|
|
195
|
+
edge_last_shape: last.edge_last_shape,
|
|
196
|
+
distinct_is_lower_bound: distinctLB,
|
|
197
|
+
boundaries_are_lower_bound: boundsLB,
|
|
198
|
+
segments_capped: capped,
|
|
199
|
+
limiting_witness: witnessCandidates[0] ?? null,
|
|
200
|
+
};
|
|
201
|
+
// Redundant guard (structurally can't fail given the ORs above, but catches a future regression).
|
|
202
|
+
const honesty = assertHonestyFold(parent, sorted);
|
|
203
|
+
if (honesty.length > 0) {
|
|
204
|
+
throw new Error(`comprehension-reduce: honesty fold violated — ${honesty.map((v) => v.message).join("; ")}`);
|
|
205
|
+
}
|
|
206
|
+
return parent;
|
|
207
|
+
}
|
|
208
|
+
/** Fold an ordered list of leaves into a single root. `fanin` controls tree shape: undefined / ≥ N →
|
|
209
|
+
* one flat k-ary merge; a small fanin (e.g. 3) builds a deeper hierarchical tree. The root GROUND is
|
|
210
|
+
* invariant to `fanin` (grouping-invariance). */
|
|
211
|
+
export function reduceColumnLeaves(leaves, fanin) {
|
|
212
|
+
const only = leaves[0];
|
|
213
|
+
if (!only)
|
|
214
|
+
throw new Error("comprehension-reduce: no leaves");
|
|
215
|
+
if (leaves.length === 1)
|
|
216
|
+
return only;
|
|
217
|
+
const f = fanin && fanin >= 2 ? fanin : leaves.length;
|
|
218
|
+
let level = sortCanonical(leaves);
|
|
219
|
+
while (level.length > 1) {
|
|
220
|
+
const next = [];
|
|
221
|
+
for (let i = 0; i < level.length; i += f) {
|
|
222
|
+
const group = level.slice(i, i + f);
|
|
223
|
+
const single = group[0];
|
|
224
|
+
if (group.length === 1 && single) {
|
|
225
|
+
next.push(single);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
next.push(mergeReduceNodes(group));
|
|
229
|
+
}
|
|
230
|
+
level = next;
|
|
231
|
+
}
|
|
232
|
+
const root = level[0];
|
|
233
|
+
if (!root)
|
|
234
|
+
throw new Error("comprehension-reduce: empty tree (unreachable)");
|
|
235
|
+
return root;
|
|
236
|
+
}
|
|
237
|
+
export function reduceNodeKey(region) {
|
|
238
|
+
return `${region.sheet}#${region.column_index}:${region.row_start}-${region.row_end}`;
|
|
239
|
+
}
|
|
240
|
+
/** Same fold as `reduceColumnLeaves`, additionally recording a `ReduceTopologyTrace` and the actual
|
|
241
|
+
* `nodesByKey` (needed by the Layer-2 accumulation to reconcile each node's value-shape seams —
|
|
242
|
+
* §13.2). The returned `root` is BYTE-IDENTICAL to `reduceColumnLeaves(leaves, fanin)` (byte-parity,
|
|
243
|
+
* tested). The trace registers EVERY node — all leaves up front, then each merge output — so its node
|
|
244
|
+
* set equals the distinct skeleton nodes (design §13.1 codex-F4: registering only merge outputs would
|
|
245
|
+
* drop leaves and pass-through nodes). A row GROUP of size 1 is a pass-through (identity): the
|
|
246
|
+
* already-registered node flows up unchanged, so no new node is created. */
|
|
247
|
+
export function reduceColumnLeavesWithTrace(leaves, fanin) {
|
|
248
|
+
const only = leaves[0];
|
|
249
|
+
if (!only)
|
|
250
|
+
throw new Error("comprehension-reduce: no leaves");
|
|
251
|
+
const nodes = new Map();
|
|
252
|
+
const nodesByKey = new Map();
|
|
253
|
+
const register = (node, childKeys) => {
|
|
254
|
+
const r = node.region;
|
|
255
|
+
const key = reduceNodeKey(r);
|
|
256
|
+
nodes.set(key, {
|
|
257
|
+
node_ref: { sheet: r.sheet, column_index: r.column_index, row_start: r.row_start, row_end: r.row_end },
|
|
258
|
+
ground_hash: reduceNodeGroundHash(node),
|
|
259
|
+
child_keys: childKeys,
|
|
260
|
+
});
|
|
261
|
+
nodesByKey.set(key, node);
|
|
262
|
+
return key;
|
|
263
|
+
};
|
|
264
|
+
// Register every leaf FIRST (F4): leaves and pass-through nodes never go through mergeReduceNodes.
|
|
265
|
+
for (const leaf of leaves)
|
|
266
|
+
register(leaf, []);
|
|
267
|
+
if (leaves.length === 1) {
|
|
268
|
+
return { root: only, trace: { nodes, root_key: reduceNodeKey(only.region) }, nodesByKey };
|
|
269
|
+
}
|
|
270
|
+
const f = fanin && fanin >= 2 ? fanin : leaves.length;
|
|
271
|
+
let level = sortCanonical(leaves);
|
|
272
|
+
while (level.length > 1) {
|
|
273
|
+
const next = [];
|
|
274
|
+
for (let i = 0; i < level.length; i += f) {
|
|
275
|
+
const group = level.slice(i, i + f);
|
|
276
|
+
const single = group[0];
|
|
277
|
+
if (group.length === 1 && single) {
|
|
278
|
+
next.push(single); // pass-through — already registered; identity, no new node.
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
const parent = mergeReduceNodes(group);
|
|
282
|
+
register(parent, group.map((c) => reduceNodeKey(c.region)));
|
|
283
|
+
next.push(parent);
|
|
284
|
+
}
|
|
285
|
+
level = next;
|
|
286
|
+
}
|
|
287
|
+
const root = level[0];
|
|
288
|
+
if (!root)
|
|
289
|
+
throw new Error("comprehension-reduce: empty tree (unreachable)");
|
|
290
|
+
return { root, trace: { nodes, root_key: reduceNodeKey(root.region) }, nodesByKey };
|
|
291
|
+
}
|
|
292
|
+
// ── leaf construction from real value tiles (LLM-free) ────────────────────────
|
|
293
|
+
const VALUE_SHAPE = "value_shape";
|
|
294
|
+
/** Build leaf nodes from one column's value tiles by grouping its NON-EMPTY segments into `leafCount`
|
|
295
|
+
* contiguous row-window blocks (default: one leaf per non-empty segment). Each leaf is a pure
|
|
296
|
+
* deterministic projection of the segments + intra-tile notes. */
|
|
297
|
+
export function buildColumnLeaves(sheet, column, opts) {
|
|
298
|
+
const nonEmpty = column.segments.filter((s) => s.dominant_shape !== null);
|
|
299
|
+
if (nonEmpty.length === 0)
|
|
300
|
+
return [];
|
|
301
|
+
const valueNotes = (column.intra_tile_notes ?? []).filter((n) => n.boundary_kind === VALUE_SHAPE);
|
|
302
|
+
const notesAsc = [...valueNotes].sort((a, b) => a.first_new_format_row - b.first_new_format_row);
|
|
303
|
+
// The TRUE value-shape at a given row = the new_shape of the latest transition at/before it (or the
|
|
304
|
+
// shape BEFORE the first transition; or the uniform dominant when there are no transitions). The
|
|
305
|
+
// segment `dominant_shape` is a windowed MAJORITY — using it as the edge shape fabricates a false
|
|
306
|
+
// seam when a transition straddles a segment window (review F3, blocker); this makes the edge shape
|
|
307
|
+
// exact and tiling-independent.
|
|
308
|
+
const shapeAtRow = (row, uniformFallback) => {
|
|
309
|
+
const firstNote = notesAsc[0];
|
|
310
|
+
if (!firstNote)
|
|
311
|
+
return uniformFallback;
|
|
312
|
+
let shape = firstNote.prev_shape;
|
|
313
|
+
for (const n of notesAsc) {
|
|
314
|
+
if (n.first_new_format_row <= row)
|
|
315
|
+
shape = n.new_shape;
|
|
316
|
+
else
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
return shape;
|
|
320
|
+
};
|
|
321
|
+
const leafCount = Math.max(1, Math.min(opts?.leafCount ?? nonEmpty.length, nonEmpty.length));
|
|
322
|
+
const per = Math.ceil(nonEmpty.length / leafCount);
|
|
323
|
+
const leaves = [];
|
|
324
|
+
for (let i = 0; i < nonEmpty.length; i += per) {
|
|
325
|
+
const block = nonEmpty.slice(i, i + per);
|
|
326
|
+
const b0 = block[0];
|
|
327
|
+
const bLast = block[block.length - 1];
|
|
328
|
+
if (!b0 || !bLast)
|
|
329
|
+
continue;
|
|
330
|
+
const rowStart = Math.min(...block.map((s) => s.row_start));
|
|
331
|
+
const rowEnd = Math.max(...block.map((s) => s.row_end));
|
|
332
|
+
const clusters = [...new Set(block.map((s) => s.dominant_shape))].sort();
|
|
333
|
+
const capped = column.segments_capped;
|
|
334
|
+
const distinctLB = block.some((s) => s.distinct_is_lower_bound);
|
|
335
|
+
const boundaries = valueNotes
|
|
336
|
+
.filter((n) => n.first_new_format_row > rowStart && n.first_new_format_row <= rowEnd)
|
|
337
|
+
.map((n) => ({
|
|
338
|
+
sheet,
|
|
339
|
+
column_index: column.column_index,
|
|
340
|
+
boundary_kind: VALUE_SHAPE,
|
|
341
|
+
prev_shape: n.prev_shape,
|
|
342
|
+
new_shape: n.new_shape,
|
|
343
|
+
last_prev_format_row: n.last_prev_format_row,
|
|
344
|
+
first_new_format_row: n.first_new_format_row,
|
|
345
|
+
}));
|
|
346
|
+
leaves.push({
|
|
347
|
+
region: { sheet, column_index: column.column_index, row_start: rowStart, row_end: rowEnd },
|
|
348
|
+
format_clusters: clusters,
|
|
349
|
+
boundaries,
|
|
350
|
+
edge_first_shape: shapeAtRow(rowStart, b0.dominant_shape),
|
|
351
|
+
edge_last_shape: shapeAtRow(rowEnd, bLast.dominant_shape),
|
|
352
|
+
distinct_is_lower_bound: distinctLB,
|
|
353
|
+
boundaries_are_lower_bound: capped,
|
|
354
|
+
segments_capped: capped,
|
|
355
|
+
// Localize the limiting witness for EITHER lower-bound axis (review F1: previously distinct-only).
|
|
356
|
+
limiting_witness: distinctLB || capped
|
|
357
|
+
? {
|
|
358
|
+
sheet,
|
|
359
|
+
column_index: column.column_index,
|
|
360
|
+
row: rowStart,
|
|
361
|
+
reason: distinctLB ? "distinct_count capped" : "segments capped",
|
|
362
|
+
}
|
|
363
|
+
: null,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
return leaves;
|
|
367
|
+
}
|