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.
Files changed (128) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +137 -0
  3. package/.onto/domains/accounting-kr/competency_qs.md +153 -153
  4. package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
  5. package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
  6. package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
  7. package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +169 -8
  8. package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
  9. package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
  10. package/.onto/processes/review/productized-live-path.md +9 -5
  11. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  12. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  13. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  14. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  15. package/README.md +64 -1
  16. package/dist/cli.js +11 -1
  17. package/dist/core-api/reconstruct-api.js +575 -24
  18. package/dist/core-api/review-api.js +70 -5
  19. package/dist/core-api/review-progress.js +7 -0
  20. package/dist/core-api/runtime-observation.js +7 -0
  21. package/dist/core-runtime/artifact-io.js +75 -3
  22. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
  23. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  24. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  25. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  26. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  27. package/dist/core-runtime/cli/run-seats.js +18 -0
  28. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  29. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  30. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  31. package/dist/core-runtime/discovery/host-detection.js +71 -22
  32. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  33. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  34. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  35. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  36. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  37. package/dist/core-runtime/discovery/supported-models.js +282 -29
  38. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  39. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  40. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  46. package/dist/core-runtime/effort-calibration-ingest.js +135 -0
  47. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  48. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  49. package/dist/core-runtime/llm/llm-caller.js +188 -47
  50. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  51. package/dist/core-runtime/llm/model-switcher.js +1 -1
  52. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  53. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  54. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  55. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  56. package/dist/core-runtime/onboard/cli-host.js +5 -2
  57. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  58. package/dist/core-runtime/onboard/host-target.js +28 -3
  59. package/dist/core-runtime/onboard/register.js +54 -12
  60. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  61. package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
  62. package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
  63. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  64. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  65. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  66. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  67. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  68. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  69. package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
  70. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  71. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  72. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  73. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  74. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  75. package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
  76. package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
  77. package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
  78. package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
  79. package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
  80. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  81. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
  82. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  83. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
  84. package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
  85. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
  86. package/dist/core-runtime/reconstruct/record.js +42 -0
  87. package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
  88. package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
  89. package/dist/core-runtime/reconstruct/run.js +5198 -536
  90. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
  91. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
  92. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
  93. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  94. package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
  95. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
  96. package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
  97. package/dist/core-runtime/review/continuation-plan.js +12 -5
  98. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  99. package/dist/core-runtime/review/materializers.js +100 -11
  100. package/dist/core-runtime/review/nesting-batch.js +6 -2
  101. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  102. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  103. package/dist/core-runtime/review/review-artifact-utils.js +217 -5
  104. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  105. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  106. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  107. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  108. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  109. package/dist/core-runtime/route-identity.js +182 -0
  110. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  111. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  112. package/dist/core-runtime/target-material-kind.js +58 -0
  113. package/dist/mcp/server.js +12 -5
  114. package/dist/tui/app.js +167 -0
  115. package/dist/tui/data/event-follower.js +116 -0
  116. package/dist/tui/data/node-detail.js +42 -0
  117. package/dist/tui/data/projection-poll.js +26 -0
  118. package/dist/tui/data/session-discovery.js +65 -0
  119. package/dist/tui/index.js +108 -0
  120. package/dist/tui/screens/log.js +25 -0
  121. package/dist/tui/screens/node-detail.js +17 -0
  122. package/dist/tui/screens/session-selector.js +26 -0
  123. package/dist/tui/screens/workflow-tree.js +90 -0
  124. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  125. package/dist/tui/view-model/review-adapter.js +270 -0
  126. package/dist/tui/view-model/tree-view-model.js +19 -0
  127. package/package.json +18 -3
  128. package/settings.example.json +77 -16
