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
@@ -7,9 +7,12 @@
7
7
  * provider calls) and by the G7 committed-config guard (which covers every
8
8
  * committed seat). Review-side runtime enforcement is a noted follow-up, so the
9
9
  * runtime gate today is wired on the reconstruct live path only. An unlisted
10
- * (provider, model) is rejected fail-loud at those gate points. Settings
11
- * resolution itself stays a PURE projection and does NOT apply this gate (see
12
- * settings-chain). The
10
+ * (provider, model) is rejected fail-loud at those gate points. The only
11
+ * runtime-owned exception is the B7 bench-candidate option passed explicitly by
12
+ * a benchmark harness: it can rescue an UNREGISTERED pair only at an exact
13
+ * allowlisted route path, and it never rescues registered role mismatches.
14
+ * Settings resolution itself stays a PURE projection and does NOT apply this
15
+ * gate (see settings-chain). The
13
16
  * authority ships with the onto install (it is located from the install root,
14
17
  * like core-lens-registry.yaml), so it is always present. The registry is data
15
18
  * (human-curated, evidence-cited); this module loads it and provides the
@@ -29,22 +32,154 @@ import { fileURLToPath } from "node:url";
29
32
  import { parse as parseYaml } from "yaml";
30
33
  import { z } from "zod";
31
34
  import { resolveInstallationPath } from "./installation-paths.js";
35
+ import { REVIEW_EXECUTION_UNIT_IDS } from "./review-execution-units.js";
32
36
  export const SUPPORTED_MODELS_AUTHORITY_PATH = ".onto/authority/supported-models.yaml";
37
+ /**
38
+ * Role vocabulary — the sealed 6-role set, each grounded in an existing code
39
+ * seat (INV-MODEL-1 role-aware design §2.1; `review` added by the 2026-07-11
40
+ * review-role registration design): `author` (semantic_author actor, certified
41
+ * by golden full-pipeline completion), `semantic_map_synthesize` /
42
+ * `semantic_map_verify` (the semantic-map capability pair), `answer_support_judge`
43
+ * (the judge adoption dispatch), `confirmation_provider` (confirmation actor),
44
+ * `review` (the review execution seats — actors teamlead/lens/synthesize and
45
+ * their unit overrides). The vocabulary is sealed here; which roles are
46
+ * LISTABLE in a registry entry is governed separately by {@link CONTRACTED_ROLES}.
47
+ */
48
+ export const SUPPORTED_MODEL_ROLES = [
49
+ "author",
50
+ "semantic_map_synthesize",
51
+ "semantic_map_verify",
52
+ "answer_support_judge",
53
+ "confirmation_provider",
54
+ "review",
55
+ ];
56
+ /**
57
+ * Roles with a DEFINED evidence contract — the only roles an entry may list in
58
+ * `roles`. Listing a vocabulary role without a contract fails registry load
59
+ * (fail-closed): certification must never outrun its evidence definition.
60
+ * Growing this set means defining that role's evidence contract and carries an
61
+ * INVARIANT-CHANGE: INV-MODEL-1 marker (role authority change).
62
+ */
63
+ export const CONTRACTED_ROLES = [
64
+ "author",
65
+ "semantic_map_synthesize",
66
+ // Evidence contract: review-cert/v2 (2026-07-11 review-role registration
67
+ // design §4) — per-check pass-rate vs a contemporaneous baseline arm,
68
+ // absolute core-check floors, pinned check universe, R7 human curation.
69
+ "review",
70
+ ];
33
71
  const SupportedModelEntrySchema = z
