onto-mcp 0.4.11 → 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 (139) hide show
  1. package/.onto/authority/core-lexicon.yaml +1 -1
  2. package/.onto/authority/supported-models.yaml +153 -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 +202 -49
  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/nesting-batch-worker-contract.md +1 -1
  11. package/.onto/processes/review/productized-live-path.md +9 -5
  12. package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
  13. package/.onto/processes/review/review-target-profile-contract.md +47 -4
  14. package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
  15. package/.onto/processes/shared/target-material-kind-contract.md +1 -1
  16. package/README.md +64 -1
  17. package/dist/cli.js +11 -1
  18. package/dist/core-api/reconstruct-api.js +679 -18
  19. package/dist/core-api/review-api.js +70 -5
  20. package/dist/core-api/review-progress.js +7 -0
  21. package/dist/core-api/runtime-observation.js +7 -0
  22. package/dist/core-runtime/artifact-io.js +131 -0
  23. package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
  24. package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
  25. package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
  26. package/dist/core-runtime/cli/prepare-review-session.js +34 -1
  27. package/dist/core-runtime/cli/render-review-final-output.js +2 -1
  28. package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
  29. package/dist/core-runtime/cli/run-seats.js +18 -0
  30. package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
  31. package/dist/core-runtime/cli/unit-resubmit.js +269 -0
  32. package/dist/core-runtime/cli/worker-structured-output.js +80 -2
  33. package/dist/core-runtime/discovery/host-detection.js +71 -22
  34. package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
  35. package/dist/core-runtime/discovery/review-cert-record.js +442 -0
  36. package/dist/core-runtime/discovery/review-execution-units.js +22 -0
  37. package/dist/core-runtime/discovery/seat-inventory.js +167 -0
  38. package/dist/core-runtime/discovery/settings-chain.js +311 -52
  39. package/dist/core-runtime/discovery/supported-models.js +289 -25
  40. package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
  41. package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
  42. package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
  43. package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
  44. package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
  45. package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
  46. package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
  47. package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
  48. package/dist/core-runtime/effort-calibration-ingest.js +326 -0
  49. package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
  50. package/dist/core-runtime/effort-calibration-review.js +22 -0
  51. package/dist/core-runtime/effort-calibration-sweep.js +110 -0
  52. package/dist/core-runtime/effort-frontier.js +134 -0
  53. package/dist/core-runtime/llm/claude-bin.js +77 -0
  54. package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
  55. package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
  56. package/dist/core-runtime/llm/llm-caller.js +316 -35
  57. package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
  58. package/dist/core-runtime/llm/model-switcher.js +1 -1
  59. package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
  60. package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
  61. package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
  62. package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
  63. package/dist/core-runtime/onboard/cli-host.js +5 -2
  64. package/dist/core-runtime/onboard/configure-provider.js +33 -1
  65. package/dist/core-runtime/onboard/host-target.js +28 -3
  66. package/dist/core-runtime/onboard/register.js +54 -12
  67. package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
  68. package/dist/core-runtime/reconstruct/artifact-types.js +37 -0
  69. package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
  70. package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
  71. package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
  72. package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
  73. package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
  74. package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
  75. package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
  76. package/dist/core-runtime/reconstruct/directive-validation.js +4 -3
  77. package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
  78. package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -9
  79. package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
  80. package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
  81. package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
  82. package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
  83. package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
  84. package/dist/core-runtime/reconstruct/material-admission-validation.js +20 -5
  85. package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
  86. package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
  87. package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
  88. package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
  89. package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
  90. package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
  91. package/dist/core-runtime/reconstruct/output-budget.js +6 -0
  92. package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
  93. package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
  94. package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
  95. package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
  96. package/dist/core-runtime/reconstruct/record.js +49 -3
  97. package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
  98. package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
  99. package/dist/core-runtime/reconstruct/run.js +5409 -517
  100. package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
  101. package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
  102. package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
  103. package/dist/core-runtime/reconstruct/source-observations.js +166 -0
  104. package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
  105. package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
  106. package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
  107. package/dist/core-runtime/review/continuation-plan.js +12 -5
  108. package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
  109. package/dist/core-runtime/review/materializers.js +100 -11
  110. package/dist/core-runtime/review/nesting-batch.js +6 -2
  111. package/dist/core-runtime/review/obligation-shardability.js +138 -0
  112. package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
  113. package/dist/core-runtime/review/review-artifact-utils.js +219 -7
  114. package/dist/core-runtime/review/review-execution-steps.js +32 -8
  115. package/dist/core-runtime/review/review-progress-contract.js +8 -0
  116. package/dist/core-runtime/review/review-prompt-budget.js +97 -0
  117. package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
  118. package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
  119. package/dist/core-runtime/route-identity.js +182 -0
  120. package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
  121. package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
  122. package/dist/core-runtime/target-material-kind.js +58 -0
  123. package/dist/mcp/server.js +27 -5
  124. package/dist/mcp/tool-schemas.js +8 -0
  125. package/dist/tui/app.js +167 -0
  126. package/dist/tui/data/event-follower.js +116 -0
  127. package/dist/tui/data/node-detail.js +42 -0
  128. package/dist/tui/data/projection-poll.js +26 -0
  129. package/dist/tui/data/session-discovery.js +65 -0
  130. package/dist/tui/index.js +108 -0
  131. package/dist/tui/screens/log.js +25 -0
  132. package/dist/tui/screens/node-detail.js +17 -0
  133. package/dist/tui/screens/session-selector.js +26 -0
  134. package/dist/tui/screens/workflow-tree.js +90 -0
  135. package/dist/tui/view-model/reconstruct-adapter.js +162 -0
  136. package/dist/tui/view-model/review-adapter.js +270 -0
  137. package/dist/tui/view-model/tree-view-model.js +19 -0
  138. package/package.json +20 -3
  139. package/settings.example.json +77 -16