@@ -0,0 +1,336 @@
1
+ /**
2
+ * S1 — deterministic stratified sampler for the B4 `synthesize-cert/v1` bench
3
+ * (design 20260706-b4-r8-harness-design v3 §3/§4/§15.1).
4
+ *
5
+ * Everything here is pure and deterministic — the orchestrator script does the
6
+ * I/O (observe → buildColumnLeaves → reduceColumnLeavesWithTrace →
7
+ * classifyFrontier per column) and hands the per-fixture candidate universe in.
8
+ * Two responsibilities live here because they must not drift apart:
9
+ *
10
+ * 1. TWO-LAYER INPUT IDENTITY (§4 · round-2 R2-IND-1):
11
+ * - `deterministic_facts_sha256` — hash of the node's LLM-independent facts
12
+ * (node_ref + sorted format_clusters + canonical value_shape_seams,
13
+ * child_summaries EXCLUDED). Computable BEFORE the reference child
14
+ * authoring spend, so sampling/scope/floor pre-checks run pre-spend
15
+ * (§3 sequencing).
16
+ * - `input_sha256` — hash of the FULL frozen SemanticSynthesisInput packet
17
+ * (child_summaries included). The same-input comparison key: every arm
18
+ * runs the identical frozen packet, so baseline/candidate rows share it.
19
+ * Both hashes canonicalize through ONE serializer, and both run the shipped
20
+ * `assertSynthesisInputBounded` source-safe envelope first, so a smuggled
21
+ * extra field can never be silently hash-invisible.
22
+ *
23
+ * 2. HONEST DETERMINISTIC SELECTION (§3, cherry-pick 금지): per
24
+ * (fixture × possessed stratum), over-provision K inputs with a seeded,
25
+ * content-blind rule — merge strata order by subtree leaf count ascending
26
+ * (a DISCLOSED cost bias; seed tie-break), leaf strata stride-sample the
27
+ * stable-key order — and record provenance (pool size, seed, stride, rank,
28
+ * selected-vs-nearest-unselected) for the R7 representativeness audit.
29
+ *
30
+ * The pre-spend floor gate is part of the sample result (not a separate call a
31
+ * caller could forget): `floor_violations` re-uses the shipped
32
+ * `synthesizeCertManifestFloorViolations` plus a pre-spend prediction of the
33
+ * record validator's `stratum_global_floor`, so an under-provisioned manifest
34
+ * is rejected before any paid call. Structural integrity failures (a candidate
35
+ * whose key/hash lies, an input_id collision) THROW instead — those are
36
+ * harness bugs, not sampling outcomes.
37
+ */
38
+ import { createHash } from "node:crypto";
39
+ import { reduceNodeKey, } from "../reconstruct/comprehension-reduce.js";
40
+ import { assertReduceTopologyIsTree, assertSynthesisInputBounded, buildSynthesisInputForNode, computeSubtreeLeafCounts, } from "../reconstruct/comprehension-semantic-map.js";
41
+ import { synthesizeCertManifestFloorViolations, SYNTHESIZE_CERT_FLOORS, } from "./synthesize-cert-record.js";
42
+ /** Folded into every stratum seed — editing the sampler's selection rule must
43
+ * ship with a version bump so a re-run cannot silently claim the old picks. */
44
+ export const SYNTHESIZE_CERT_SAMPLER_VERSION = "synthesize-cert-sampler/v1";
45
+ /** §3 over-provisioning: at a pessimistic decisive rate of 0.4, K×reps(3)×0.4 =
46
+ * 6 ≥ the per-stratum decisive floor (5), so a stratum survives real losses. */
47
+ export const SYNTHESIZE_CERT_DEFAULT_PER_STRATUM_K = 5;
48
+ const sha256Hex = (text) => createHash("sha256").update(text).digest("hex");
49
+ /** Same projection format as the record module's internal stratum key (kept a
50
+ * one-line local; the record module's helper is private and that file is the
51
+ * frozen evidence contract). */
52
+ const stratumKey = (s) => `seam=${s.seam}|merge=${s.merge}`;
53
+ const cmpStr = (x, y) => (x < y ? -1 : x > y ? 1 : 0);
54
+ /** Fixed-key-order canonical shape shared by BOTH identity hashes: sorted
55
+ * clusters, canonically-sorted re-keyed seams, re-keyed node_ref. Child
56
+ * summaries (when included) keep their packet order — they are positional
57
+ * prompt content, and the builder's order is already deterministic. */
58
+ function canonicalPacketShape(packet, includeChildren) {
59
+ const r = packet.node_ref;
60
+ const seams = packet.value_shape_seams
61
+ .map((s) => ({ row: s.row, prev_shape: s.prev_shape, new_shape: s.new_shape }))
62
+ .sort((x, y) => x.row - y.row || cmpStr(x.prev_shape, y.prev_shape) || cmpStr(x.new_shape, y.new_shape));
63
+ return {
64
+ node_ref: { sheet: r.sheet, column_index: r.column_index, row_start: r.row_start, row_end: r.row_end },
65
+ format_clusters: [...packet.format_clusters].sort(),
66
+ value_shape_seams: seams,
67
+ ...(includeChildren
68
+ ? { child_summaries: packet.child_summaries.map((c) => ({ key: c.key, summary: c.summary })) }
69
+ : {}),
70
+ };
71
+ }
72
+ /** `deterministic_facts_sha256` (§4): child-independent by construction — the
73
+ * facts are wrapped with child_summaries=[] to reuse the shipped source-safe
74
+ * envelope guard, then hashed WITHOUT the child field. */
75
+ export function synthesizeCertDeterministicFactsSha256(facts) {
76
+ const probe = {
77
+ node_ref: facts.node_ref,
78
+ format_clusters: facts.format_clusters,
79
+ value_shape_seams: facts.value_shape_seams,
80
+ child_summaries: [],
81
+ };
82
+ assertSynthesisInputBounded(probe);
83
+ return sha256Hex(JSON.stringify(canonicalPacketShape(probe, false)));
84
+ }
85
+ /** `input_sha256` (§4): identity of the FULL frozen packet (child_summaries
86
+ * included) — the same-input comparison key across arms. The exact-key envelope
87
+ * guard runs first so an extra/smuggled field fails closed instead of being
88
+ * silently excluded from the hash. */
89
+ export function synthesizeCertInputSha256(packet) {
90
+ assertSynthesisInputBounded(packet);
91
+ return sha256Hex(JSON.stringify(canonicalPacketShape(packet, true)));
92
+ }
93
+ /**
94
+ * Enumerates the sampler universe for ONE column pipeline: every non-subsumed
95
+ * node, stratum-tagged (merge = accumulating mode; seam = the packet's canonical
96
+ * value_shape_seams are non-empty). The deterministic facts come through the
97
+ * SINGLE-SOURCE `buildSynthesisInputForNode` (with placeholder child summaries,
98
+ * discarded) so the facts hashed here are exactly the fields the frozen packet
99
+ * will carry — the two cannot drift.
100
+ */
101
+ export function collectSynthesizeCertCandidates(args) {
102
+ const { trace, nodesByKey, modes, sheetIndex } = args;
103
+ if (!Number.isSafeInteger(sheetIndex) || sheetIndex < 0) {
104
+ throw new Error(`synthesize-cert-sampler: sheetIndex must be a non-negative safe integer, got ${sheetIndex}`);
105
+ }
106
+ assertReduceTopologyIsTree(trace);
107
+ const leafCounts = computeSubtreeLeafCounts(trace);
108
+ const out = [];
109
+ for (const [key, tnode] of trace.nodes) {
110
+ const mode = modes.get(key);
111
+ if (mode === undefined) {
112
+ throw new Error(`synthesize-cert-sampler: no frontier mode for ${key} — modes must cover the trace`);
113
+ }
114
+ if (mode === "subsumed")
115
+ continue;
116
+ // Placeholder child summaries: buildSynthesisInputForNode requires a summary
117
+ // per consumed child; the facts we keep exclude child_summaries entirely.
118
+ const consumed = tnode.child_keys.filter((k) => modes.get(k) !== "subsumed");
119
+ const packetShape = buildSynthesisInputForNode(trace, nodesByKey, modes, key, new Map(consumed.map((k) => [k, ""])));
120
+ const facts = {
121
+ node_ref: packetShape.node_ref,
122
+ format_clusters: packetShape.format_clusters,
123
+ value_shape_seams: packetShape.value_shape_seams,
124
+ };
125
+ const leafCount = leafCounts.get(key);
126
+ if (leafCount === undefined) {
127
+ throw new Error(`synthesize-cert-sampler: no leaf count for ${key} (impossible)`);
128
+ }
129
+ out.push({
130
+ node_key: key,
131
+ node_ref: packetShape.node_ref,
132
+ sheet_index: sheetIndex,
133
+ stratum: { seam: facts.value_shape_seams.length > 0, merge: mode === "accumulating" },
134
+ subtree_leaf_count: leafCount,
135
+ deterministic_facts: facts,
136
+ deterministic_facts_sha256: synthesizeCertDeterministicFactsSha256(facts),
137
+ });
138
+ }
139
+ out.sort((a, b) => cmpStr(a.node_key, b.node_key));
140
+ return out;
141
+ }
142
+ function buildInputId(fixtureId, c) {
143
+ const r = c.node_ref;
144
+ return `${fixtureId.slice(0, 8)}-s${c.sheet_index}-c${r.column_index}-r${r.row_start}_${r.row_end}`;
145
+ }
146
+ /** Nearest unselected pool index for a selected index (scan outward; ties →
147
+ * earlier index). Returns -1 when every pool member was selected. */
148
+ function nearestUnselectedIndex(selected, index, poolSize) {
149
+ for (let dist = 1; dist < poolSize; dist += 1) {
150
+ const lo = index - dist;
151
+ if (lo >= 0 && !selected.has(lo))
152
+ return lo;
153
+ const hi = index + dist;
154
+ if (hi < poolSize && !selected.has(hi))
155
+ return hi;
156
+ }
157
+ return -1;
158
+ }
159
+ export function sampleStratifiedManifest(fixtures, opts) {
160
+ const perStratumK = opts?.perStratumK ?? SYNTHESIZE_CERT_DEFAULT_PER_STRATUM_K;
161
+ const declaredReps = opts?.declaredReps ?? SYNTHESIZE_CERT_FLOORS.minRepsPerFixtureArm;
162
+ if (!Number.isSafeInteger(perStratumK) || perStratumK < 1) {
163
+ throw new Error(`synthesize-cert-sampler: perStratumK must be a positive safe integer, got ${perStratumK}`);
164
+ }
165
+ if (!Number.isSafeInteger(declaredReps) || declaredReps < 1) {
166
+ throw new Error(`synthesize-cert-sampler: declaredReps must be a positive safe integer, got ${declaredReps}`);
167
+ }
168
+ const manifest = [];
169
+ const provenance = [];
170
+ const seenFixtureIds = new Set();
171
+ for (const fixture of fixtures) {
172
+ // fixture_id must be the workbook's real content sha (64-hex): the input_id
173
+ // prefix and the whitespace-free id guarantee both derive from it.
174
+ if (!/^[0-9a-f]{64}$/.test(fixture.fixture_id)) {
175
+ throw new Error(`synthesize-cert-sampler: fixture_id must be a lowercase sha256 hex (workbook content identity), got '${fixture.fixture_id}'`);
176
+ }
177
+ if (seenFixtureIds.has(fixture.fixture_id)) {
178
+ throw new Error(`synthesize-cert-sampler: duplicate fixture_id ${fixture.fixture_id}`);
179
+ }
180
+ seenFixtureIds.add(fixture.fixture_id);
181
+ // Candidate integrity — a candidate whose declared identity lies is a
182
+ // harness bug, fail-closed (§3 정직 선정 rests on these being real).
183
+ const seenNodeKeys = new Set();
184
+ for (const c of fixture.candidates) {
185
+ if (reduceNodeKey(c.node_ref) !== c.node_key) {
186
+ throw new Error(`synthesize-cert-sampler: candidate node_key '${c.node_key}' != reduceNodeKey(node_ref) '${reduceNodeKey(c.node_ref)}'`);
187
+ }
188
+ if (seenNodeKeys.has(c.node_key)) {
189
+ throw new Error(`synthesize-cert-sampler: duplicate candidate node_key '${c.node_key}' in fixture ${fixture.fixture_id}`);
190
+ }
191
+ seenNodeKeys.add(c.node_key);
192
+ if (synthesizeCertDeterministicFactsSha256(c.deterministic_facts) !== c.deterministic_facts_sha256) {
193
+ throw new Error(`synthesize-cert-sampler: candidate ${c.node_key} deterministic_facts_sha256 does not recompute from its facts`);
194
+ }
195
+ if (c.stratum.seam !== (c.deterministic_facts.value_shape_seams.length > 0)) {
196
+ throw new Error(`synthesize-cert-sampler: candidate ${c.node_key} seam flag disagrees with its value_shape_seams`);
197
+ }
198
+ if (!Number.isSafeInteger(c.sheet_index) || c.sheet_index < 0) {
199
+ throw new Error(`synthesize-cert-sampler: candidate ${c.node_key} sheet_index invalid`);
200
+ }
201
+ if (!Number.isSafeInteger(c.subtree_leaf_count) || c.subtree_leaf_count < 1) {
202
+ throw new Error(`synthesize-cert-sampler: candidate ${c.node_key} subtree_leaf_count invalid`);
203
+ }
204
+ }
205
+ // Group by possessed stratum, stable-key order (§3-1).
206
+ const pools = new Map();
207
+ for (const c of [...fixture.candidates].sort((a, b) => cmpStr(a.node_key, b.node_key))) {
208
+ const k = stratumKey(c.stratum);
209
+ const pool = pools.get(k) ?? [];
210
+ pool.push(c);
211
+ pools.set(k, pool);
212
+ }
213
+ for (const [poolKey, pool] of [...pools.entries()].sort((a, b) => cmpStr(a[0], b[0]))) {
214
+ const stratum = pool[0].stratum;
215
+ const seed = sha256Hex(`${fixture.fixture_id}|${poolKey}|${SYNTHESIZE_CERT_SAMPLER_VERSION}`);
216
+ const isMerge = stratum.merge;
217
+ // merge: order by subtree leaf count ascending (disclosed content-blind
218
+ // cost bias, §3-2), seed tie-break, node_key as the final total order.
219
+ const ordered = isMerge
220
+ ? [...pool].sort((a, b) => a.subtree_leaf_count - b.subtree_leaf_count ||
221
+ cmpStr(sha256Hex(`${seed}|${a.node_key}`), sha256Hex(`${seed}|${b.node_key}`)) ||
222
+ cmpStr(a.node_key, b.node_key))
223
+ : pool; // leaf strata: stable-key order (already sorted).
224
+ const takeAll = ordered.length <= perStratumK;
225
+ let stride = null;
226
+ let pickedIndices;
227
+ if (takeAll) {
228
+ pickedIndices = ordered.map((_, i) => i);
229
+ }
230
+ else if (isMerge) {
231
+ pickedIndices = Array.from({ length: perStratumK }, (_, i) => i); // smallest-K cost bound
232
+ }
233
+ else {
234
+ // Seeded stride over the stable-key order: spreads picks across the
235
+ // pool instead of clustering at its head. offset+ (K-1)·stride ≤ N-1.
236
+ stride = Math.floor(ordered.length / perStratumK);
237
+ const offset = parseInt(seed.slice(0, 12), 16) % stride;
238
+ pickedIndices = Array.from({ length: perStratumK }, (_, i) => offset + i * stride);
239
+ }
240
+ const selectedSet = new Set(pickedIndices);
241
+ pickedIndices.forEach((poolIndex, rank) => {
242
+ const c = ordered[poolIndex];
243
+ const nearest = nearestUnselectedIndex(selectedSet, poolIndex, ordered.length);
244
+ manifest.push({
245
+ fixture_id: fixture.fixture_id,
246
+ input_id: buildInputId(fixture.fixture_id, c),
247
+ node_key: c.node_key,
248
+ sheet_index: c.sheet_index,
249
+ stratum: c.stratum,
250
+ subtree_leaf_count: c.subtree_leaf_count,
251
+ deterministic_facts: c.deterministic_facts,
252
+ deterministic_facts_sha256: c.deterministic_facts_sha256,
253
+ sampling_rank: rank,
254
+ nearest_unselected_id: nearest === -1 ? null : buildInputId(fixture.fixture_id, ordered[nearest]),
255
+ });
256
+ });
257
+ provenance.push({
258
+ fixture_id: fixture.fixture_id,
259
+ stratum,
260
+ pool_size: ordered.length,
261
+ selected_count: pickedIndices.length,
262
+ seed,
263
+ ordering: isMerge ? "subtree_leaf_count_asc" : "stable_key_stride",
264
+ stride,
265
+ });
266
+ }
267
+ }
268
+ // §3 E: whitespace-free + globally unique input_ids, asserted before freeze.
269
+ const seenInputIds = new Set();
270
+ for (const entry of manifest) {
271
+ if (!/^\S+$/.test(entry.input_id)) {
272
+ throw new Error(`synthesize-cert-sampler: input_id '${entry.input_id}' contains whitespace or is empty`);
273
+ }
274
+ if (seenInputIds.has(entry.input_id)) {
275
+ throw new Error(`synthesize-cert-sampler: input_id collision '${entry.input_id}' — ids must be globally unique`);
276
+ }
277
+ seenInputIds.add(entry.input_id);
278
+ }
279
+ // Pre-spend floor gate (§3 sequencing): deterministic-facts scope — the
280
+ // input_sha256 slot carries deterministic_facts_sha256 because the frozen
281
+ // packet sha does not exist yet; the floor lint never reads the sha.
282
+ const floorViolations = synthesizeCertManifestFloorViolations({
283
+ inputManifest: manifest.map((e) => ({
284
+ fixture_id: e.fixture_id,
285
+ input_id: e.input_id,
286
+ input_sha256: e.deterministic_facts_sha256,
287
+ stratum: e.stratum,
288
+ })),
289
+ declaredReps,
290
+ });
291
+ // Pre-spend prediction of the record validator's stratum_global_floor: every
292
+ // 2×2 stratum must be floor-reachable (inputs × reps ≥ decisive floor) in at
293
+ // least ONE fixture — a manifest missing a stratum entirely passes the
294
+ // possessed-strata lint above but is guaranteed to fail post-spend.
295
+ const selectedByFixtureStratum = new Map();
296
+ for (const e of manifest) {
297
+ const k = `${e.fixture_id} ${stratumKey(e.stratum)}`;
298
+ selectedByFixtureStratum.set(k, (selectedByFixtureStratum.get(k) ?? 0) + 1);
299
+ }
300
+ for (const combo of [
301
+ { seam: false, merge: false },
302
+ { seam: false, merge: true },
303
+ { seam: true, merge: false },
304
+ { seam: true, merge: true },
305
+ ]) {
306
+ const reachable = fixtures.some((fx) => (selectedByFixtureStratum.get(`${fx.fixture_id} ${stratumKey(combo)}`) ?? 0) * declaredReps >=
307
+ SYNTHESIZE_CERT_FLOORS.minDecisivePerStratumArm);
308
+ if (!reachable) {
309
+ floorViolations.push({
310
+ code: "stratum_global_floor",
311
+ message: `stratum ${stratumKey(combo)} is floor-reachable in no fixture (inputs x reps < ${SYNTHESIZE_CERT_FLOORS.minDecisivePerStratumArm} everywhere) — the record WILL fail stratum_global_floor; do not spend`,
312
+ subject_id: null,
313
+ });
314
+ }
315
+ }
316
+ const manifestIdentitySha256 = sha256Hex(JSON.stringify([...manifest]
317
+ .sort((a, b) => cmpStr(a.input_id, b.input_id))
318
+ .map((e) => ({
319
+ fixture_id: e.fixture_id,
320
+ input_id: e.input_id,
321
+ node_key: e.node_key,
322
+ stratum: { seam: e.stratum.seam, merge: e.stratum.merge },
323
+ deterministic_facts_sha256: e.deterministic_facts_sha256,
324
+ sampling_rank: e.sampling_rank,
325
+ nearest_unselected_id: e.nearest_unselected_id,
326
+ }))));
327
+ return {
328
+ manifest,
329
+ provenance,
330
+ floor_violations: floorViolations,
331
+ manifest_identity_sha256: manifestIdentitySha256,
332
+ sampler_version: SYNTHESIZE_CERT_SAMPLER_VERSION,
333
+ per_stratum_k: perStratumK,
334
+ declared_reps: declaredReps,
335
+ };
336
+ }
@@ -29,6 +29,7 @@
29
29
  * layer; populating it correctly is deferred to P4b, when real multi-effort