34
72
  .object({
35
73
  provider: z.string().min(1),
36
74
  model: z.string().min(1),
37
75
  verified_at: z.string().min(1),
38
76
  benchmark_evidence_refs: z.array(z.string().min(1)).min(1),
77
+ // Role-restricted certification (INV-MODEL-1 role-aware design §2.2).
78
+ // ABSENT = grandfathered full-route allowance: the entry predates the role
79
+ // dimension and keeps its flat-registry permissions (an owner
80
+ // backward-compatibility decision, NOT a stage-traversal evidence claim).
81
+ // PRESENT = evidence-contracted certification: the model is valid ONLY at
82
+ // dispatches whose required role is listed. Values are further restricted
83
+ // to CONTRACTED_ROLES at load (see assertContractedRoles).
84
+ roles: z.array(z.enum(SUPPORTED_MODEL_ROLES)).min(1).optional(),
39
85
  notes: z.string().optional(),
86
+ // Active (provider, model) context window in tokens — the SSOT the
87
+ // reconstruct projection-budget helper (deriveDocumentExcerptProjectionBudget)
88
+ // reads to scale a single text document's seed-stage excerpt to the model.
89
+ // Optional: an entry without it falls back to the static projection FLOOR
90
+ // (model-agnostic, no regression). INV-MODEL-1; the window field is
91
+ // G4-protected (a change requires an INVARIANT-CHANGE: INV-MODEL-1 marker).
92
+ context_window_tokens: z.number().int().positive().optional(),
93
+ // Citation for context_window_tokens, kept SEPARATE from
94
+ // benchmark_evidence_refs: a context window is a published model spec, not a
95
+ // benchmark result. Required whenever context_window_tokens is present so a
96
+ // window value is never unsourced (C7).
97
+ context_window_provenance: z.string().min(1).optional(),
98
+ // Provider-published maximum output ceiling. Reconstruct direct-API
99
+ // headroom preflight consumes this value before any provider call.
100
+ // INV-MODEL-1 / G4 protected, independently provenance-backed.
101
+ max_output_tokens: z.number().int().positive().safe().optional(),
102
+ max_output_tokens_provenance: z.string().min(1).optional(),
103
+ })
104
+ .strict()
105
+ .refine((entry) => entry.context_window_tokens === undefined ||
106
+ entry.context_window_provenance !== undefined, {
107
+ message: "context_window_tokens requires context_window_provenance (a window value must cite its source)",
108
+ path: ["context_window_provenance"],
40
109
  })
41
- .strict();
110
+ .refine((entry) => entry.max_output_tokens === undefined ||
111
+ entry.max_output_tokens_provenance !== undefined, {
112
+ message: "max_output_tokens requires max_output_tokens_provenance (an output limit must cite its source)",
113
+ path: ["max_output_tokens_provenance"],
114
+ });
42
115
  const SupportedModelRegistrySchema = z
43
116
  .object({
44
117
  schema_version: z.string().min(1),
45
118
  supported_models: z.array(SupportedModelEntrySchema),
46
119
  })