@@ -11,6 +11,8 @@
11
11
  * telemetry value.
12
12
  */
13
13
  import crypto from "node:crypto";
14
+ import { witnessedReconstructRouteIdentity, } from "../route-identity.js";
15
+ import { readStructuredDispatchFailureEvidence } from "../llm/structured-dispatch-error.js";
14
16
  /**
15
17
  * Maps authored-artifact names (the `artifactName` passed to the JSON author
16
18
  * call) to the pipeline unit that owns the call. Repair and timeout-recovery
@@ -19,6 +21,17 @@ import crypto from "node:crypto";
19
21
  */
20
22
  const UNIT_ID_BY_AUTHORED_ARTIFACT_NAME = new Map([
21
23
  ["SourceObservationDirective", "observation_directive"],
24
+ // P1-C2 leaf-read (the first LLM-touch). Its absence here was the defect: callLlmRecorded
25
+ // (run.ts) resolves the unit as its first line, BEFORE the LLM call, so an unmapped name throws
26
+ // and the leaf-read caller swallowed it (R9) → zero capture, silently. Mapped to its own unit so
27
+ // leaf-read attempts/failures are recorded, not silently degraded.
28
+ ["leaf-read", "leaf_read"],
29
+ // Layer-2 semantic_map stage (wiring design 20260702 §6 F5, Defect-1 discipline): BOTH author
30
+ // capability call names are mapped BEFORE any author realization lands — an unmapped
31
+ // callJsonAuthor artifactName throws pre-LLM and silently kills the whole stage (the leaf-read
32
+ // production defect this map's fail-loud contract exists to prevent).
33
+ ["semantic-map-synthesize", "semantic_map"],
34
+ ["semantic-map-verify", "semantic_map"],
22
35
  ["ExplorationSynthesis", "exploration_synthesis"],
23
36
  ["SourceFrontier", "source_frontier"],
24
37
  ["SourcePurposeCandidates", "source_purpose_candidates"],
@@ -38,9 +51,16 @@ const UNIT_ID_BY_AUTHORED_ARTIFACT_NAME = new Map([
38
51
  ["FailureClassification", "failure_classification"],
39
52
  ["RevisionProposal", "revision_proposal"],
40
53
  ["StopDecision", "stop_decision"],
54
+ // Maturation value-read cut (the second LLM-touch). Both authoring calls (location
55
+ // selection + discharge judgment) own the single maturation_value_read unit, so a
56
+ // value-read total-failure is recorded, not silently degraded (leaf_read precedent —
57
+ // an unmapped name throws in callLlmRecorded before the LLM call).
58
+ ["MaturationValueReadLocation", "maturation_value_read"],
59
+ ["MaturationValueReadJudgment", "maturation_value_read"],
41
60
  ["MaturationQuestionFrontier", "maturation_question_frontier"],
42
61
  ["MaturationClosureFrontier", "maturation_closure_frontier"],
43
62
  ["AnswerSupportLedger", "answer_support_ledger"],
63
+ ["AnswerSupportJudgment", "answer_support_judgment"],
44
64
  ["MaturationAnswerClaims", "maturation_answer_claims"],
45
65
  ["OntologyExpansion", "ontology_expansion"],
46
66
  ["FinalOutput", "final_output"],
@@ -74,6 +94,11 @@ export function attemptKindForAuthoredArtifactName(artifactName) {
74
94
  return "initial";
75
95
  }
76
96
  export function failureClassForLlmCallError(error, isTimeout) {
97
+ const structured = readStructuredDispatchFailureEvidence(error);
98
+ if (structured?.failure_class === "transport" &&
99
+ structured.failure_code === "timeout") {
100
+ return "timeout";
101
+ }
77
102
  return isTimeout(error) ? "timeout" : "provider_error";
78
103
  }
79
104
  function sha256Hex(text) {
@@ -84,8 +109,9 @@ function addSourceIdentityRef(row, ref) {
84
109
  row.source_identity_refs.push(ref);
85
110
  }
86
111
  }
87
- export function createReconstructExecutionTelemetryCollector() {
112
+ export function createReconstructExecutionTelemetryCollector(options = {}) {
88
113
  const byUnitId = new Map();
114
+ const routeProjectionByUnitId = new Map();
89
115
  function unitRow(unitId) {
90
116
  const existing = byUnitId.get(unitId);
91
117
  if (existing)
@@ -101,6 +127,7 @@ export function createReconstructExecutionTelemetryCollector() {
101
127
  provider_route: null,
102
128
  model_id: null,
103
129
  effort: null,
130
+ route_identity: null,
104
131
  prompt_policy_sha256: null,
105
132
  source_identity_refs: [],
106
133
  attempt_count: 0,
@@ -117,19 +144,66 @@ export function createReconstructExecutionTelemetryCollector() {
117
144
  row.duration_ms += Math.max(0, Math.round(input.durationMs));
118
145
  row.prompt_chars += Math.max(0, input.promptChars);
119
146
  row.output_chars += Math.max(0, input.outputChars);
120
- if (typeof input.providerTokensIn === "number" && input.providerTokensIn > 0) {
147
+ if (typeof input.providerTokensIn === "number" && input.providerTokensIn >= 0) {
121
148
  row.provider_tokens_in = (row.provider_tokens_in ?? 0) + input.providerTokensIn;
122
149
  }
123
- if (typeof input.providerTokensOut === "number" && input.providerTokensOut > 0) {
150
+ if (typeof input.providerTokensOut === "number" && input.providerTokensOut >= 0) {
124
151
  row.provider_tokens_out =
125
152
  (row.provider_tokens_out ?? 0) + input.providerTokensOut;
126
153
  }
127
- if (input.providerRoute)
128
- row.provider_route = input.providerRoute;
129
- if (input.modelId)
130
- row.model_id = input.modelId;
131
- if (input.effort)
132
- row.effort = input.effort;
154
+ const routeIdentity = input.provider || input.executionAdapter || input.effectiveBaseUrl
155
+ ? witnessedReconstructRouteIdentity({
156
+ provider: input.provider,
157
+ executionAdapter: input.executionAdapter,
158
+ declaredBillingMode: input.declaredBillingMode,
159
+ effectiveBaseUrl: input.effectiveBaseUrl,
160
+ })
161
+ : null;
162
+ const hasRouteProjection = Boolean(input.providerRoute || input.modelId || input.effort || routeIdentity);
163
+ if (hasRouteProjection && !options.nullMixedRouteProjection) {
164
+ if (input.providerRoute)
165
+ row.provider_route = input.providerRoute;
166
+ if (input.modelId)
167
+ row.model_id = input.modelId;
168
+ if (input.effort)
169
+ row.effort = input.effort;
170
+ if (routeIdentity)
171
+ row.route_identity = routeIdentity;
172
+ }
173
+ else if (hasRouteProjection) {
174
+ const projection = routeProjectionByUnitId.get(input.unitId) ?? {
175
+ providerRoute: null,
176
+ modelId: null,
177
+ effort: null,
178
+ routeIdentity: null,
179
+ mixed: false,
180
+ };
181
+ const conflicts = (prior, next) => prior !== null &&
182
+ next !== null &&
183
+ JSON.stringify(prior) !== JSON.stringify(next);
184
+ projection.mixed = projection.mixed ||
185
+ conflicts(projection.providerRoute, input.providerRoute ?? null) ||
186
+ conflicts(projection.modelId, input.modelId ?? null) ||
187
+ conflicts(projection.effort, input.effort ?? null) ||
188
+ conflicts(projection.routeIdentity, routeIdentity);
189
+ projection.providerRoute ??= input.providerRoute ?? null;
190
+ projection.modelId ??= input.modelId ?? null;
191
+ projection.effort ??= input.effort ?? null;
192
+ projection.routeIdentity ??= routeIdentity;
193
+ routeProjectionByUnitId.set(input.unitId, projection);
194
+ if (!projection.mixed) {
195
+ row.provider_route = projection.providerRoute;
196
+ row.model_id = projection.modelId;
197
+ row.effort = projection.effort;
198
+ row.route_identity = projection.routeIdentity;
199
+ }
200
+ else {
201
+ row.provider_route = null;
202
+ row.model_id = null;
203
+ row.effort = null;
204
+ row.route_identity = null;
205
+ }
206
+ }
133
207
  if (row.prompt_policy_sha256 === null &&
134
208
  input.kind === "initial" &&
135
209
  typeof input.systemPrompt === "string") {
@@ -173,6 +247,7 @@ export function createReconstructExecutionTelemetryCollector() {
173
247
  },
174
248
  reset() {
175
249
  byUnitId.clear();
250
+ routeProjectionByUnitId.clear();
176
251
  },
177
252
  };
178
253
  }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Single source of truth for the reconstruct final-output append-section set (G(c)).
3
+ *
4
+ * The final output emits a fixed set of markdown sections. Their identity (section_id,
5
+ * heading, prompt-policy id, emission ownership, provenance-binding requirement,
6
+ * activation) used to be scattered across run.ts in up to FOUR copies per section — the
7
+ * emitter fn's inline `## <Heading>` literal, the provenance binding's `heading` field, the
8
+ * prompt-policy `deterministic_runtime_append_sections` hint, and (for the bindings section)
9
+ * the `required_fragments` section-id literal. This module is the one place that identity
10
+ * lives; run.ts consumes it for ALL of those representations, and the G9 parity guard
11
+ * (scripts/check-final-output-sections-parity.ts, INV-SCHEMA-1) asserts the registry node
12
+ * `final_output_append_sections` declares exactly this surface. Side-effect-free.
13
+ *
14
+ * Headings are BARE (no `## ` prefix): run.ts prepends `## ` at the emit site, and the
15
+ * provenance gate matches on `## ` + heading (post-seed-validation.ts via markdown-section).
16
+ *
17
+ * Two ORDERS that differ today are pinned INDEPENDENTLY here (neither derived from the other):
18
+ * - the prompt-policy order (claim before artifact) — PROMPT_POLICY_APPEND_SECTION_IDS;
19
+ * - the provenance-bindings order (artifact before claim) — the bound-row order of
20
+ * FINAL_OUTPUT_SECTIONS, exposed via provenanceBindingSectionIds().
21
+ */
22
+ /** Canonical hyphen section ids (also the load-bearing required_fragments text). */
23
+ export const FINAL_OUTPUT_SECTION_IDS = {
24
+ seedAnswerability: "seed-answerability",
25
+ claimProjection: "claim-projection",
26
+ artifactTruth: "artifact-truth",
27
+ runtimeArtifactTruthFooter: "runtime-artifact-truth-footer",
28
+ runtimeProvenanceBindings: "runtime-provenance-bindings",
29
+ sourceProjectionTruncation: "source-projection-truncation",
30
+ workbookInventoryProjectionTruncation: "workbook-inventory-projection-truncation",
31
+ unresolvedRevisionProposals: "unresolved-revision-proposals",
32
+ };
33
+ /** Bare section headings (no `## ` prefix). The emit site prepends `## `. */
34
+ export const FINAL_OUTPUT_SECTION_HEADINGS = {
35
+ seedAnswerability: "Seed Answerability",
36
+ claimProjection: "Claim Projection",
37
+ artifactTruth: "Artifact Truth",
38
+ runtimeArtifactTruthFooter: "Runtime Artifact Truth Footer",
39
+ runtimeProvenanceBindings: "Runtime Provenance Bindings",
40
+ sourceProjectionTruncation: "Source Projection Truncation",
41
+ workbookInventoryProjectionTruncation: "Workbook Inventory Projection Truncation",
42
+ unresolvedRevisionProposals: "Unresolved Revision Proposals",
43
+ };
44
+ const ID = FINAL_OUTPUT_SECTION_IDS;
45
+ const H = FINAL_OUTPUT_SECTION_HEADINGS;
46
+ /**
47
+ * Canonical ordered descriptors. The 5 bound rows are in the PROVENANCE-BINDINGS order
48
+ * (artifact before claim) — this is the order finalOutputProvenanceSectionBindings emits and
49
+ * the order persisted into the validation artifact, so it is load-bearing and must be
50
+ * preserved. The 3 conditional rows follow in emit order.
51
+ */
52
+ export const FINAL_OUTPUT_SECTIONS = [
53
+ { section_id: ID.seedAnswerability, heading: H.seedAnswerability, prompt_policy_id: "seed_answerability", emit_owner: "always_section", provenance_binding_required: true, activation: "always" },
54
+ { section_id: ID.artifactTruth, heading: H.artifactTruth, prompt_policy_id: "artifact_truth", emit_owner: "always_section", provenance_binding_required: true, activation: "always" },
55
+ { section_id: ID.claimProjection, heading: H.claimProjection, prompt_policy_id: "claim_projection", emit_owner: "always_section", provenance_binding_required: true, activation: "always" },
56
+ { section_id: ID.runtimeArtifactTruthFooter, heading: H.runtimeArtifactTruthFooter, prompt_policy_id: "provenance_footer", emit_owner: "always_section", provenance_binding_required: true, activation: "always" },
57
+ { section_id: ID.runtimeProvenanceBindings, heading: H.runtimeProvenanceBindings, prompt_policy_id: "provenance_bindings", emit_owner: "always_section", provenance_binding_required: true, activation: "always" },
58
+ { section_id: ID.sourceProjectionTruncation, heading: H.sourceProjectionTruncation, prompt_policy_id: null, emit_owner: "conditional_markdown", provenance_binding_required: false, activation: "document_projection_truncation_nonempty" },
59
+ { section_id: ID.workbookInventoryProjectionTruncation, heading: H.workbookInventoryProjectionTruncation, prompt_policy_id: null, emit_owner: "conditional_markdown", provenance_binding_required: false, activation: "workbook_inventory_projection_truncation_nonempty" },
60
+ { section_id: ID.unresolvedRevisionProposals, heading: H.unresolvedRevisionProposals, prompt_policy_id: null, emit_owner: "conditional_markdown", provenance_binding_required: false, activation: "disclosed_revision_proposals_nonempty" },
61
+ ];
62
+ /**
63
+ * The prompt-policy `deterministic_runtime_append_sections` order (claim BEFORE artifact) —
64
+ * pinned independently of the bindings order. Its SET must equal the non-null
65
+ * prompt_policy_id set of FINAL_OUTPUT_SECTIONS (asserted by the G9 guard); its ORDER is the
66
+ * frozen current prompt surface (asserted byte-for-byte by an exact-ordered run.test.ts test).
67
+ */
68
+ export const PROMPT_POLICY_APPEND_SECTION_IDS = [
69
+ "seed_answerability",
70
+ "claim_projection",
71
+ "artifact_truth",
72
+ "provenance_footer",
73
+ "provenance_bindings",
74
+ ];
75
+ export function promptPolicyAppendSectionIds() {
76
+ return [...PROMPT_POLICY_APPEND_SECTION_IDS];
77
+ }
78
+ /** The bound section_ids in provenance-bindings order (artifact before claim). */
79
+ export function provenanceBindingSectionIds() {
80
+ return FINAL_OUTPUT_SECTIONS.filter((s) => s.provenance_binding_required).map((s) => s.section_id);
81
+ }
82
+ /**
83
+ * The runtime-provenance-bindings section's required_fragments: the OTHER bound section_ids
84
+ * (every bound section except runtime-provenance-bindings itself), in bindings order. Derived
85
+ * here so the load-bearing validated-text list cannot drift from the canonical set.
86
+ */
87
+ export function runtimeProvenanceBindingsRequiredFragments() {
88
+ return provenanceBindingSectionIds().filter((id) => id !== FINAL_OUTPUT_SECTION_IDS.runtimeProvenanceBindings);
89
+ }
@@ -6,6 +6,7 @@ import { parse as parseYaml } from "yaml";
6
6
  import { resolveRegistryRef, } from "./contract-registry.js";
7
7
  import { isPathInsideRoot, } from "../path-boundary.js";
8
8
  import { assertReconstructDomainId, } from "./domain-id.js";
9
+ import { assertObligation } from "./obligation-assertion.js";
9
10
  function isRecord(value) {
10
11
  return typeof value === "object" && value !== null && !Array.isArray(value);
11
12
  }
@@ -300,6 +301,25 @@ async function buildAdmittedDomainSnapshots(args) {
300
301
  }
301
302
  return snapshots;
302
303
  }
304
+ /**
305
+ * The exact field allow-list embedded in the `validator_records` governing-snapshot
306
+ * family (and therefore in the authored-artifact reuse hash). The loader preserves
307
+ * additional fields on a parsed validator record (e.g. validation_obligations,
308
+ * conditional_validation_obligations) so callers can read them, but those MUST NOT
309
+ * enter the snapshot/reuse hash or every prior authored artifact would be forced to
310
+ * re-author. This is an ALLOW-LIST, not a strip-list: any future parsed field is
311
+ * excluded by default and must be consciously added here to enter the snapshot, so
312
+ * the no-rotation guarantee holds for the whole field class, not just today's two.
313
+ */
314
+ export function projectValidatorRecordSnapshotFields(validator) {
315
+ return {
316
+ validator_id: validator.validator_id,
317
+ gate_ids: validator.gate_ids,
318
+ validator_version: validator.validator_version,
319
+ input_authority_refs: validator.input_authority_refs,
320
+ output_ref: validator.output_ref,
321
+ };
322
+ }
303
323
  function snapshotFamily(args) {
304
324
  const items = Array.isArray(args.value) ? args.value : [];
305
325
  return {
@@ -409,7 +429,10 @@ export async function buildReconstructRunGoverningSnapshot(args) {
409
429
  snapshotFamily({
410
430
  familyId: "validator_records",
411
431
  sourceRef: "reconstruct-contract-registry.yaml#validator_records",
412
- value: args.contractRegistry.validator_records,
432
+ // Allow-list projection: obligation fields the loader now preserves are
433
+ // excluded so the family sha (and the reuse hash it feeds) stays byte-identical.
434
+ value: args.contractRegistry.validator_records
435
+ .map(projectValidatorRecordSnapshotFields),
413
436
  selectedIds: args.contractRegistry.validator_records
414
437
  .map((validator) => validator.validator_id),
415
438
  }),
@@ -601,6 +624,18 @@ export async function validateReconstructRunGoverningSnapshot(args) {
601
624
  ];
602
625
  }
603
626
  const violations = [];
627
+ // Reached only when the registry hash matched: the per-field equality comparisons below now run, so
628
+ // record the four snapshot-freeze obligations whose named scope maps 1:1 to a non-shared per-field
629
+ // check and is fully entailed by equality to the authority projection (selected ids == registry ids
630
+ // → resolve; version/snapshot map carries an entry per selected id → recorded). The pattern-catalog
631
+ // canonical-URI obligation is intentionally NOT stamped here: the rebuild derives the URI from a fixed
632
+ // URN convention of the id and never reads the registry canonical_uri_policy it names (PARKED).
633
+ if (args.assertedObligationIds) {
634
+ assertObligation(args.assertedObligationIds, "selected_reference_standard_ids_resolve_to_reference_standard_registry");
635
+ assertObligation(args.assertedObligationIds, "selected_reference_standard_versions_or_snapshots_are_recorded");
636
+ assertObligation(args.assertedObligationIds, "selected_pattern_catalog_ids_resolve_to_reference_pattern_catalog_registry");
637
+ assertObligation(args.assertedObligationIds, "selected_pattern_catalog_versions_or_snapshots_are_recorded");
638
+ }
604
639
  const checks = [
605
640
  {
606
641
  subject: "governing_snapshot.registry",
@@ -0,0 +1,305 @@
1
+ import { createHash } from "node:crypto";
2
+ import { compareColumnResidualDesc, } from "../spreadsheet-structure-observer.js";
3
+ import { LEAF_SEMANTIC_ROLES, } from "./comprehension-artifact.js";
4
+ import { readReconstructLlmDispatchFailureError } from "./llm-dispatch-failure.js";
5
+ // ─────────────────────────────────────────────────────────────────────────────
6
+ // leaf-reader (§3.2 / P1-C2-A · P1-C2-B′) — the FIRST LLM-touch. For a region the deterministic
7
+ // observer could not fully capture — a low-confidence (unstructured) region, OR a high-confidence
8
+ // column whose structure is incomplete (P1-C2-B′ §3) — the leaf-reader asks the LLM to CAPTURE what
9
+ // structure missed: a provisional label, an optional analytical role, and an optional note on the
10
+ // structure/meaning the aggregate signatures reveal.
11
+ //
12
+ // Honesty (§2.2/§2.3):
13
+ // - The label/role/note TEXT is the LLM's only contribution. The confidence tags are DETERMINISTIC:
14
+ // the read is always forced confidence='low' and is_lower_bound=true — the LLM cannot claim a
15
+ // strong reading on a structurally weak region (graceful degrade, non-authoritative).
16
+ // - Input is bounded deterministic structural metadata: value-tile shapes/format-identities,
17
+ // header-candidate rows, and (for a resolved high-confidence column) the deterministic column
18
+ // HEADER LABEL + inferred type/distinct count. NO raw DATA cell values and no literal formatCodes
19
+ // reach the LLM (source-safety, P1-C1 §3.4). The header label is column IDENTITY (already visible
20
+ // to the authoring LLM via the inventory), not row data; PII/value redaction is out of scope by
21
+ // owner governance. So `captured_note` is free LLM text but is grounded ONLY on these aggregate
22
+ // signals + the header label — it has no raw data value to restate.
23
+ // - Localization stays grounded on the deterministic value-tile (degrade is naming-only); a wrong
24
+ // label cannot corrupt correlation because this cut feeds no reduce/merge.
25
+ // - Failure (LLM hard error) and empty reads are explicit outcomes, never a silent success (§11 R9).
26
+ //
27
+ // This module is LLM-caller-injected (`callLlm`) so it is exercised by the INV-MOCK-1 fixture in
28
+ // tests and the real authoring caller in production; the module itself is realization-agnostic.
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ /** Single-source leaf-read prompt template. Registered in the authoring-prompt catalog (CG-1) so an
31
+ * edit rotates the resume key; also hashed into the llm_touch_fingerprint ⓑ (leaf_prompt_sha256).
32
+ * The opening line is the mock dispatcher's stable key — keep it stable when editing the body. */
33
+ export const LEAF_READ_SYSTEM_PROMPT = [
34
+ "Read provisional column labels for a low-confidence spreadsheet region.",
35
+ "",
36
+ "You are given bounded, aggregate-only evidence about spreadsheet columns the deterministic",
37
+ "observer could NOT fully capture — either a region whose header could not be resolved",
38
+ "(low-confidence), or a high-confidence column whose structure is incomplete (free-text or",
39
+ "high-residual content the deterministic summary does not pin down). The evidence is value-shape /",
40
+ "display-format signatures, the rows where a signature changes, candidate header row indices, and —",
41
+ "for a resolved column — the deterministic column HEADER LABEL / inferred type / distinct count. You",
42
+ "are NOT given raw DATA cell values: the header label is column identity, not row data.",
43
+ "",
44
+ "For each column you can read, return:",
45
+ " - tentative_label: a SHORT label naming what the column most likely holds;",
46
+ ' - semantic_role (optional): one of "category" | "measure" | "identifier" | "free_text" |',
47
+ ' "reference" — a SINGLE-COLUMN guess from the signals; "identifier"/"reference" are tentative',
48
+ " hints only (relationships are judged downstream, not here);",
49
+ " - captured_note (optional): a SHORT note on structure or meaning the signatures reveal that the",
50
+ " deterministic summary did not (e.g. a hidden grouping, or what a format boundary signifies).",
51
+ "Omit a column you cannot read rather than guessing. Return STRICT JSON:",
52
+ '{ "labels": [{ "column_index": <int>, "tentative_label": "<short label>",',
53
+ ' "semantic_role": "<role>"?, "captured_note": "<short note>"? }],',
54
+ ' "unread_columns": [{ "column_index": <int>, "reason": "<why unreadable>" }] }',
55
+ "",
56
+ "Your reads are provisional and non-authoritative; the runtime tags every read low-confidence.",
57
+ "Base every label, role, and note ONLY on the deterministic signals provided (header label, inferred",
58
+ "type, distinct count, value-shape/format signatures, boundary rows) — you are given NO raw DATA cell",
59
+ "values, so a note must never invent or restate a literal data value.",
60
+ ].join("\n");
61
+ export function leafReadPromptSha256() {
62
+ return createHash("sha256").update(LEAF_READ_SYSTEM_PROMPT).digest("hex");
63
+ }
64
+ /**
65
+ * Derive bounded leaf-read evidence for every LOW-confidence sheet from the deterministic inventory.
66
+ * Returns one region per low-confidence sheet that has value-tile signal. High-confidence sheets are
67
+ * left to the deterministic path (no leaf-read). Source-safe by construction (aggregate signatures
68
+ * + row indices only).
69
+ */
70
+ export function extractLowConfidenceLeafEvidence(inventory) {
71
+ const tilesBySheet = new Map();
72
+ for (const tile of inventory.segmented_value_tiles ?? [])
73
+ tilesBySheet.set(tile.sheet, tile);
74
+ const regions = [];
75
+ for (const sheet of inventory.per_sheet_data ?? []) {
76
+ if (sheet.header_confidence !== "low")
77
+ continue;
78
+ const tile = tilesBySheet.get(sheet.sheet);
79
+ if (!tile)
80
+ continue;
81
+ const columns = tile.columns.map((col) => {
82
+ const lastSegment = col.segments.length > 0 ? col.segments[col.segments.length - 1] : undefined;
83
+ return {
84
+ column_index: col.column_index,
85
+ dominant_shape: lastSegment ? lastSegment.dominant_shape : null,
86
+ dominant_format: lastSegment ? lastSegment.dominant_format : null,
87
+ boundaries: col.intra_tile_notes.map((note) => ({
88
+ boundary_kind: note.boundary_kind,
89
+ prev_shape: note.prev_shape,
90
+ new_shape: note.new_shape,
91
+ first_new_format_row: note.first_new_format_row,
92
+ })),
93
+ };
94
+ });
95
+ if (columns.length === 0)
96
+ continue;
97
+ regions.push({
98
+ sheet: sheet.sheet,
99
+ trigger: "low_confidence_header",
100
+ header_candidate_rows: sheet.header_rows ?? [],
101
+ columns,
102
+ });
103
+ }
104
+ return regions;
105
+ }
106
+ export const DEFAULT_STRUCTURE_LEAF_TRIGGER_OPTS = { max_columns: 64 };
107
+ /**
108
+ * Deterministic structure-incompleteness trigger (P1-C2-B′ §2.1). Selects leaf-read targets by a
109
+ * DETERMINISTIC predicate over inventory signals — NOT an LLM judgment of "importance" (which would
110
+ * make the read-set non-deterministic and reopen DET-1). The reason to read is capture-completeness:
111
+ * read every column UNLESS structure trivially captures it (a single constant; empty). High-residual
112
+ * (free-text / content-rich) columns are prioritised; the cap bounds fan-out with an honest capped
113
+ * census. Low-confidence sheets keep the P1-C2-A guarantee (always read; no regression).
114
+ *
115
+ * LLM-free, so the resulting read-set is a pure function of the inventory — the property that makes
116
+ * the existing resume contract sound (§4).
117
+ */
118
+ export function extractStructureLeafEvidence(inventory, opts = DEFAULT_STRUCTURE_LEAF_TRIGGER_OPTS) {
119
+ const tilesBySheet = new Map();
120
+ for (const tile of inventory.segmented_value_tiles ?? [])
121
+ tilesBySheet.set(tile.sheet, tile);
122
+ // (1) Low-confidence sheets: the P1-C2-A read-set — ALWAYS read (no cap, no regression).
123
+ const lowConfidenceRegions = extractLowConfidenceLeafEvidence(inventory);
124
+ const lowConfidenceSheets = new Set(lowConfidenceRegions.map((r) => r.sheet));
125
+ const candidates = [];
126
+ for (const sheet of inventory.per_sheet_data ?? []) {
127
+ if (lowConfidenceSheets.has(sheet.sheet))
128
+ continue; // already fully read by (1)
129
+ const tile = tilesBySheet.get(sheet.sheet);
130
+ for (const column of sheet.columns) {
131
+ if (!isStructureIncomplete(column))
132
+ continue;
133
+ candidates.push({ sheet: sheet.sheet, column, tile });
134
+ }
135
+ }
136
+ // Deterministic priority: highest residual (structure summarises LEAST) first.
137
+ candidates.sort((a, b) => {
138
+ const r = compareColumnResidualDesc(a.column, b.column);
139
+ if (r !== 0)
140
+ return r;
141
+ return a.sheet < b.sheet ? -1 : a.sheet > b.sheet ? 1 : 0;
142
+ });
143
+ const selected = candidates.slice(0, Math.max(0, opts.max_columns));
144
+ const capped = candidates.slice(Math.max(0, opts.max_columns));
145
+ // Group selected high-confidence candidates into per-sheet regions (enriched with value-tile
146
+ // boundary witnesses by column index when available).
147
+ const bySheet = new Map();
148
+ for (const { sheet, column, tile } of selected) {
149
+ let region = bySheet.get(sheet);
150
+ if (!region) {
151
+ region = { sheet, trigger: "structure_incomplete", header_candidate_rows: [], columns: [] };
152
+ bySheet.set(sheet, region);
153
+ }
154
+ const tileCol = tile?.columns.find((c) => c.column_index === column.index);
155
+ const lastSegment = tileCol && tileCol.segments.length > 0 ? tileCol.segments[tileCol.segments.length - 1] : undefined;
156
+ region.columns.push({
157
+ column_index: column.index,
158
+ column_name: column.name,
159
+ inferred_type: column.inferred_type,
160
+ distinct_count: column.distinct_count,
161
+ distinct_is_estimate: column.distinct_count_is_estimate,
162
+ dominant_shape: lastSegment ? lastSegment.dominant_shape : null,
163
+ dominant_format: lastSegment ? lastSegment.dominant_format : null,
164
+ boundaries: (tileCol?.intra_tile_notes ?? []).map((note) => ({
165
+ boundary_kind: note.boundary_kind,
166
+ prev_shape: note.prev_shape,
167
+ new_shape: note.new_shape,
168
+ first_new_format_row: note.first_new_format_row,
169
+ })),
170
+ });
171
+ }
172
+ return {
173
+ regions: [...lowConfidenceRegions, ...bySheet.values()],
174
+ capped_columns: capped.map((c) => ({
175
+ sheet: c.sheet,
176
+ column_index: c.column.index,
177
+ column_name: c.column.name,
178
+ })),
179
+ };
180
+ }
181
+ /** Deterministic "structure trivially captures this column" predicate (P1-C2-B′ §2.1). A column is a
182
+ * leaf-read candidate UNLESS structure already says everything: a single constant value, empty, or a
183
+ * single uniform formula. (Cost is no concern, so the default leans toward reading — missing a
184
+ * capturable fact is the defect to avoid, not an extra read.)
185
+ *
186
+ * Uniform-formula skip (gate follow-up #3): the observer marks `is_uniform_formula` when EXACTLY one
187
+ * single-column formula pattern provably covers the column's data cells (a fill-down whose references
188
+ * shift but whose structure is one repeated formula; Excel shared-formula dedup collapses these). The
189
+ * observer is conservative — a partial-formula / multi-formula / multi-column column is NOT marked, so
190
+ * this never wrongly skips. (See InventoryColumn.is_uniform_formula for the residual title-row caveat.) */
191
+ function isStructureIncomplete(column) {
192
+ if (column.inferred_type === "empty")
193
+ return false;
194
+ if (column.non_empty_count === 0)
195
+ return false;
196
+ if (column.distinct_count <= 1 && !column.distinct_count_is_estimate)
197
+ return false; // single constant
198
+ if (column.is_uniform_formula)
199
+ return false; // one repeated formula → structure fully captures it (#3)
200
+ return true;
201
+ }
202
+ /** sha256 of the read-set-shaping LOGIC source (the deterministic trigger: selection + predicate +
203
+ * ordering). Folded into the llm_touch_fingerprint ⓑ so editing the read-set logic TAUTOLOGICALLY
204
+ * rotates the resume key (CG-1 pattern; [[cg1-catalog-mechanism-decision]]) — NOT relying on a manual
205
+ * comprehension_version bump. The read-set is a pure function of the inventory AND this logic: the
206
+ * inventory inputs are covered by ⓐ (content_sha256 + adapter_version, which DETERMINE the inventory),
207
+ * this covers the logic, and structure_leaf_trigger_config covers the config. Over-rotates on a
208
+ * cosmetic source edit (safe: fail toward regenerate, never silent-stale). */
209
+ export function structureLeafTriggerLogicSha256() {
210
+ const sep = "";
211
+ return createHash("sha256")
212
+ .update(extractStructureLeafEvidence.toString())
213
+ .update(sep)
214
+ .update(extractLowConfidenceLeafEvidence.toString())
215
+ .update(sep)
216
+ .update(isStructureIncomplete.toString())
217
+ .update(sep)
218
+ .update(compareColumnResidualDesc.toString())
219
+ .digest("hex");
220
+ }
221
+ /** Bound on a captured note (the LLM's free-text gist of structure the deterministic summary missed).
222
+ * It is derived from aggregate signatures only (no raw cell values reach the LLM), so this caps prose
223
+ * length, not source exposure. */
224
+ const MAX_CAPTURED_NOTE_CHARS = 240;
225
+ const LEAF_SEMANTIC_ROLE_SET = new Set(LEAF_SEMANTIC_ROLES);
226
+ /**
227
+ * Run the LLM leaf-read (CAPTURE) for one region the deterministic observer could not fully capture.
228
+ * `callLlm` is injected (mock fixture in tests, real authoring caller in production). The LLM
229
+ * contributes only the label / role / note TEXT; confidence and is_lower_bound are forced (low /
230
+ * true) regardless of the trigger. An unrecognised semantic_role is dropped (the label is kept);
231
+ * a captured_note is trimmed and length-bounded.
232
+ */
233
+ export async function readStructureLeaf(args) {
234
+ const { evidence, callLlm } = args;
235
+ let raw;
236
+ try {
237
+ raw = await callLlm(LEAF_READ_SYSTEM_PROMPT, {
238
+ sheet: evidence.sheet,
239
+ header_candidate_rows: evidence.header_candidate_rows,
240
+ columns: evidence.columns,
241
+ });
242
+ }
243
+ catch (error) {
244
+ // Provider output-ceiling failures are run-terminal: partial output must
245
+ // never enter leaf degradation or permit a later semantic call.
246
+ if (readReconstructLlmDispatchFailureError(error))
247
+ throw error;
248
+ // §11 R9: ordinary transport/provider errors remain explicit FAILED
249
+ // outcomes and degrade to the deterministic producer.
250
+ return { kind: "failed", reason: `leaf-read LLM call failed: ${error.message}` };
251
+ }
252
+ let parsed;
253
+ try {
254
+ parsed = JSON.parse(raw);
255
+ }
256
+ catch {
257
+ return { kind: "failed", reason: "leaf-read returned unparseable JSON" };
258
+ }
259
+ const rawLabels = Array.isArray(parsed.labels) ? parsed.labels : [];
260
+ const knownColumns = new Set(evidence.columns.map((c) => c.column_index));
261
+ const labels = rawLabels
262
+ .filter((l) => l &&
263
+ typeof l.column_index === "number" &&
264
+ knownColumns.has(l.column_index) &&
265
+ typeof l.tentative_label === "string" &&
266
+ l.tentative_label.trim() !== "")
267
+ .map((l) => {
268
+ // Capture (P1-C2-B′ §3): an unrecognised role is dropped (label kept); a note is trimmed +
269
+ // length-bounded. The LLM saw only aggregate signatures, so a note carries no raw cell value.
270
+ const role = typeof l.semantic_role === "string" && LEAF_SEMANTIC_ROLE_SET.has(l.semantic_role)
271
+ ? l.semantic_role
272
+ : undefined;
273
+ const note = typeof l.captured_note === "string" && l.captured_note.trim() !== ""
274
+ ? l.captured_note.trim().slice(0, MAX_CAPTURED_NOTE_CHARS)
275
+ : undefined;
276
+ const label = {
277
+ sheet: evidence.sheet,
278
+ column_index: l.column_index,
279
+ tentative_label: l.tentative_label.trim(),
280
+ // Forced honesty tags (§2.2): the region is structurally incomplete, so the read is always a
281
+ // non-authoritative lower bound regardless of any confidence the model asserts.
282
+ confidence: "low",
283
+ is_lower_bound: true,
284
+ };
285
+ if (role)
286
+ label.semantic_role = role;
287
+ if (note)
288
+ label.captured_note = note;
289
+ return label;
290
+ });
291
+ if (labels.length === 0) {
292
+ return { kind: "unread", reason: "leaf-read produced no readable labels for this region" };
293
+ }
294
+ const limitingReason = evidence.trigger === "structure_incomplete"
295
+ ? "structure-incomplete region; columns captured provisionally from value-tile signatures"
296
+ : "low header_confidence region; labels read provisionally from value-tile signatures";
297
+ return {
298
+ kind: "produced",
299
+ result: {
300
+ labels,
301
+ limiting_region_ref: `${evidence.sheet}!region`,
302
+ limiting_reason: limitingReason,
303
+ },
304
+ };
305
+ }