30
30
  * sweep data exists to verify the per-stage unit-telemetry attribution.
31
31
  */
32
+ import { modelProviderFromRuntimeProvider, profileDerivedRouteIdentity, routeHintMatches, routeToken, witnessedReconstructRouteIdentity, worstRouteCompleteness, } from "./route-identity.js";
32
33
  import { reviewRunGateSignal } from "./effort-calibration-review.js";
33
34
  import { reconstructRunGateSignal } from "./effort-calibration-reconstruct.js";
34
35
  const UNJUDGED = { passed: null, qualityScore: null };
@@ -189,3 +190,137 @@ export function ingestReconstructReport(report, tag) {
189
190
  }
190
191
  return out;
191
192
  }
193
+ // ---------------------------------------------------------------------------
194
+ // Route derivation (S1.3 — CLI consumer of the witnessed route identity)
195
+ // ---------------------------------------------------------------------------
196
+ /**
197
+ * Derive one reconstruct run's RouteIdentity. Prefers the harness-surfaced
198
+ * witnessed `route_identity` (S1.1/S1.2). A legacy report that predates route
199
+ * witnessing carries only the provider-only `provider_route`; that string is
200
+ * degraded through the witnessed constructor (adapter unknown → route_completeness
201
+ * `provider_only`, or `under_determined` for an unknown brand) so the CLI records
202
+ * an honest, degraded identity instead of failing (design §10). Returns null when
203
+ * neither is present (no route evidence on this run).
204
+ */
205
+ export function reconstructRunRouteIdentity(run) {
206
+ const witnessed = run.metadata?.route_identity;
207
+ if (witnessed)
208
+ return witnessed;
209
+ const providerRoute = run.metadata?.provider_route;
210
+ if (providerRoute == null)
211
+ return null;
212
+ return witnessedReconstructRouteIdentity({
213
+ provider: providerRoute,
214
+ executionAdapter: null,
215
+ declaredBillingMode: null,
216
+ effectiveBaseUrl: null,
217
+ });
218
+ }
219
+ /**
220
+ * Derive one review run's RouteIdentity from its (already structurally rich)
221
+ * runtime_route projection. Review has no result-level witness — the route is
222
+ * resolved from settings/profile — so provenance is profile_derived (design §4).
223
+ * A legacy report that carries only the provider-level `runtime_provider` token
224
+ * (no `model_provider`) falls back to the brand mapping, degrading to
225
+ * provider_only — symmetric with the reconstruct legacy path (design §10) rather
226
+ * than discarding the recoverable brand. Returns null when the run carries no
227
+ * route projection at all.
228
+ */
229
+ export function reviewRunRouteIdentity(run) {
230
+ const route = run.review_profile?.runtime_route;
231
+ if (!route)
232
+ return null;
233
+ return profileDerivedRouteIdentity({
234
+ executionAdapter: route.execution_adapter ?? null,
235
+ modelProvider: route.model_provider ?? modelProviderFromRuntimeProvider(route.runtime_provider),
236
+ billingMode: route.billing_mode ?? null,
237
+ effectiveBaseUrl: route.base_url ?? null,
238
+ realization: route.artifact_generation_realization ?? null,
239
+ });
240
+ }
241
+ /**
242
+ * The route identities of the reconstruct runs that ACTUALLY feed the frontier —
243
+ * the same runs `ingestReconstructReport` retains (telemetry shows the swept
244
+ * stage ran at the point). Route evidence is collected from these retained
245
+ * samples only, never from runs whose applied effort / judge mismatch dropped
246
+ * them, so a discarded wrong-route run can't taint route_completeness or add a
247
+ * phantom route (finding: derive route evidence from retained samples). A
248
+ * retained run that carries no route witness contributes null (kept, not
249
+ * dropped, so the summary can downgrade for it). Failed runs carry no telemetry
250
+ * and assert no route, so they are excluded.
251
+ */
252
+ export function reconstructRetainedRouteIdentities(report, tag) {
253
+ const point = tag ?? deriveReconstructTag(report);
254
+ if (!point)
255
+ return [];
256
+ const out = [];
257
+ for (const run of report.runs ?? []) {
258
+ if (!appliedEffortMatches(run, point.stage, point.effort))
259
+ continue;
260
+ out.push(retainedRunRouteIdentity(run, point));
261
+ }
262
+ return out;
263
+ }
264
+ /**
265
+ * The route identity that backs a retained sample at `point`. For an author
266
+ * point the run-level metadata route (= the first author unit) is the swept
267
+ * unit's route. For a JUDGE point the swept unit is the answer_support_judgment
268
+ * call, which may run on a different route than the author, so its OWN
269
+ * route_identity is read; a judge unit that ran without a route witness yields
270
+ * null (the route is undetermined, never silently the author's).
271
+ */
272
+ function retainedRunRouteIdentity(run, point) {
273
+ if (point.stage === "judge") {
274
+ const judgeUnit = (run.units ?? []).find((u) => u.step_id === JUDGE_STEP_ID &&
275
+ u.effort === point.effort &&
276
+ (u.llm_call_count ?? 0) >= 1);
277
+ return judgeUnit?.route_identity ?? null;
278
+ }
279
+ return reconstructRunRouteIdentity(run);
280
+ }
281
+ /**
282
+ * The route identities of the review runs that feed the frontier — the gated
283
+ * (completed) candidate and unit-sweep baseline runs, mirroring
284
+ * `ingestReviewReport`'s attribution. Failed runs carry no quality gate or route
285
+ * telemetry, so only gated runs are sampled. A gated run missing its route
286
+ * projection contributes null (so the summary can downgrade for it).
287
+ */
288
+ export function reviewRetainedRouteIdentities(report) {
289
+ const out = [];
290
+ for (const run of report.runs ?? []) {
291
+ if (!run.semantic_quality_gate)
292
+ continue;
293
+ const isCandidate = Boolean(run.varied_unit_id && run.varied_effort);
294
+ const isBaseline = Boolean(run.base_effort && run.case_id?.startsWith(UNIT_SWEEP_BASE_PREFIX));
295
+ if (!isCandidate && !isBaseline)
296
+ continue;
297
+ out.push(reviewRunRouteIdentity(run));
298
+ }
299
+ return out;
300
+ }
301
+ export function summarizeDerivedRoutes(derived, declaredRouteHint) {
302
+ // A null among contributing runs is a retained sample with no route witness —
303
+ // the profile cannot claim a complete route while including it (design §10).
304
+ const hasMissingWitness = derived.some((id) => id === null);
305
+ const byKey = new Map();
306
+ for (const identity of derived) {
307
+ if (!identity)
308
+ continue;
309
+ const key = JSON.stringify([routeToken(identity), identity.effective_base_url ?? null]);
310
+ if (!byKey.has(key))
311
+ byKey.set(key, identity);
312
+ }
313
+ const identities = [...byKey.values()];
314
+ const tokens = identities.map((id) => id.effective_base_url
315
+ ? `${routeToken(id)}@${id.effective_base_url}`
316
+ : routeToken(id));
317
+ const completeness = identities.length === 0 || hasMissingWitness
318
+ ? "under_determined"
319
+ : worstRouteCompleteness(identities.map((id) => id.route_completeness));
320
+ return {
321
+ identities,
322
+ tokens,
323
+ completeness,
324
+ hintCorroborated: identities.some((id) => routeHintMatches(declaredRouteHint, id)),
325
+ };
326
+ }