47
- .strict();
120
+ .strict()
121
+ .superRefine((registry, ctx) => {
122
+ const seen = new Set();
123
+ registry.supported_models.forEach((entry, index) => {
124
+ const key = `${entry.provider}\u0000${entry.model}`;
125
+ if (seen.has(key)) {
126
+ ctx.addIssue({
127
+ code: "custom",
128
+ path: ["supported_models", index],
129
+ message: `duplicate supported model pair: ${entry.provider}/${entry.model}`,
130
+ });
131
+ }
132
+ seen.add(key);
133
+ });
134
+ });
135
+ export const RECONSTRUCT_SEMANTIC_MAP_SYNTHESIZE_LLM_ROUTE_PATH = "reconstruct.execution.actors.semantic_map_synthesize.llm";
136
+ export const RECONSTRUCT_DISPATCH_FALLBACK_LLM_ROUTE_PATH = "reconstruct.execution.dispatch_fallback.llm";
137
+ /**
138
+ * Single owner of dispatch → required-role derivation (design §2.3).
139
+ * Settings seats without a finer mapping require `author` — the strongest
140
+ * certification (golden full-pipeline completion) — so a role-restricted entry
141
+ * can never occupy an unmapped seat (fail-closed default).
142
+ */
143
+ /** Bounded matcher for the review execution seats (review-role design §3):
144
+ * the three actor seats (nested and legacy settings forms — the same fixed
145
+ * actor names the settings schema accepts) and unit overrides restricted to
146
+ * the KNOWN unit-id vocabulary. The bound is load-bearing: G7 walks raw
147
+ * parsed YAML without the strict zod layer, so an unknown actor or unit key
148
+ * DOES reach the resolver — it must fall through to the fail-closed `author`
149
+ * default, never be adopted as a review seat. Salvage transcription
150
+ * (`...retry.salvage.transcription_llm`) deliberately does not match. */
151
+ const REVIEW_ACTOR_SEAT_PATH = /^review\.execution\.(?:actors\.)?(?:teamlead|lens|synthesize)\.llm$/;
152
+ const REVIEW_UNIT_SEAT_PATH = /^review\.execution\.units\.([^.[]+)\.llm$/;
153
+ function isReviewSeatPath(path) {
154
+ if (REVIEW_ACTOR_SEAT_PATH.test(path))
155
+ return true;
156
+ const unit = REVIEW_UNIT_SEAT_PATH.exec(path);
157
+ return unit !== null &&
158
+ REVIEW_EXECUTION_UNIT_IDS.includes(unit[1]);
159
+ }
160
+ export function requiredSupportedModelRoleForDispatch(dispatch) {
161
+ if (dispatch.kind === "request_judge")
162
+ return "answer_support_judge";
163
+ if (dispatch.kind === "semantic_map_synthesize")
164
+ return "semantic_map_synthesize";
165
+ if (dispatch.kind === "semantic_map_verify")
166
+ return "semantic_map_verify";
167
+ switch (dispatch.path) {
168
+ case "reconstruct.execution.actors.semantic_author.llm":
169
+ return "author";
170
+ case "reconstruct.execution.actors.confirmation_provider.llm":
171
+ return "confirmation_provider";
172
+ case RECONSTRUCT_SEMANTIC_MAP_SYNTHESIZE_LLM_ROUTE_PATH:
173
+ return "semantic_map_synthesize";
174
+ default:
175
+ if (isReviewSeatPath(dispatch.path))
176
+ return "review";
177
+ // Salvage transcription, top-level llm, unknown review actor/unit keys,
178
+ // and any future unmapped path: require the strongest certification
179
+ // (fail-closed).
180
+ return "author";
181
+ }
182
+ }
48
183
  /** Locates the authority registry shipped with the install by walking up from
49
184
  * this module to the install root's `.onto/authority/` — the same resolver
50
185
  * core-lens-registry.yaml uses. The authority is an install resource, not a
@@ -67,12 +202,12 @@ function findSupportedModelsAuthorityPath() {
67
202
  "This authority file is the source of truth for selectable models and " +
68
203
  "must ship with the onto installation.");
69
204
  }
70
- /** Loads and shape-validates the supported-model registry from the install root.
71
- * Strict: the authority always ships, so a missing or malformed file is an
72
- * installation error, not a skip. */
73
- export function loadSupportedModelRegistry() {
74
- const filePath = findSupportedModelsAuthorityPath();
75
- const parsed = SupportedModelRegistrySchema.safeParse(parseYaml(fs.readFileSync(filePath, "utf8")));
205
+ /** Shape-validates raw (already-parsed) registry data. The one validation path
206
+ * both {@link loadSupportedModelRegistry} (disk) and tests use, so the schema
207
+ * contract — strict shape, positive-int `context_window_tokens`, a window value
208
+ * only with its provenance — is exercised without touching the install file. */
209
+ export function parseSupportedModelRegistry(raw) {
210
+ const parsed = SupportedModelRegistrySchema.safeParse(raw);
76
211
  if (!parsed.success) {
77
212
  throw new Error(`Malformed supported-model registry at ${SUPPORTED_MODELS_AUTHORITY_PATH}: ` +
78
213
  parsed.error.issues
@@ -80,8 +215,35 @@ export function loadSupportedModelRegistry() {
80
215
  .join("; "));
81
216
  }
82
217
  assertRepoRelativeEvidenceRefs(parsed.data);
218
+ assertContractedRoles(parsed.data);
83
219
  return parsed.data;
84
220
  }
221
+ /** Fail-closed listable-role check: every role an entry LISTS must have a
222
+ * defined evidence contract ({@link CONTRACTED_ROLES}). The zod enum admits the
223
+ * full sealed vocabulary so role names never drift, but a certification without
224
+ * an evidence contract must not exist — reject at load, not at gate time. */
225
+ export function assertContractedRoles(registry) {
226
+ const bad = [];
227
+ for (const entry of registry.supported_models) {
228
+ for (const role of entry.roles ?? []) {
229
+ if (!CONTRACTED_ROLES.includes(role)) {
230
+ bad.push(`${entry.provider}/${entry.model}: ${role}`);
231
+ }
232
+ }
233
+ }
234
+ if (bad.length > 0) {
235
+ throw new Error(`Malformed supported-model registry at ${SUPPORTED_MODELS_AUTHORITY_PATH}: ` +
236
+ "roles lists role(s) without a defined evidence contract (listable roles: " +
237
+ `${CONTRACTED_ROLES.join(", ")}):\n${bad.map((b) => ` - ${b}`).join("\n")}`);
238
+ }
239
+ }
240
+ /** Loads and shape-validates the supported-model registry from the install root.
241
+ * Strict: the authority always ships, so a missing or malformed file is an
242
+ * installation error, not a skip. */
243
+ export function loadSupportedModelRegistry() {
244
+ const filePath = findSupportedModelsAuthorityPath();
245
+ return parseSupportedModelRegistry(parseYaml(fs.readFileSync(filePath, "utf8")));
246
+ }
85
247
  /** Validates that every `benchmark_evidence_refs` entry is a repo-relative path
86
248
  * (not absolute, no `..` escape), so the cited evidence is an in-repo auditable
87
249
  * artifact rather than an out-of-tree file. Pure string check — safe at runtime
@@ -147,13 +309,31 @@ export function collectModelSelections(settings) {
147
309
  if (value === null || typeof value !== "object")
148
310
  return;
149
311
  const record = value;
312
+ if (trail === RECONSTRUCT_DISPATCH_FALLBACK_LLM_ROUTE_PATH) {
313
+ const provider = typeof record.provider === "string" ? record.provider : undefined;
314
+ const model = typeof record.model === "string" ? record.model : undefined;
315
+ for (const kind of ["semantic_map_synthesize", "semantic_map_verify"]) {
316
+ out.push({
317
+ provider,
318
+ model,
319
+ path: `${trail}#${kind}`,
320
+ requiredRole: requiredSupportedModelRoleForDispatch({ kind }),
321
+ });
322
+ }
323
+ return;
324
+ }
150
325
  if (typeof record.model === "string" || typeof record.provider === "string") {
326
+ const path = trail || "(root)";
151
327
  out.push({
152
328
  provider: typeof record.provider === "string"
153
329
  ? record.provider
154
330
  : undefined,
155
331
  model: typeof record.model === "string" ? record.model : undefined,
156
- path: trail || "(root)",
332
+ path,
333
+ requiredRole: requiredSupportedModelRoleForDispatch({
334
+ kind: "settings_path",
335
+ path,
336
+ }),
157
337
  });
158
338
  }
159
339
  for (const [key, child] of Object.entries(record)) {
@@ -163,37 +343,110 @@ export function collectModelSelections(settings) {
163
343
  visit(settings, "");
164
344
  return out;
165
345
  }
346
+ /** Canonical settings-to-dispatch collector consumed by runtime and G7. */
347
+ export const collectSupportedModelDispatches = collectModelSelections;
348
+ /** Does `entry` cover `role`? Absent `roles` = grandfathered full-route
349
+ * allowance (covers every role — flat-registry backward compatibility);
350
+ * present = covers exactly the listed roles. */
351
+ function entryCoversRole(entry, role) {
352
+ return entry.roles === undefined || entry.roles.includes(role);
353
+ }
354
+ function supportedModelEntryFor(registry, provider, model) {
355
+ return registry.supported_models.find((entry) => entry.provider === provider && entry.model === model);
356
+ }
357
+ export function supportedModelMaxOutputTokens(registry, provider, model) {
358
+ return supportedModelEntryFor(registry, provider, model)?.max_output_tokens;
359
+ }
360
+ function isAllowedUnregisteredBenchCandidate(route, registry, options) {
361
+ if (route.provider === undefined || route.model === undefined)
362
+ return false;
363
+ if (supportedModelEntryFor(registry, route.provider, route.model) !== undefined) {
364
+ return false;
365
+ }
366
+ return (options?.benchCandidates ?? []).some((candidate) => candidate.provider === route.provider &&
367
+ candidate.model === route.model &&
368
+ candidate.allowedRoutePaths.includes(route.path));
369
+ }
166
370
  /** Non-throwing membership check: is (provider, model) a benchmark-verified
167
- * supported route? Reuses the same verified-pair set as
371
+ * supported route FOR THIS DISPATCH? Reuses the same registry as
168
372
  * {@link assertSupportedModelRoutes}, but returns a boolean so opt-in callers
169
373
  * (e.g. the answer-support judge per-stage model override) can DEGRADE to the
170
374
  * inherited config when an override is unsupported, instead of failing the run.
171
- * An unresolved provider or model is not verified. */
172
- export function isSupportedModelRoute(provider, model, registry) {
375
+ * The dispatch parameter is REQUIRED (no default) so every caller names its
376
+ * dispatch and a role-restricted entry can never be adopted at a dispatch it
377
+ * is not certified for (F6-b leak closure). An unresolved provider or model is
378
+ * not verified. */
379
+ export function isSupportedModelRoute(provider, model, registry, dispatch) {
173
380
  if (provider === undefined || model === undefined)
174
381
  return false;
175
- return registry.supported_models.some((entry) => entry.provider === provider && entry.model === model);
382
+ const role = requiredSupportedModelRoleForDispatch(dispatch);
383
+ const entry = supportedModelEntryFor(registry, provider, model);
384
+ return entry !== undefined && entryCoversRole(entry, role);
176
385
  }
177
386
  /** Throws if any effective route is not a benchmark-verified (provider, model)
178
- * pair. A route whose effective provider OR model could not be resolved is
179
- * rejected (fail-loud) rather than leniently accepted — the route must resolve
180
- * to a verified pair, otherwise the runtime would dispatch a route the gate
181
- * cannot verify. */
182
- export function assertSupportedModelRoutes(routes, registry) {
183
- const verified = new Set(registry.supported_models.map((entry) => `${entry.provider} ${entry.model}`));
184
- const violations = routes.filter((route) => route.provider === undefined ||
185
- route.model === undefined ||
186
- !verified.has(`${route.provider} ${route.model}`));
387
+ * pair CERTIFIED for the route's required role. A route whose effective
388
+ * provider OR model could not be resolved is rejected (fail-loud) rather than
389
+ * leniently accepted the route must resolve to a verified pair, otherwise
390
+ * the runtime would dispatch a route the gate cannot verify. A pair that is
391
+ * registered but role-restricted (entry.roles present) is rejected at any
392
+ * route whose requiredRole it does not list. */
393
+ export function assertSupportedModelRoutes(routes, registry, options) {
394
+ const violations = routes.filter((route) => {
395
+ if (route.provider === undefined || route.model === undefined)
396
+ return true;
397
+ const entry = supportedModelEntryFor(registry, route.provider, route.model);
398
+ if (entry !== undefined)
399
+ return !entryCoversRole(entry, route.requiredRole);
400
+ return !isAllowedUnregisteredBenchCandidate(route, registry, options);
401
+ });
187
402
  if (violations.length === 0)
188
403
  return;
189
404
  const detail = violations
190
- .map((route) => `- ${route.path}: ${route.provider ? `${route.provider}/` : "(unresolved provider)/"}${route.model ?? "(unresolved model)"}`)
405
+ .map((route) => {
406
+ const pair = `${route.provider ? `${route.provider}/` : "(unresolved provider)/"}${route.model ?? "(unresolved model)"}`;
407
+ const entry = route.provider !== undefined && route.model !== undefined
408
+ ? supportedModelEntryFor(registry, route.provider, route.model)
409
+ : undefined;
410
+ const reason = entry?.roles
411
+ ? ` — certified for [${entry.roles.join(", ")}], seat requires ${route.requiredRole}`
412
+ : "";
413
+ return `- ${route.path}: ${pair}${reason}`;
414
+ })
191
415
  .join("\n");
192
416
  const allowed = registry.supported_models
193
- .map((entry) => `${entry.provider}/${entry.model}`)
417
+ .map((entry) => `${entry.provider}/${entry.model}${entry.roles ? ` (roles: ${entry.roles.join(", ")})` : ""}`)
194
418
  .join(", ");
195
419
  throw new Error("settings.json selects model route(s) not verified as supported by benchmark " +
196
420
  `(see ${SUPPORTED_MODELS_AUTHORITY_PATH}):\n${detail}\n` +
197
- `Benchmark-verified selectable models: ${allowed}. Add a model only after ` +
198
- "a benchmark record shows it completing a pipeline run.");
421
+ `Benchmark-verified selectable models: ${allowed}. Add a model (or role) ` +
422
+ "only with the evidence its contract requires (see the registry header).");
423
+ }
424
+ export function assertB4BenchCandidateDispatchAllowed(args) {
425
+ const registry = args.registry ?? loadSupportedModelRegistry();
426
+ const route = {
427
+ provider: args.provider,
428
+ model: args.model,
429
+ path: RECONSTRUCT_SEMANTIC_MAP_SYNTHESIZE_LLM_ROUTE_PATH,
430
+ requiredRole: requiredSupportedModelRoleForDispatch({
431
+ kind: "settings_path",
432
+ path: RECONSTRUCT_SEMANTIC_MAP_SYNTHESIZE_LLM_ROUTE_PATH,
433
+ }),
434
+ };
435
+ try {
436
+ assertSupportedModelRoutes([route], registry);
437
+ return { allowance: "registered_supported", route };
438
+ }
439
+ catch (error) {
440
+ if (supportedModelEntryFor(registry, args.provider, args.model) !== undefined) {
441
+ throw error;
442
+ }
443
+ }
444
+ assertSupportedModelRoutes([route], registry, {
445
+ benchCandidates: [{
446
+ provider: args.provider,
447
+ model: args.model,
448
+ allowedRoutePaths: [RECONSTRUCT_SEMANTIC_MAP_SYNTHESIZE_LLM_ROUTE_PATH],
449
+ }],
450
+ });
451
+ return { allowance: "bench_candidate", route };
199
452
  }
@@ -0,0 +1,197 @@
1
+ /**
2
+ * S7 — `synthesize-cert/v1` record assembly for the B4 bench (design
3
+ * 20260706-b4-r8-harness-design v3 §9/§15.7).
4
+ *
5
+ * Pure projection from the run's real lineage (sampled entries → frozen
6
+ * packets → judgement rows) into the frozen record contract. The declared
7
+ * aggregates are filled by CALLING the validator module's own
8
+ * `computeSynthesizeCertAggregates` (§6.3 parser-ownership: the
9
+ * declared-vs-recomputed comparison then only fails on post-hoc tampering),
10
+ * and the negative_arm block comes from the mutation module's single source —
11
+ * the record can never cite a transform the harness did not run.
12
+ *
13
+ * The assembler shape-validates its own output (schema parse, fail-loud) but
14
+ * does NOT run the semantic recompute: publishing is the orchestrator's gate —
15
+ * `validateSynthesizeCertRecord(record) === []` AND the capsule binding gate
16
+ * (synthesize-cert-capsule.ts) must both pass before anything is persisted as
17
+ * evidence (§9-4). `created_at` is caller-supplied (the harness core stays
18
+ * deterministic and replayable).
19
+ */
20
+ import { buildInputCorruptionV1NegativeArm } from "./synthesize-cert-mutation.js";
21
+ import { computeSynthesizeCertAggregates, parseSynthesizeCertRecord, SYNTHESIZE_CERT_ARMS, SYNTHESIZE_CERT_CONTRACT, SynthesizeCertDispatchConfigSchema, } from "./synthesize-cert-record.js";
22
+ /**
23
+ * record.input_manifest from the sampled entries + their frozen packets.
24
+ * The packet is the sha AUTHORITY (§4 input_sha256 exists only post-freeze);
25
+ * every lineage disagreement (missing packet, fixture or facts drift) throws.
26
+ */
27
+ export function buildSynthesizeCertInputManifest(entries, packets) {
28
+ const packetByInputId = new Map(packets.map((p) => [p.input_id, p]));
29
+ return entries.map((entry) => {
30
+ const packet = packetByInputId.get(entry.input_id);
31
+ if (!packet) {
32
+ throw new Error(`synthesize-cert-assemble: sampled input ${entry.input_id} has no frozen packet`);
33
+ }
34
+ if (packet.fixture_id !== entry.fixture_id ||
35
+ packet.deterministic_facts_sha256 !== entry.deterministic_facts_sha256) {
36
+ throw new Error(`synthesize-cert-assemble: packet lineage for ${entry.input_id} disagrees with its sampled entry (fixture or deterministic facts drift)`);
37
+ }
38
+ return {
39
+ fixture_id: entry.fixture_id,
40
+ input_id: entry.input_id,
41
+ input_sha256: packet.input_sha256,
42
+ stratum: { seam: entry.stratum.seam, merge: entry.stratum.merge },
43
+ };
44
+ });
45
+ }
46
+ /**
47
+ * Pure capture→arm_dispatch projection (effort-witness design §4.5.1-8).
48
+ * Groups the run's capture lines (parsed live-calls.jsonl objects) by the
49
+ * three record arm roles — reference/judge lines are not record arms and are
50
+ * ignored. Line semantics (design §4.5.1-10): a line WITHOUT a `dispatch` key
51
+ * is a pre-field legacy line = NO EVIDENCE, never normalized to "dispatched
52
+ * with no knobs" (the sonnet-5 20260708 legacy lines were in fact effort-low
53
+ * dispatches — that normalization would fabricate a false witness). A line
54
+ * with `dispatch: {}` IS the witness "no knob on the dispatched config".
55
+ * All-legacy → { legacy: true }; mixed legacy/new, an empty arm, a malformed
56
+ * dispatch shape, or within-arm inconsistency → violations (fail-loud).
57
+ * LIMIT: B4 arms are single-call `synthesizeSemanticMapNode` seats today; if
58
+ * a future arm adds base-effort verify calls, per-arm consistency must gain a
59
+ * call-kind axis or it will false-throw on a legitimate mix (design §4.5.1-11).
60
+ */
61
+ export function projectSynthesizeCertArmDispatch(captureLines) {
62
+ const byArm = new Map(SYNTHESIZE_CERT_ARMS.map((arm) => [arm, []]));
63
+ for (const raw of captureLines) {
64
+ if (typeof raw !== "object" || raw === null)
65
+ continue;
66
+ const line = raw;
67
+ const role = line.role;
68
+ if (typeof role !== "string" || !byArm.has(role))
69
+ continue;
70
+ byArm.get(role).push({
71
+ seq: line.seq,
72
+ dispatch: line.dispatch,
73
+ hasKey: Object.hasOwn(line, "dispatch"),
74
+ });
75
+ }
76
+ const armLineTotal = [...byArm.values()].reduce((n, lines) => n + lines.length, 0);
77
+ const legacyTotal = [...byArm.values()].reduce((n, lines) => n + lines.filter((l) => !l.hasKey).length, 0);
78
+ if (armLineTotal > 0 && legacyTotal === armLineTotal) {
79
+ return { armDispatch: null, legacy: true, violations: [] };
80
+ }
81
+ const violations = [];
82
+ const armDispatch = {};
83
+ for (const arm of SYNTHESIZE_CERT_ARMS) {
84
+ const lines = byArm.get(arm);
85
+ if (lines.length === 0) {
86
+ violations.push(`arm ${arm}: no captured calls — a declared dispatch without witness is not certifiable`);
87
+ continue;
88
+ }
89
+ let witnessed = null;
90
+ for (const line of lines) {
91
+ if (!line.hasKey) {
92
+ violations.push(`arm ${arm}: capture line seq=${String(line.seq)} predates the dispatch witness (mixed legacy/new capture — no evidence line in a witnessing run)`);
93
+ continue;
94
+ }
95
+ const parsed = SynthesizeCertDispatchConfigSchema.safeParse(line.dispatch);
96
+ if (!parsed.success) {
97
+ violations.push(`arm ${arm}: capture line seq=${String(line.seq)} carries a malformed dispatch witness: ${parsed.error.issues.map((i) => i.message).join("; ")}`);
98
+ continue;
99
+ }
100
+ if (witnessed === null) {
101
+ witnessed = parsed.data;
102
+ }
103
+ else if (witnessed.reasoning_effort !== parsed.data.reasoning_effort ||
104
+ witnessed.thinking_mode !== parsed.data.thinking_mode) {
105
+ violations.push(`arm ${arm}: inconsistent dispatch within one arm (${JSON.stringify(witnessed)} vs ${JSON.stringify(parsed.data)} at seq=${String(line.seq)})`);
106
+ }
107
+ }
108
+ if (witnessed !== null)
109
+ armDispatch[arm] = witnessed;
110
+ }
111
+ if (violations.length > 0)
112
+ return { armDispatch: null, legacy: false, violations };
113
+ return {
114
+ armDispatch: armDispatch,
115
+ legacy: false,
116
+ violations: [],
117
+ };
118
+ }
119
+ /**
120
+ * Pure declared-vs-witnessed dispatch guard (effort-witness design §3 verify +
121
+ * §4.5.1-9). Compares the preflight DECLARATION against the capture WITNESS
122
+ * knob-by-knob per arm, plus two deterministic realization-boundary rules
123
+ * keyed on each arm's DECLARED provider brand:
124
+ * (a) an openai(codex-route) arm must witness a present `reasoning_effort` —
125
+ * a knobless codex dispatch inherits an unobservable host-TOML effort and
126
+ * is not certifiable as any effort;
127
+ * (b) no arm may witness `reasoning_effort` and `thinking_mode` together —
128
+ * the anthropic route ignores effort once thinking is disabled, so that
129
+ * certification claim would be unrealized.
130
+ * Returns violation strings; empty = the certified dispatch is the dispatched
131
+ * dispatch.
132
+ */
133
+ export function synthesizeCertDispatchGuardViolations(args) {
134
+ const violations = [];
135
+ for (const arm of SYNTHESIZE_CERT_ARMS) {
136
+ const declared = args.declared[arm];
137
+ const witnessed = args.witnessed[arm];
138
+ if (declared.reasoning_effort !== witnessed.reasoning_effort) {
139
+ violations.push(`arm ${arm}: declared reasoning_effort=${declared.reasoning_effort ?? "(absent)"} but witnessed ${witnessed.reasoning_effort ?? "(absent)"}`);
140
+ }
141
+ if (declared.thinking_mode !== witnessed.thinking_mode) {
142
+ violations.push(`arm ${arm}: declared thinking_mode=${declared.thinking_mode ?? "(absent)"} but witnessed ${witnessed.thinking_mode ?? "(absent)"}`);
143
+ }
144
+ if (args.armProviders[arm] === "openai" && witnessed.reasoning_effort === undefined) {
145
+ violations.push(`arm ${arm}: openai(codex-route) arm witnessed NO reasoning_effort — the worker would inherit an unobservable host-TOML effort; declare and dispatch an explicit effort`);
146
+ }
147
+ if (witnessed.reasoning_effort !== undefined && witnessed.thinking_mode !== undefined) {
148
+ violations.push(`arm ${arm}: witnessed BOTH reasoning_effort and thinking_mode — the anthropic route ignores effort once thinking is disabled, so this certification claim would be unrealized`);
149
+ }
150
+ }
151
+ return violations;
152
+ }
153
+ export function assembleSynthesizeCertRecord(args) {
154
+ const inputManifest = buildSynthesizeCertInputManifest(args.entries, args.packets);
155
+ const judgementRows = [...args.judgementRows];
156
+ const negative = buildInputCorruptionV1NegativeArm(args.mutationSeed);
157
+ const record = {
158
+ record_contract: SYNTHESIZE_CERT_CONTRACT,
159
+ created_at: args.createdAt,
160
+ provider: args.candidateModel.provider,
161
+ model: args.candidateModel.model,
162
+ declared_reps: args.declaredReps,
163
+ arm_prompt_sha256: {
164
+ baseline: args.promptSha256,
165
+ candidate: args.promptSha256,
166
+ negative_control: args.promptSha256,
167
+ },
168
+ arm_model: {
169
+ baseline: { provider: args.baselineModel.provider, model: args.baselineModel.model },
170
+ candidate: { provider: args.candidateModel.provider, model: args.candidateModel.model },
171
+ negative_control: {
172
+ provider: args.candidateModel.provider,
173
+ model: args.candidateModel.model,
174
+ },
175
+ },
176
+ ...(args.armDispatch !== undefined ? { arm_dispatch: args.armDispatch } : {}),
177
+ negative_arm: negative,
178
+ input_manifest: inputManifest,
179
+ judgement_rows: judgementRows,
180
+ declared_aggregates: computeSynthesizeCertAggregates({
181
+ inputManifest,
182
+ judgementRows,
183
+ }),
184
+ reproduction: {
185
+ command: args.reproduction.command,
186
+ source_paths: [...args.reproduction.source_paths],
187
+ limitations: args.reproduction.limitations,
188
+ },
189
+ };
190
+ const parsed = parseSynthesizeCertRecord(record);
191
+ if (!parsed.record) {
192
+ throw new Error(`synthesize-cert-assemble: assembled record failed the contract schema — ${parsed.violations
193
+ .map((v) => v.message)
194
+ .join("; ")}`);
195
+ }
196
+ return parsed.record;
197
+ }