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
@@ -1,12 +1,12 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { assertArrayField, atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
6
+ import { assertObligation } from "./obligation-assertion.js";
5
7
  const SOURCE_SAFETY_CANONICAL_AXES = [
6
8
  "lifecycle_state",
7
9
  "authorization_state",
8
- "privacy_state",
9
- "redaction_state",
10
10
  "proof_sufficiency_state",
11
11
  "replay_state",
12
12
  ];
@@ -27,26 +27,13 @@ const AUTHORIZATION_STATES = [
27
27
  "unknown",
28
28
  "not_required",
29
29
  ];
30
- const PRIVACY_STATES = [
31
- "non_sensitive",
32
- "privacy_sensitive",
33
- "unknown",
34
- ];
35
- const REDACTION_STATES = [
36
- "none",
37
- "redacted",
38
- "required",
39
- "insufficient",
40
- ];
41
30
  const PROOF_SUFFICIENCY_STATES = [
42
31
  "sufficient_for_claim",
43
32
  "insufficient_for_claim",
44
- "trace_only",
45
33
  "unavailable",
46
34
  ];
47
35
  const REPLAY_STATES = [
48
36
  "replay_allowed",
49
- "replay_with_redaction",
50
37
  "no_replay_use",
51
38
  "unknown",
52
39
  ];
@@ -60,32 +47,9 @@ const INTENDED_CONSUMPTIONS = [
60
47
  const VISIBILITY_TIERS = [
61
48
  "consumption_allowed",
62
49
  "internal_only",
63
- "redacted_output_only",
64
50
  "no_prompt_use",
65
51
  "no_replay_use",
66
52
  ];
67
- const ALLOWED_PROOF_FORMS = [
68
- "raw_value",
69
- "hash",
70
- "bounded_summary",
71
- "source_ref_only",
72
- "unavailable",
73
- ];
74
- const SAFE_REDACTED_PROOF_FORMS = new Set([
75
- "hash",
76
- "bounded_summary",
77
- "source_ref_only",
78
- ]);
79
- const SENSITIVE_SOURCE_PATTERNS = [
80
- /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/,
81
- /\b(?:api[_-]?key|secret|password|passwd|pwd|token)\b\s*[:=]\s*['"]?[^'"\s]{8,}/i,
82
- /\bAuthorization:\s*Bearer\s+[A-Za-z0-9._~+/=-]{12,}/i,
83
- /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/,
84
- /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
85
- /\b\d{3}-\d{2}-\d{4}\b/,
86
- /\b\d{6}-[1-4]\d{6}\b/,
87
- /\b(?:\+?\d{1,3}[-.\s]?)?(?:\(?\d{2,4}\)?[-.\s]?)\d{3,4}[-.\s]?\d{4}\b/,
88
- ];
89
53
  function isoNow() {
90
54
  return new Date().toISOString();
91
55
  }
@@ -95,10 +59,6 @@ function isRecord(value) {
95
59
  async function readYamlDocument(filePath) {
96
60
  return parseYaml(await fs.readFile(filePath, "utf8"));
97
61
  }
98
- async function writeYamlDocument(filePath, value) {
99
- await fs.mkdir(path.dirname(filePath), { recursive: true });
100
- await fs.writeFile(filePath, stringifyYaml(value), "utf8");
101
- }
102
62
  function violation(args) {
103
63
  return {
104
64
  code: args.code,
@@ -110,24 +70,9 @@ function violation(args) {
110
70
  function inList(value, allowed) {
111
71
  return typeof value === "string" && allowed.includes(value);
112
72
  }
113
- function arrayValues(value, allowed) {
114
- if (!Array.isArray(value))
115
- return [];
116
- return value.filter((item) => inList(item, allowed));
117
- }
118
73
  export function sourceSafetyRowIdForObservation(observation, intendedConsumption = "prompt_context") {
119
74
  return `source_safety:${observation.observation_id}:${intendedConsumption}`;
120
75
  }
121
- function observationContentExcerpt(observation) {
122
- const excerpt = observation.structural_data.content_excerpt;
123
- return typeof excerpt === "string" ? excerpt : null;
124
- }
125
- function hasSensitiveSourceEvidence(observation) {
126
- const excerpt = observationContentExcerpt(observation);
127
- if (!excerpt)
128
- return false;
129
- return SENSITIVE_SOURCE_PATTERNS.some((pattern) => pattern.test(excerpt));
130
- }
131
76
  function explicitConsumptionAuthorizations(observation) {
132
77
  const rawAuthorizations = observation.structural_data.source_safety_consumption_authorizations;
133
78
  if (!Array.isArray(rawAuthorizations))
@@ -147,9 +92,6 @@ function stableRuleRef(row) {
147
92
  .slice(0, 16);
148
93
  return `source-safety-visibility-v1:${digest}`;
149
94
  }
150
- function hasSafeRedactedProofForm(row) {
151
- return row.redaction_evidence.allowed_proof_forms.some((form) => SAFE_REDACTED_PROOF_FORMS.has(form));
152
- }
153
95
  export function deriveSourceSafetyVisibilityTier(row) {
154
96
  const intendedConsumption = row.visibility_derivation.intended_consumption;
155
97
  if (intendedConsumption === "replay" &&
@@ -162,9 +104,7 @@ export function deriveSourceSafetyVisibilityTier(row) {
162
104
  row.authorization_state === "unknown") {
163
105
  return "no_prompt_use";
164
106
  }
165
- if (row.proof_sufficiency_state === "unavailable" ||
166
- row.redaction_state === "insufficient" ||
167
- row.redaction_evidence.allowed_proof_forms.includes("unavailable")) {
107
+ if (row.proof_sufficiency_state === "unavailable") {
168
108
  return "no_prompt_use";
169
109
  }
170
110
  if (intendedConsumption === "material_claim" &&
@@ -175,15 +115,6 @@ export function deriveSourceSafetyVisibilityTier(row) {
175
115
  row.proof_sufficiency_state !== "sufficient_for_claim") {
176
116
  return "internal_only";
177
117
  }
178
- if (row.redaction_state === "redacted" ||
179
- row.redaction_state === "required" ||
180
- row.replay_state === "replay_with_redaction" ||
181
- row.privacy_state === "privacy_sensitive") {
182
- return hasSafeRedactedProofForm(row) ? "redacted_output_only" : "internal_only";
183
- }
184
- if (row.privacy_state === "unknown") {
185
- return hasSafeRedactedProofForm(row) ? "redacted_output_only" : "internal_only";
186
- }
187
118
  if (intendedConsumption === "public_output" ||
188
119
  intendedConsumption === "prompt_context" ||
189
120
  intendedConsumption === "material_claim") {
@@ -192,40 +123,29 @@ export function deriveSourceSafetyVisibilityTier(row) {
192
123
  return "internal_only";
193
124
  }
194
125
  function buildSafetyRowForObservation(observation, intendedConsumption) {
195
- const sensitive = hasSensitiveSourceEvidence(observation);
196
- const excerpt = observationContentExcerpt(observation);
197
126
  const explicitlyAuthorized = explicitConsumptionAuthorizations(observation)
198
127
  .has(intendedConsumption);
199
- const consumptionAuthorized = runtimeInternalConsumption(intendedConsumption) || explicitlyAuthorized;
200
- const allowedProofForms = sensitive
201
- ? ["hash", "bounded_summary", "source_ref_only"]
202
- : !consumptionAuthorized
203
- ? ["hash", "bounded_summary", "source_ref_only"]
204
- : excerpt
205
- ? ["raw_value", "hash", "bounded_summary", "source_ref_only"]
206
- : ["hash", "bounded_summary", "source_ref_only"];
128
+ // Defect-3 basis A (runtime-target provenance): a user-provided reconstruct
129
+ // runtime-target source authorizes the outward tiers (material_claim,
130
+ // public_output) by provenance. Scoped to the two upper tiers only — the
131
+ // internal tiers are already covered by runtimeInternalConsumption, and a
132
+ // non-target observation (is_runtime_target_source absent/false) never gets
133
+ // basis A, so this cannot leak to frontier-discovered sources.
134
+ const provenanceAuthorized = observation.is_runtime_target_source === true &&
135
+ (intendedConsumption === "material_claim" ||
136
+ intendedConsumption === "public_output");
137
+ const consumptionAuthorized = runtimeInternalConsumption(intendedConsumption) || explicitlyAuthorized ||
138
+ provenanceAuthorized;
207
139
  const rowBase = {
208
140
  safety_row_id: sourceSafetyRowIdForObservation(observation, intendedConsumption),
209
141
  subject_ref: observation.source_ref,
210
142
  subject_kind: "source_ref",
211
143
  lifecycle_state: "active",
212
144
  authorization_state: consumptionAuthorized ? "authorized" : "unknown",
213
- privacy_state: sensitive
214
- ? "privacy_sensitive"
215
- : consumptionAuthorized
216
- ? "non_sensitive"
217
- : "unknown",
218
- redaction_state: sensitive ? "required" : consumptionAuthorized ? "none" : "insufficient",
219
- proof_sufficiency_state: sensitive
220
- ? "trace_only"
221
- : consumptionAuthorized
222
- ? "sufficient_for_claim"
223
- : "insufficient_for_claim",
224
- replay_state: sensitive
225
- ? "replay_with_redaction"
226
- : consumptionAuthorized
227
- ? "replay_allowed"
228
- : "unknown",
145
+ proof_sufficiency_state: consumptionAuthorized
146
+ ? "sufficient_for_claim"
147
+ : "insufficient_for_claim",
148
+ replay_state: consumptionAuthorized ? "replay_allowed" : "unknown",
229
149
  visibility_tier: "internal_only",
230
150
  visibility_derivation: {
231
151
  intended_consumption: intendedConsumption,
@@ -240,11 +160,6 @@ function buildSafetyRowForObservation(observation, intendedConsumption) {
240
160
  ? "source_safety_explicit_consumption_authorization"
241
161
  : "runtime_target_ref_read_scope"
242
162
  : null,
243
- redaction_evidence: {
244
- raw_value_available: excerpt !== null,
245
- allowed_proof_forms: allowedProofForms,
246
- redaction_rule_ref: sensitive ? "source-safety-sensitive-source-pattern-v1" : null,
247
- },
248
163
  tombstone: {
249
164
  tombstone_ref: null,
250
165
  reason: null,
@@ -252,9 +167,6 @@ function buildSafetyRowForObservation(observation, intendedConsumption) {
252
167
  downstream_refs: [],
253
168
  },
254
169
  limitation_refs: [
255
- ...(sensitive
256
- ? [`source-safety-sensitive-source:${observation.observation_id}`]
257
- : []),
258
170
  ...(!consumptionAuthorized
259
171
  ? [
260
172
  `source-safety-consumption-authorization-gap:${observation.observation_id}:${intendedConsumption}`,
@@ -271,6 +183,7 @@ function buildSafetyRowsForObservation(observation) {
271
183
  return INTENDED_CONSUMPTIONS.map((intendedConsumption) => buildSafetyRowForObservation(observation, intendedConsumption));
272
184
  }
273
185
  export function buildSourceSafetyLedgerFromSourceObservations(args) {
186
+ assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
274
187
  return {
275
188
  schema_version: "1",
276
189
  session_id: args.sourceObservations.session_id,
@@ -280,9 +193,11 @@ export function buildSourceSafetyLedgerFromSourceObservations(args) {
280
193
  };
281
194
  }
282
195
  function sourceObservationSubjectRefs(sourceObservations) {
196
+ assertArrayField(sourceObservations.observations, "source-observations", "observations");
283
197
  return new Set(sourceObservations.observations.map((observation) => observation.source_ref));
284
198
  }
285
199
  function sourceObservationBindingBySafetyRowId(sourceObservations) {
200
+ assertArrayField(sourceObservations.observations, "source-observations", "observations");
286
201
  return new Map(sourceObservations.observations.flatMap((observation) => INTENDED_CONSUMPTIONS.map((intendedConsumption) => [
287
202
  sourceSafetyRowIdForObservation(observation, intendedConsumption),
288
203
  observation,
@@ -296,67 +211,17 @@ function validateCanonicalAxes(args) {
296
211
  SOURCE_SAFETY_CANONICAL_AXES.some((axis) => !axisSet.has(axis))) {
297
212
  args.violations.push(violation({
298
213
  code: "visibility_axis_set_invalid",
299
- message: "visibility_derivation.derived_from_axes must contain exactly the six canonical source-safety axes",
214
+ message: "visibility_derivation.derived_from_axes must contain exactly the four canonical source-safety axes",
300
215
  subjectId: args.row.safety_row_id,
301
216
  }));
302
217
  }
303
218
  }
304
- function validateSupportingDetailConsistency(args) {
305
- if (args.row.redaction_state !== "none" &&
306
- args.row.redaction_evidence.allowed_proof_forms.includes("raw_value")) {
307
- args.violations.push(violation({
308
- code: "supporting_detail_contradiction",
309
- message: "redaction_evidence.allowed_proof_forms cannot grant raw_value when top-level redaction_state requires or applies redaction",
310
- subjectId: args.row.safety_row_id,
311
- axis: "redaction_state",
312
- }));
313
- }
314
- if (args.row.redaction_state === "none" &&
315
- args.row.redaction_evidence.redaction_rule_ref) {
316
- args.violations.push(violation({
317
- code: "supporting_detail_contradiction",
318
- message: "redaction_evidence.redaction_rule_ref must be null when top-level redaction_state is none",
319
- subjectId: args.row.safety_row_id,
320
- axis: "redaction_state",
321
- }));
322
- }
323
- if (args.row.proof_sufficiency_state === "unavailable" &&
324
- !args.row.redaction_evidence.allowed_proof_forms.includes("unavailable")) {
325
- args.violations.push(violation({
326
- code: "supporting_detail_contradiction",
327
- message: "proof_sufficiency_state unavailable must be supported by allowed_proof_forms including unavailable",
328
- subjectId: args.row.safety_row_id,
329
- axis: "proof_sufficiency_state",
330
- }));
331
- }
332
- if (args.row.redaction_evidence.allowed_proof_forms.includes("unavailable") &&
333
- args.row.proof_sufficiency_state !== "unavailable") {
334
- args.violations.push(violation({
335
- code: "supporting_detail_contradiction",
336
- message: "allowed_proof_forms unavailable contradicts a top-level proof_sufficiency_state that is not unavailable",
337
- subjectId: args.row.safety_row_id,
338
- axis: "proof_sufficiency_state",
339
- }));
340
- }
341
- if (args.row.redaction_evidence.raw_value_available === false &&
342
- args.row.redaction_evidence.allowed_proof_forms.includes("raw_value")) {
343
- args.violations.push(violation({
344
- code: "supporting_detail_contradiction",
345
- message: "redaction_evidence cannot allow raw_value when raw_value_available is false",
346
- subjectId: args.row.safety_row_id,
347
- axis: "redaction_state",
348
- }));
349
- }
350
- }
351
219
  function normalizeSafetyRow(rawRow) {
352
220
  if (!isRecord(rawRow))
353
221
  return null;
354
222
  const derivation = isRecord(rawRow.visibility_derivation)
355
223
  ? rawRow.visibility_derivation
356
224
  : {};
357
- const redactionEvidence = isRecord(rawRow.redaction_evidence)
358
- ? rawRow.redaction_evidence
359
- : {};
360
225
  const tombstone = isRecord(rawRow.tombstone) ? rawRow.tombstone : {};
361
226
  return {
362
227
  safety_row_id: typeof rawRow.safety_row_id === "string" ? rawRow.safety_row_id : "",
@@ -370,12 +235,6 @@ function normalizeSafetyRow(rawRow) {
370
235
  authorization_state: inList(rawRow.authorization_state, AUTHORIZATION_STATES)
371
236
  ? rawRow.authorization_state
372
237
  : "",
373
- privacy_state: inList(rawRow.privacy_state, PRIVACY_STATES)
374
- ? rawRow.privacy_state
375
- : "",
376
- redaction_state: inList(rawRow.redaction_state, REDACTION_STATES)
377
- ? rawRow.redaction_state
378
- : "",
379
238
  proof_sufficiency_state: inList(rawRow.proof_sufficiency_state, PROOF_SUFFICIENCY_STATES)
380
239
  ? rawRow.proof_sufficiency_state
381
240
  : "",
@@ -389,7 +248,13 @@ function normalizeSafetyRow(rawRow) {
389
248
  intended_consumption: inList(derivation.intended_consumption, INTENDED_CONSUMPTIONS)
390
249
  ? derivation.intended_consumption
391
250
  : "",
392
- derived_from_axes: arrayValues(derivation.derived_from_axes, SOURCE_SAFETY_CANONICAL_AXES),
251
+ // Keep every string entry as-is (do NOT filter to the canonical set): a stale
252
+ // or tampered row carrying retired axes (e.g. privacy_state/redaction_state)
253
+ // must reach validateCanonicalAxes and fail `visibility_axis_set_invalid`,
254
+ // not be silently laundered down to exactly the four canonical axes.
255
+ derived_from_axes: (Array.isArray(derivation.derived_from_axes)
256
+ ? derivation.derived_from_axes.filter((axis) => typeof axis === "string")
257
+ : []),
393
258
  derivation_rule_ref: typeof derivation.derivation_rule_ref === "string"
394
259
  ? derivation.derivation_rule_ref
395
260
  : "",
@@ -397,13 +262,6 @@ function normalizeSafetyRow(rawRow) {
397
262
  authorization_scope_ref: typeof rawRow.authorization_scope_ref === "string"
398
263
  ? rawRow.authorization_scope_ref
399
264
  : null,
400
- redaction_evidence: {
401
- raw_value_available: redactionEvidence.raw_value_available === true,
402
- allowed_proof_forms: arrayValues(redactionEvidence.allowed_proof_forms, ALLOWED_PROOF_FORMS),
403
- redaction_rule_ref: typeof redactionEvidence.redaction_rule_ref === "string"
404
- ? redactionEvidence.redaction_rule_ref
405
- : null,
406
- },
407
265
  tombstone: {
408
266
  tombstone_ref: typeof tombstone.tombstone_ref === "string" ? tombstone.tombstone_ref : null,
409
267
  reason: typeof tombstone.reason === "string" ? tombstone.reason : null,
@@ -418,7 +276,25 @@ function normalizeSafetyRow(rawRow) {
418
276
  };
419
277
  }
420
278
  export function validateSourceSafetyLedger(args) {
279
+ assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
421
280
  const violations = [];
281
+ // G(a) deferred-7 slice 1: record the three obligations this validator fully enforces. Stamped here,
282
+ // before the per-row loop, so they fire on zero-row input (the enforcement sites exist unconditionally).
283
+ // asserted_obligation_ids is in-memory-only telemetry (Stage 0 #145): it is stripped at the write
284
+ // boundary and excluded from reuseMatchArtifactHash, so stamping this reuse-hashed validation artifact
285
+ // does not rotate reuse provenance. PARKED (see obligation-coverage-ledger.yaml):
286
+ // - preserve_..._consumption_boundaries: no independent "no substitution" enforcer (the per-consumption
287
+ // required rows are the only related check), so it cannot bind non-overlappingly.
288
+ // - validate_every_observation_has_source_safety_rows_for_each_intended_consumption (codex #147 P1):
289
+ // the required-row pass only proves each source_safety:<obs>:<consumption> ID STRING is present; it
290
+ // never binds that ID suffix to the row's visibility_derivation.intended_consumption (the field
291
+ // deriveSourceSafetyVisibilityTier uses). So a public_output-ID row can carry prompt_context
292
+ // derivation and a tier derived for the wrong consumption, pass, and mislead downstream lookup-by-ID.
293
+ // PARK pending an ID-suffix ↔ intended_consumption binding.
294
+ const assertedObligationIds = [];
295
+ assertObligation(assertedObligationIds, "validate_exactly_four_canonical_source_safety_axes");
296
+ assertObligation(assertedObligationIds, "validate_source_safety_subject_refs_against_observed_source_refs");
297
+ assertObligation(assertedObligationIds, "validate_visibility_tier_is_derived_not_independent_authority");
422
298
  const rawLedger = args.sourceSafetyLedger;
423
299
  if (!isRecord(rawLedger) || !Array.isArray(rawLedger.safety_rows)) {
424
300
  violations.push(violation({
@@ -509,13 +385,6 @@ export function validateSourceSafetyLedger(args) {
509
385
  allowed: AUTHORIZATION_STATES,
510
386
  axis: "authorization_state",
511
387
  },
512
- { field: "privacy_state", value: row.privacy_state, allowed: PRIVACY_STATES, axis: "privacy_state" },
513
- {
514
- field: "redaction_state",
515
- value: row.redaction_state,
516
- allowed: REDACTION_STATES,
517
- axis: "redaction_state",
518
- },
519
388
  {
520
389
  field: "proof_sufficiency_state",
521
390
  value: row.proof_sufficiency_state,
@@ -542,15 +411,7 @@ export function validateSourceSafetyLedger(args) {
542
411
  subjectId: row.safety_row_id,
543
412
  }));
544
413
  }
545
- if (row.redaction_evidence.allowed_proof_forms.length === 0) {
546
- violations.push(violation({
547
- code: "missing_required_field",
548
- message: "source safety row must name at least one allowed proof form",
549
- subjectId: row.safety_row_id,
550
- }));
551
- }
552
414
  validateCanonicalAxes({ row, violations });
553
- validateSupportingDetailConsistency({ row, violations });
554
415
  if (VISIBILITY_TIERS.includes(row.visibility_tier)) {
555
416
  const expected = deriveSourceSafetyVisibilityTier(row);
556
417
  if (row.visibility_tier !== expected) {
@@ -561,6 +422,34 @@ export function validateSourceSafetyLedger(args) {
561
422
  }));
562
423
  }
563
424
  }
425
+ // Defect-3 D3 (basis-attribution enforcement): an outward consumption tier
426
+ // (material_claim/public_output) that actually REACHES consumption_allowed MUST
427
+ // be justified by a canonical basis — A (the observation is the runtime target,
428
+ // is_runtime_target_source) or B (explicit source self-declaration). Without
429
+ // this, a forged/replayed row could reach consumption_allowed on a
430
+ // frontier-discovered/non-target source (the four axes alone don't bind to the
431
+ // basis). The trigger keys on the DERIVED outcome, not a single
432
+ // authorization_state literal: both "authorized" AND "not_required" derive to
433
+ // consumption_allowed for these tiers, so checking only "authorized" left a
434
+ // "not_required" bypass. Internal tiers are exempt (runtime read scope), and a
435
+ // legit non-target row derives to no_prompt_use (producer emits "unknown"), so
436
+ // D3 stays silent there.
437
+ const consumption = row.visibility_derivation.intended_consumption;
438
+ if ((consumption === "material_claim" || consumption === "public_output") &&
439
+ deriveSourceSafetyVisibilityTier(row) === "consumption_allowed" &&
440
+ expectedObservation) {
441
+ const basisA = expectedObservation.is_runtime_target_source === true;
442
+ const basisB = explicitConsumptionAuthorizations(expectedObservation)
443
+ .has(consumption);
444
+ if (!basisA && !basisB) {
445
+ violations.push(violation({
446
+ code: "unjustified_consumption_authorization",
447
+ message: `source safety row ${row.safety_row_id} reaches consumption_allowed for ${consumption} without a runtime-target-provenance (A) or explicit-source (B) authorization basis`,
448
+ subjectId: row.safety_row_id,
449
+ axis: "authorization_state",
450
+ }));
451
+ }
452
+ }
564
453
  }
565
454
  for (const requiredSafetyRowId of requiredSafetyRowIds) {
566
455
  if (!rowIds.has(requiredSafetyRowId)) {
@@ -582,10 +471,10 @@ export function validateSourceSafetyLedger(args) {
582
471
  safety_row_count: rows.length,
583
472
  no_prompt_use_count: rows.filter((row) => row.visibility_tier === "no_prompt_use")
584
473
  .length,
585
- redacted_output_only_count: rows.filter((row) => row.visibility_tier === "redacted_output_only").length,
586
474
  validation_results: violations.length === 0
587
475
  ? ["source_safety_ledger_valid"]
588
476
  : ["source_safety_ledger_invalid"],
477
+ asserted_obligation_ids: assertedObligationIds,
589
478
  violations,
590
479
  };
591
480
  }
@@ -1,7 +1,9 @@
1
1
  import crypto from "node:crypto";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
+ import { parse as parseYaml } from "yaml";
5
+ import { assertArrayField, atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
6
+ import { assertObligation } from "./obligation-assertion.js";
5
7
  import { sourceSafetyRowIdForObservation } from "./source-safety-validation.js";
6
8
  const SIGNAL_AXES = [
7
9
  "actor",
@@ -111,10 +113,6 @@ function inList(value, allowed) {
111
113
  async function readYamlDocument(filePath) {
112
114
  return parseYaml(await fs.readFile(filePath, "utf8"));
113
115
  }
114
- async function writeYamlDocument(filePath, value) {
115
- await fs.mkdir(path.dirname(filePath), { recursive: true });
116
- await fs.writeFile(filePath, stringifyYaml(value), "utf8");
117
- }
118
116
  async function sha256File(filePath) {
119
117
  if (!filePath)
120
118
  return null;
@@ -139,12 +137,15 @@ function violation(args) {
139
137
  };
140
138
  }
141
139
  function safetyRowById(sourceSafetyLedger) {
140
+ assertArrayField(sourceSafetyLedger.safety_rows, "source-safety-ledger", "safety_rows");
142
141
  return new Map(sourceSafetyLedger.safety_rows.map((row) => [row.safety_row_id, row]));
143
142
  }
144
143
  function selectedProfileRefForObservation(args) {
144
+ assertArrayField(args.targetMaterialProfile.selected_source_profiles, "target-material-profile", "selected_source_profiles");
145
145
  return args.targetMaterialProfile.selected_source_profiles.find((profile) => profile.target_material_kind === args.observation.target_material_kind)?.profile_ref ?? null;
146
146
  }
147
147
  function scoutScopeState(args) {
148
+ assertArrayField(args.targetMaterialProfile.target_material_kind_candidates, "target-material-profile", "target_material_kind_candidates");
148
149
  if (args.targetMaterialProfile.target_material_kind === "mixed" &&
149
150
  args.targetMaterialProfile.target_material_kind_candidates.some((kind) => kind === "code" || kind === "document")) {
150
151
  return "member_scoped_composite";
@@ -172,7 +173,6 @@ function visibilityStateForSafetyRow(row) {
172
173
  case "consumption_allowed":
173
174
  return "prompt_visible";
174
175
  case "internal_only":
175
- case "redacted_output_only":
176
176
  return "redacted";
177
177
  case "no_prompt_use":
178
178
  case "no_replay_use":
@@ -315,6 +315,10 @@ function buildCoverageSlots(args) {
315
315
  });
316
316
  }
317
317
  export function buildSourceScoutPackFromArtifacts(args) {
318
+ assertArrayField(args.sourceSafetyLedger.safety_rows, "source-safety-ledger", "safety_rows");
319
+ assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
320
+ assertArrayField(args.targetMaterialProfile.selected_source_profiles, "target-material-profile", "selected_source_profiles");
321
+ assertArrayField(args.targetMaterialProfile.target_refs, "target-material-profile", "target_refs");
318
322
  const scopeState = scoutScopeState({
319
323
  targetMaterialProfile: args.targetMaterialProfile,
320
324
  targetMaterialProfileValidation: args.targetMaterialProfileValidation,
@@ -398,7 +402,46 @@ function validateNoSelectedPurposeLeak(sourceScoutPack, violations) {
398
402
  }
399
403
  }
400
404
  export async function validateSourceScoutPack(args) {
405
+ assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
406
+ assertArrayField(args.sourceScoutPack.profile_scout_coverage_slots, "source-scout-pack", "profile_scout_coverage_slots");
401
407
  const violations = [];
408
+ // G(a) deferred-7 slice 5 (LAST): ONE mode-agnostic fn is shared by THREE registry validator_ids
409
+ // (source-scout-pack[-pre-seed|-post-maturation]-validator) — no mode arg, one uniform check set. The harvest
410
+ // attributes this asserted set to ALL 3 validator_ids; recording only an obligation registered for all 3
411
+ // (no registry-absent pair). asserted_obligation_ids is in-memory-only telemetry (Stage 0 #145) on a
412
+ // reuse-hashed (run.ts source_scout_pack_validation_sha256) + scout-captured (sha256File raw-file) artifact,
413
+ // so stamping rotates neither channel. Stamped before the per-row loop so it fires on zero signal rows.
414
+ //
415
+ // RECORD 1 (shared across all 3 modes, uniformly enforced):
416
+ // - validate_signal_rows_resolve_to_source_observations → signal_observation_missing (per-row, UNCONDITIONAL:
417
+ // every signal row's observation_id must resolve in source-observations; +per-row content-sha match). The
418
+ // passed observations are bound to the chain via source_observations_hash_mismatch (pack snapshot hash vs
419
+ // sha256File(ref)); the resolution check itself does not depend on that pinning.
420
+ //
421
+ // PARKED 8 (ledger notes carry detail):
422
+ // - validate_prompt_visible_rows_have_source_safety_validation_refs (×3): the named per-row field
423
+ // source_safety_ledger_validation_ref is BUILT but never read by the validator — it checks source_safety_
424
+ // row_id (ledger row) + global validation_status + tier instead, so the named validation-ref field is
425
+ // unenforced (a row with valid row_id but null/wrong validation_ref passes).
426
+ // - validate_group_and_coverage_refs_resolve (base): "group" has no enforcement surface (no group structure);
427
+ // only coverage_slot→signal_row resolution (coverage_slot_signal_missing) is checked → compound, half missing.
428
+ // - validate_scout_pack_is_profile_local_and_contains_no_selected_purpose_required_element_refs (base):
429
+ // validateNoSelectedPurposeLeak is a JSON substring search (no-purpose-leak half, fragile); "profile-local"
430
+ // is never checked → compound, half missing.
431
+ // - validate_scout_scope_derives_from_target_material_profile_validation (base) /
432
+ // validate_snapshot_scope_derives_from_target_material_profile_validation (pre-seed+post-maturation):
433
+ // scout_scope is copied from the pack; only internal consistency (unsupported_scope_overclaimed) is checked,
434
+ // never recomputed/derived from the target-material-profile-validation content (scoutScopeState() unused here).
435
+ // - validate_snapshot_lineage_validation_ref_and_hash_match_current_lineage_authority (pre-seed+post-maturation):
436
+ // slice-3 null-read + caller-authority — sha256File(null)=null so a null lineage ref + null pack hash passes,
437
+ // and the "current lineage authority" is the caller-supplied ref, not derived from the chain.
438
+ // - validate_pre_seed_scout_snapshot_is_immutable_consumed_authority (pre-seed) /
439
+ // validate_post_maturation_scout_snapshot_is_immutable_audit_authority (post-maturation): only INPUT-artifact
440
+ // hashes (freshness) are checked; snapshot immutability / consumed-or-audit authority state is not enforced.
441
+ const assertedObligationIds = [];
442
+ assertObligation(assertedObligationIds, "validate_signal_rows_resolve_to_source_observations");
443
+ // signal_rows is this validator's primary subject and is already shape-checked
444
+ // gracefully below (→ schema_shape_invalid violation), so it is NOT asserted.
402
445
  const rawPack = args.sourceScoutPack;
403
446
  if (!isRecord(rawPack) || !Array.isArray(rawPack.signal_rows)) {
404
447
  violations.push(violation({
@@ -645,6 +688,7 @@ export async function validateSourceScoutPack(args) {
645
688
  validation_results: violations.length === 0
646
689
  ? ["source_scout_pack_valid"]
647
690
  : ["source_scout_pack_invalid"],
691
+ asserted_obligation_ids: assertedObligationIds,
648
692
  violations,
649
693
  };
650
694
  }