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,6 +1,8 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
5
+ import { assertObligation } from "./obligation-assertion.js";
4
6
  import { ontologySeedClaimProjections, ontologySeedExcludedClaimIds, } from "./seed-claim-projections.js";
5
7
  import { collectOntologySeedRefs } from "./ontology-seed-validation.js";
6
8
  import { loadReconstructContractRegistry, } from "./contract-registry.js";
@@ -77,6 +79,16 @@ const REVISION_ACTIONS = [
77
79
  "reject",
78
80
  "defer",
79
81
  ];
82
+ // Shared revision-proposal predicates (M4: one blocker predicate used identically at the
83
+ // stop gate (run.ts) and the continuation gate (maturation-validation.ts); both import from
84
+ // here, the revision-proposal concept owner, so the two gates cannot drift). reject|defer
85
+ // block continuation/stop; every non-reuse proposal is disclosed.
86
+ export function isRevisionBlocker(proposal) {
87
+ return proposal.action === "reject" || proposal.action === "defer";
88
+ }
89
+ export function isRevisionDisclosed(proposal) {
90
+ return proposal.action !== "reuse";
91
+ }
80
92
  function isoNow() {
81
93
  return new Date().toISOString();
82
94
  }
@@ -162,7 +174,7 @@ function seedLimitationIds(seed) {
162
174
  .map((record) => stringField(record, "limitation_id"))
163
175
  .filter((id) => id !== null));
164
176
  }
165
- function knownSeedRefs(seed) {
177
+ export function knownSeedRefs(seed) {
166
178
  if (!seed)
167
179
  return new Set();
168
180
  const refs = collectOntologySeedRefs(seed);
@@ -563,6 +575,14 @@ function validateClaimRealizationMapAgainstClaims(args) {
563
575
  const seen = new Set();
564
576
  const stanceCounts = initCountMap(CLAIM_REALIZATION_STANCES);
565
577
  const observations = observationsById(args.sourceObservations);
578
+ // G(a) slice 7: record the three claim-realization obligations before the per-realization loop so
579
+ // they are proven wired on a zero-realization map. Each was audited to a distinct enforcement region
580
+ // (no laundering): require_exactly_one = duplicate_id (in-loop) + missing_required_coverage (the
581
+ // coverage loop below); claim_ids = unknown_id; evidence_refs = validateEvidenceRef vs source obs.
582
+ const assertedObligationIds = [];
583
+ assertObligation(assertedObligationIds, "require_exactly_one_realization_for_each_seed_claim");
584
+ assertObligation(assertedObligationIds, "validate_realization_claim_ids_against_ontology_seed_claim_ids");
585
+ assertObligation(assertedObligationIds, "validate_realization_evidence_refs_against_source_observations");
566
586
  for (const realization of args.claimRealizationMap.claim_realizations) {
567
587
  const subjectId = realization.claim_id;
568
588
  if (seen.has(subjectId)) {
@@ -635,6 +655,7 @@ function validateClaimRealizationMapAgainstClaims(args) {
635
655
  validation_results: violations.length === 0
636
656
  ? ["claim_realization_map_valid"]
637
657
  : ["claim_realization_map_invalid"],
658
+ asserted_obligation_ids: assertedObligationIds,
638
659
  violations,
639
660
  };
640
661
  }
@@ -890,6 +911,41 @@ export function validateCompetencyQuestionsForOntologySeed(args) {
890
911
  }
891
912
  function validateCompetencyQuestionsAgainstEligibleClaims(args) {
892
913
  const violations = [];
914
+ // G(a) slice 27: record the 9 obligations this validator FULLY enforces with a distinct,
915
+ // name-matching site. These are the REF-RESOLUTION obligations ("validate X refs against/close-to
916
+ // authority Y" — every PRESENT ref must resolve to its registry/seed/manifest authority), plus
917
+ // question-id uniqueness and the exactly-one admitted-competency disposition rule. Authority-backed
918
+ // stamps are GATED on the authoritative input being supplied (codex #142 R1): the thin
919
+ // validateCompetencyQuestions path passes no registry/seed/governing-snapshot, so a zero-question
920
+ // artifact must NOT claim those obligations were enforced against an absent authority. Each stamp is
921
+ // before the per-question loop so a zero-question set still records once the authority is present; the
922
+ // enforcement code always runs, only the stamp is gated.
923
+ // Parked (see obligation-coverage-ledger.yaml notes): emit_required_evidence_scope (PROJECTION_ONLY);
924
+ // classification_level_axis / hidden_label facet coverage (NOT_FOUND — fields absent); p3-diagnostic
925
+ // (no code); query/visualization/graph refs (presence-only — codex #142 R1); and — codex #142 R2 — the
926
+ // 8 *_facet_coverage obligations (presence-only: resolve present facets but never require facets when
927
+ // the axis is applicable) and validate_required_ontology_handoff_axis_coverage_or_limitation_backed_
928
+ // disposition (counts inactive/unsupported questions as coverage; no active/limitation-backed filter).
929
+ const assertedObligationIds = [];
930
+ // Pure question-set check — enforceable with no external authority.
931
+ assertObligation(assertedObligationIds, "require_unique_question_id");
932
+ // Registry-backed ref resolution — only when the contract registry authority is supplied.
933
+ if (args.contractRegistry) {
934
+ assertObligation(assertedObligationIds, "validate_coverage_axes_against_coverage_axis_registry");
935
+ assertObligation(assertedObligationIds, "validate_ontology_handoff_axes_against_ontology_handoff_axis_registry");
936
+ assertObligation(assertedObligationIds, "validate_reference_standard_refs_against_reference_standard_registry");
937
+ assertObligation(assertedObligationIds, "validate_pattern_catalog_refs_against_reference_pattern_catalog_registry");
938
+ }
939
+ // Seed-backed closure — only when the ontology seed authority is supplied.
940
+ if (args.ontologySeed) {
941
+ assertObligation(assertedObligationIds, "validate_seed_ref_refs_close_against_known_seed_refs");
942
+ assertObligation(assertedObligationIds, "validate_limitation_refs_close_against_handoff_limitations");
943
+ }
944
+ // Run-manifest governing-snapshot authority — only when the snapshot is supplied.
945
+ if (args.governingSnapshot) {
946
+ assertObligation(assertedObligationIds, "validate_domain_competency_trace_refs_against_run_manifest_context_snapshot");
947
+ assertObligation(assertedObligationIds, "enforce_admitted_domain_competency_disposition_rule");
948
+ }
893
949
  const normalizedCompetencyQuestions = normalizeCompetencyQuestionsAtBoundary(args.competencyQuestions);
894
950
  violations.push(...normalizedCompetencyQuestions.violations);
895
951
  const eligibleClaims = new Set(args.eligibleClaimIds);
@@ -1274,11 +1330,36 @@ function validateCompetencyQuestionsAgainstEligibleClaims(args) {
1274
1330
  validation_results: violations.length === 0
1275
1331
  ? ["competency_questions_valid"]
1276
1332
  : ["competency_questions_invalid"],
1333
+ asserted_obligation_ids: assertedObligationIds,
1277
1334
  violations,
1278
1335
  };
1279
1336
  }
1280
1337
  export function validateCompetencyQuestionAssessment(args) {
1281
1338
  const violations = [];
1339
+ // G(a) slice 12: record the three obligations with a distinct, name-matching enforcement site,
1340
+ // stamped unconditionally before the per-assessment / per-question loops so they fire on a
1341
+ // zero-assessment artifact:
1342
+ // - require_exactly_one_assessment_per_authoritative_question (duplicate_id on a repeat +
1343
+ // missing_required_coverage when a question has no assessment → exactly one).
1344
+ // - validate_downstream_effect_consistent_with_answer_status (downstream_effect must equal
1345
+ // expectedDownstreamEffect(answer_status): invalid_enum).
1346
+ // - validate_required_seed_refs_close_against_question_seed_refs (bidirectional closure:
1347
+ // unknown_id for a ref outside the question, missing_required_coverage for a missing one).
1348
+ // PARKED (not recorded), name broader than code — honest declared≠wired, NOT laundered (see
1349
+ // obligation-coverage-ledger.yaml notes):
1350
+ // - validate_answer_status_against_active_answerability_contract: the validator checks enum
1351
+ // validity + per-status STRUCTURAL requirements (answerable→evidence_refs/required_seed_refs,
1352
+ // unsupported/deferred→missing_source_or_confirmation), but never verifies the answer_status
1353
+ // reflects actual answerability from evidence CONTENT — the answerability judgment is the
1354
+ // content-blind host-LLM step, not validated here.
1355
+ // - validate_answerability_trace_refs_close_against_seed_evidence_limitations_and_proofs: there
1356
+ // is no answerability_trace_refs / limitation_refs / proof_refs field; the validator closes
1357
+ // only evidence_refs and required_seed_refs (the latter already recorded above), never
1358
+ // limitations or proofs.
1359
+ const assertedObligationIds = [];
1360
+ assertObligation(assertedObligationIds, "require_exactly_one_assessment_per_authoritative_question");
1361
+ assertObligation(assertedObligationIds, "validate_downstream_effect_consistent_with_answer_status");
1362
+ assertObligation(assertedObligationIds, "validate_required_seed_refs_close_against_question_seed_refs");
1282
1363
  const questionIds = new Set(args.competencyQuestions.questions.map((question) => question.question_id));
1283
1364
  const questionById = new Map(args.competencyQuestions.questions.map((question) => [
1284
1365
  question.question_id,
@@ -1453,6 +1534,7 @@ export function validateCompetencyQuestionAssessment(args) {
1453
1534
  validation_results: violations.length === 0
1454
1535
  ? ["competency_question_assessment_valid"]
1455
1536
  : ["competency_question_assessment_invalid"],
1537
+ asserted_obligation_ids: assertedObligationIds,
1456
1538
  violations,
1457
1539
  };
1458
1540
  }
@@ -1530,7 +1612,25 @@ export function validateFailureClassification(args) {
1530
1612
  }
1531
1613
  export function validateRevisionProposal(args) {
1532
1614
  const violations = [];
1615
+ // Every published proposal is carry-forward scope, so its target must resolve
1616
+ // to a real authority — not just the `failure` case. Claim/question targets
1617
+ // come from the failures' own claim_id/question_id; seed targets from the
1618
+ // ontology-seed's seed_refs (knownSeedRefs). An unresolved target is a
1619
+ // fabricated/stale carry-forward and is rejected as unknown_id.
1533
1620
  const failureIds = new Set(args.failureClassification.failures.map((failure) => failure.failure_id));
1621
+ const failureClaimIds = new Set(args.failureClassification.failures
1622
+ .map((failure) => failure.claim_id)
1623
+ .filter((claimId) => Boolean(claimId)));
1624
+ const failureQuestionIds = new Set(args.failureClassification.failures
1625
+ .map((failure) => failure.question_id)
1626
+ .filter((questionId) => Boolean(questionId)));
1627
+ const seedRefs = knownSeedRefs(args.ontologySeed);
1628
+ const targetAuthorities = {
1629
+ failure: { ids: failureIds, authority: "failure" },
1630
+ claim: { ids: failureClaimIds, authority: "failure claim" },
1631
+ question: { ids: failureQuestionIds, authority: "failure question" },
1632
+ seed: { ids: seedRefs, authority: "ontology-seed seed_ref" },
1633
+ };
1534
1634
  const seen = new Set();
1535
1635
  const actionCounts = initCountMap(REVISION_ACTIONS);
1536
1636
  for (const proposal of args.revisionProposal.proposals) {
@@ -1542,11 +1642,23 @@ export function validateRevisionProposal(args) {
1542
1642
  }));
1543
1643
  }
1544
1644
  seen.add(proposal.proposal_id);
1545
- if (proposal.target_type === "failure" &&
1546
- !failureIds.has(proposal.target_id)) {
1645
+ // target_type is the typed union for fresh-authored proposals, but resume /
1646
+ // manual artifact validation loads it from YAML, so a stale value (e.g. the
1647
+ // removed domain_context) can reach here. An unknown type has no authority to
1648
+ // validate against, so reject it as invalid_enum instead of skipping the
1649
+ // authority check and disclosing an unbacked target as carry-forward.
1650
+ const targetAuthority = targetAuthorities[proposal.target_type];
1651
+ if (!targetAuthority) {
1652
+ violations.push(violation({
1653
+ code: "invalid_enum",
1654
+ message: `invalid proposal target_type: ${proposal.target_type}`,
1655
+ subjectId: proposal.proposal_id,
1656
+ }));
1657
+ }
1658
+ else if (!targetAuthority.ids.has(proposal.target_id)) {
1547
1659
  violations.push(violation({
1548
1660
  code: "unknown_id",
1549
- message: `proposal references unknown failure: ${proposal.target_id}`,
1661
+ message: `proposal references unknown ${targetAuthority.authority}: ${proposal.target_id}`,
1550
1662
  subjectId: proposal.proposal_id,
1551
1663
  }));
1552
1664
  }
@@ -1626,10 +1738,6 @@ export function validateFinalOutputProvenance(args) {
1626
1738
  }
1627
1739
  return violations;
1628
1740
  }
1629
- async function writeYamlDocument(filePath, value) {
1630
- await fs.mkdir(path.dirname(filePath), { recursive: true });
1631
- await fs.writeFile(filePath, stringifyYaml(value), "utf8");
1632
- }
1633
1741
  async function readYamlDocument(filePath) {
1634
1742
  return parseYaml(await fs.readFile(filePath, "utf8"));
1635
1743
  }
@@ -1690,7 +1798,8 @@ export async function writeCompetencyQuestionsValidationForOntologySeedArtifact(
1690
1798
  readYamlDocument(args.ontologySeedPath),
1691
1799
  readYamlDocument(args.ontologySeedValidationPath),
1692
1800
  readYamlDocument(args.sourceObservationsPath),
1693
- loadReconstructContractRegistry({ registryPath: args.registryPath }),
1801
+ args.contractRegistry ??
1802
+ loadReconstructContractRegistry({ registryPath: args.registryPath }),
1694
1803
  ]);
1695
1804
  const manifest = args.governingSnapshot || !args.reconstructRunManifestPath
1696
1805
  ? null
@@ -1752,13 +1861,15 @@ export async function writeFailureClassificationValidationArtifact(args) {
1752
1861
  return validation;
1753
1862
  }
1754
1863
  export async function writeRevisionProposalValidationArtifact(args) {
1755
- const [revisionProposal, failureClassification] = await Promise.all([
1864
+ const [revisionProposal, failureClassification, ontologySeed] = await Promise.all([
1756
1865
  readYamlDocument(args.revisionProposalPath),
1757
1866
  readYamlDocument(args.failureClassificationPath),
1867
+ readYamlDocument(args.ontologySeedPath),
1758
1868
  ]);
1759
1869
  const validation = validateRevisionProposal({
1760
1870
  revisionProposal,
1761
1871
  failureClassification,
1872
+ ontologySeed,
1762
1873
  revisionProposalRef: path.resolve(args.revisionProposalPath),
1763
1874
  failureClassificationRef: path.resolve(args.failureClassificationPath),
1764
1875
  });
@@ -1,16 +1,12 @@
1
1
  import fs from "node:fs/promises";
2
- import path from "node:path";
3
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
2
+ import { parse as parseYaml } from "yaml";
3
+ import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
4
4
  function isoNow() {
5
5
  return new Date().toISOString();
6
6
  }
7
7
  async function readYamlDocument(filePath) {
8
8
  return parseYaml(await fs.readFile(filePath, "utf8"));
9
9
  }
10
- async function writeYamlDocument(filePath, value) {
11
- await fs.mkdir(path.dirname(filePath), { recursive: true });
12
- await fs.writeFile(filePath, stringifyYaml(value), "utf8");
13
- }
14
10
  function violation(args) {
15
11
  return {
16
12
  code: args.code,
@@ -1,6 +1,9 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
3
+ import { parse as parseYaml } from "yaml";
4
+ import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
5
+ import { assertObligation } from "./obligation-assertion.js";
6
+ import { TARGET_MATERIAL_KINDS } from "../target-material-kind.js";
4
7
  const PURPOSE_EVIDENCE_KINDS = [
5
8
  "P1",
6
9
  "P2",
@@ -8,16 +11,18 @@ const PURPOSE_EVIDENCE_KINDS = [
8
11
  "P4",
9
12
  "P5",
10
13
  ];
14
+ const PURPOSE_FRAME_STATUSES = [
15
+ "source_declared",
16
+ "evidence_inferred",
17
+ "limitation_backed",
18
+ "unresolved",
19
+ ];
11
20
  function isoNow() {
12
21
  return new Date().toISOString();
13
22
  }
14
23
  async function readYamlDocument(filePath) {
15
24
  return parseYaml(await fs.readFile(filePath, "utf8"));
16
25
  }
17
- async function writeYamlDocument(filePath, value) {
18
- await fs.mkdir(path.dirname(filePath), { recursive: true });
19
- await fs.writeFile(filePath, stringifyYaml(value), "utf8");
20
- }
21
26
  function isRecord(value) {
22
27
  return typeof value === "object" && value !== null && !Array.isArray(value);
23
28
  }
@@ -96,6 +101,22 @@ function hasNonEmptyString(value) {
96
101
  export function validateSourcePurposeCandidates(args) {
97
102
  const artifact = args.sourcePurposeCandidates;
98
103
  const violations = [];
104
+ // G(a) slice 24 — record the obligations this validator FULLY enforces with a distinct violation
105
+ // (RECORD 6/10). The other 4 stay parked (see obligation-coverage-ledger.yaml notes): derive_
106
+ // confirmation_required and expose_selected_..._id are PROJECTION-ONLY (computed into the return
107
+ // object with no violation → no breaching binding); require_purpose_candidate_status_field_to_be_
108
+ // purpose_source_status is PARTIAL (only the artifact-level alias-field rejection is enforced, no
109
+ // per-candidate purpose_source_status enum check); and require_exactly_one_primary_..._when_selected
110
+ // is enforced UNCONDITIONALLY (blind to the "when selected" discriminator the contract scopes it to —
111
+ // slice-20 superset-blind park). Stamped before any per-candidate guard so the recorder fires on
112
+ // zero-candidate input.
113
+ const assertedObligationIds = [];
114
+ assertObligation(assertedObligationIds, "preserve_contradictions_in_candidate_status_or_limitations");
115
+ assertObligation(assertedObligationIds, "reject_p5_only_primary_purpose_candidate");
116
+ assertObligation(assertedObligationIds, "require_mixed_purpose_frame_elements_to_carry_member_scope_refs_member_target_material_kind_member_source_refs_and_cross_material_ref_refs_or_validated_exclusion");
117
+ assertObligation(assertedObligationIds, "require_non_p1_primary_to_have_two_evidence_kinds_and_one_p2_p3_or_p4");
118
+ assertObligation(assertedObligationIds, "require_purpose_adequacy_frame_elements_to_have_surface_dimension_facet_and_evidence_or_limitation_state");
119
+ assertObligation(assertedObligationIds, "validate_purpose_candidate_evidence_refs_against_source_observations");
99
120
  const evidenceIndex = observationEvidenceIndex(args.sourceObservations);
100
121
  const rawArtifact = artifact;
101
122
  if ("source_purpose_status" in rawArtifact || "inference_status" in rawArtifact) {
@@ -220,12 +241,74 @@ export function validateSourcePurposeCandidates(args) {
220
241
  subjectId: candidate.purpose_candidate_id,
221
242
  }));
222
243
  }
223
- if (!Array.isArray(frame.required_elements) || frame.required_elements.length === 0) {
244
+ // The frame header (frame_kind, frame_status, material_kind_requirements) is required for
245
+ // every candidate regardless of rank. The direct-call mapper enforces it on the authoring
246
+ // path; this validator must enforce it on the disk/reuse path (source-purpose-candidates.yaml
247
+ // is parsed without schema validation) so a malformed frame is never blessed as valid. Only
248
+ // required_elements is rank-dependent — a rejected candidate may leave it empty (below).
249
+ if (!hasNonEmptyString(frame.frame_kind)) {
250
+ violations.push(violation({
251
+ code: "required_element_missing",
252
+ message: `${candidatePath}.adequacy_frame.frame_kind must be non-empty`,
253
+ subjectId: candidate.purpose_candidate_id,
254
+ }));
255
+ }
256
+ if (!PURPOSE_FRAME_STATUSES.includes(frame.frame_status)) {
257
+ violations.push(violation({
258
+ code: "invalid_enum",
259
+ message: `${candidatePath}.adequacy_frame.frame_status is not a valid frame status`,
260
+ subjectId: candidate.purpose_candidate_id,
261
+ }));
262
+ }
263
+ const requirements = frame.material_kind_requirements;
264
+ if (typeof requirements !== "object" || requirements === null) {
224
265
  violations.push(violation({
225
266
  code: "required_element_missing",
226
- message: `${candidatePath}.adequacy_frame.required_elements must not be empty`,
267
+ message: `${candidatePath}.adequacy_frame.material_kind_requirements is required`,
227
268
  subjectId: candidate.purpose_candidate_id,
228
269
  }));
270
+ }
271
+ else {
272
+ if (!TARGET_MATERIAL_KINDS.includes(requirements.target_material_kind)) {
273
+ violations.push(violation({
274
+ code: "invalid_enum",
275
+ message: `${candidatePath}.adequacy_frame.material_kind_requirements.target_material_kind is invalid`,
276
+ subjectId: candidate.purpose_candidate_id,
277
+ }));
278
+ }
279
+ if (!Array.isArray(requirements.required_facets)) {
280
+ violations.push(violation({
281
+ code: "required_element_missing",
282
+ message: `${candidatePath}.adequacy_frame.material_kind_requirements.required_facets must be an array`,
283
+ subjectId: candidate.purpose_candidate_id,
284
+ }));
285
+ }
286
+ if (!hasNonEmptyString(requirements.rationale)) {
287
+ violations.push(violation({
288
+ code: "required_element_missing",
289
+ message: `${candidatePath}.adequacy_frame.material_kind_requirements.rationale must be non-empty`,
290
+ subjectId: candidate.purpose_candidate_id,
291
+ }));
292
+ }
293
+ }
294
+ if (!Array.isArray(frame.required_elements) || frame.required_elements.length === 0) {
295
+ // Rejected candidates record a considered-and-excluded alternative for provenance,
296
+ // not an active adequacy frame, so they may leave required_elements as an empty array
297
+ // (the full frame header above is still required for every rank). Mirrors the rejected
298
+ // exemption for supporting evidence. Only the empty-array case is exempt: a non-array
299
+ // required_elements still violates the artifact contract for every rank, and
300
+ // non-rejected candidates must still carry frame elements. When a rejected candidate
301
+ // does provide elements, the element-format checks below still apply.
302
+ const rejectedEmptyArray = candidate.rank === "rejected" &&
303
+ Array.isArray(frame.required_elements) &&
304
+ frame.required_elements.length === 0;
305
+ if (!rejectedEmptyArray) {
306
+ violations.push(violation({
307
+ code: "required_element_missing",
308
+ message: `${candidatePath}.adequacy_frame.required_elements must not be empty`,
309
+ subjectId: candidate.purpose_candidate_id,
310
+ }));
311
+ }
229
312
  continue;
230
313
  }
231
314
  for (const [elementIndex, element] of frame.required_elements.entries()) {
@@ -292,6 +375,7 @@ export function validateSourcePurposeCandidates(args) {
292
375
  validation_results: violations.length === 0
293
376
  ? ["source_purpose_candidates_valid"]
294
377
  : ["source_purpose_candidates_invalid"],
378
+ asserted_obligation_ids: assertedObligationIds,
295
379
  violations,
296
380
  };
297
381
  }
@@ -299,6 +383,33 @@ export function validatePurposeConfirmation(args) {
299
383
  const confirmation = args.purposeConfirmation;
300
384
  const sourceValidation = args.sourcePurposeCandidatesValidation;
301
385
  const violations = [];
386
+ // G(a) slice 11: record the three obligations whose enforcement matches the authoritative contract
387
+ // (ontology-seeding-and-maturation-design.md §purpose-confirmation; confirmation_required is the
388
+ // derived inferred/limitation_backed predicate and "the only source for the purpose-confirmation
389
+ // gate"). All three reach a distinct, name-matching enforcement site below; stamped unconditionally
390
+ // up front so they fire on any input:
391
+ // - validate_confirmation_status_against_selected_purpose_candidate (selected_primary_mismatch
392
+ // when purpose_candidate_id != selected_purpose_candidate_id — distinct site).
393
+ // - require_confirmation_for_inferred_or_limitation_backed_purpose (when confirmation_required is
394
+ // true a non-confirmed status — e.g. not_required — blocks: conflicting_state). Per the contract
395
+ // the validator consumes confirmation_required rather than re-deriving inferred/limitation_backed.
396
+ // - block_seed_readiness_when_confirmation_is_pending_rejected_unavailable_or_evidence_check_pending
397
+ // (each of those statuses blocks seed readiness even when confirmation is not required).
398
+ // The two former obligations are DISTINCT facets with non-overlapping breaching inputs: ob "require"
399
+ // is bound by confirmation_required=true + status=not_required (a status not in the "block" list);
400
+ // ob "block" is bound by confirmation_required=false + status=pending (no confirmation required, yet
401
+ // it still blocks). PARKED (not recorded), name broader than code — honest declared≠wired, NOT
402
+ // laundered (see obligation-coverage-ledger.yaml notes):
403
+ // - require_revised_confirmation_to_preserve_source_conflict_or_trigger_purpose_discovery_rerun:
404
+ // only the rerun arm is enforced (revised_pending_evidence_check → must_rerun_purpose_discovery);
405
+ // the "preserve source conflict" arm is never checked — source_conflict_policy is never read.
406
+ // - validate_confirmation_status_against_source_purpose_candidate_status_and_validation_confirmation_required:
407
+ // the validator reads only sourceValidation.validation_status and confirmation_required, never the
408
+ // candidate's own purpose_source_status the name references.
409
+ const assertedObligationIds = [];
410
+ assertObligation(assertedObligationIds, "validate_confirmation_status_against_selected_purpose_candidate");
411
+ assertObligation(assertedObligationIds, "require_confirmation_for_inferred_or_limitation_backed_purpose");
412
+ assertObligation(assertedObligationIds, "block_seed_readiness_when_confirmation_is_pending_rejected_unavailable_or_evidence_check_pending");
302
413
  if (confirmation.session_id !== sourceValidation.session_id) {
303
414
  violations.push(violation({
304
415
  code: "session_id_mismatch",
@@ -387,6 +498,7 @@ export function validatePurposeConfirmation(args) {
387
498
  validation_results: violations.length === 0
388
499
  ? ["purpose_confirmation_valid"]
389
500
  : ["purpose_confirmation_invalid"],
501
+ asserted_obligation_ids: assertedObligationIds,
390
502
  violations,
391
503
  };
392
504
  }
@@ -1,7 +1,8 @@
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 { atomicWriteYamlDocument } from "../artifact-io.js";
5
6
  const RECORD_ARTIFACT_KEYS = [
6
7
  "reconstruct_run_control",
7
8
  "reconstruct_run_control_validation",
@@ -14,11 +15,21 @@ const RECORD_ARTIFACT_KEYS = [
14
15
  "source_inventory",
15
16
  "initial_source_frontier",
16
17
  "source_observations",
18
+ "seed_stage_prompt_source_observations",
17
19
  "source_observation_delta",
18
20
  "source_observation_delta_validation",
19
21
  "source_observation_reentry_validation",
20
22
  "source_observation_lineage_index",
21
23
  "source_observation_lineage_index_validation",
24
+ // W3 code review W3-001: normalizeRefs silently DROPS any typed ref key missing from this list —
25
+ // leaf_read_census was typed on ReconstructRecordArtifactRefs but dropped here (pre-existing
26
+ // latent type↔behavior mismatch); registered together with the semantic_map refs so the record
27
+ // (the durable primary evidence artifact) carries what the type declares.
28
+ "leaf_read_census",
29
+ "dispatch_incomplete",
30
+ "semantic_map_census",
31
+ "semantic_map_sidecar",
32
+ "semantic_map_resume_validation",
22
33
  "source_safety_ledger",
23
34
  "source_safety_ledger_validation",
24
35
  "source_scout_pack",
@@ -78,6 +89,8 @@ const RECORD_ARTIFACT_KEYS = [
78
89
  "maturation_authority_response_validation",
79
90
  "answer_support_ledger",
80
91
  "answer_support_ledger_validation",
92
+ "answer_support_judgment",
93
+ "answer_support_judgment_validation",
81
94
  "maturation_answer_claims",
82
95
  "maturation_answer_claims_validation",
83
96
  "ontology_expansion",
@@ -303,6 +316,30 @@ function buildWarnings(args) {
303
316
  }
304
317
  return warnings;
305
318
  }
319
+ /**
320
+ * The single canonical terminal-status projection over a reconstruct record (design §16.7).
321
+ *
322
+ * "Is this run done, and how?" is judged here and nowhere else: `getRunStatus` (the `status`
323
+ * field), `deriveReconstructProgress` (liveness / poll interval) and the TUI `deriveWorkflowStatus`
324
+ * all derive from this one function, so a graceful terminal is treated uniformly as terminal
325
+ * without each consumer re-checking `record_stage === "completed"` (the removed "diamond").
326
+ * `terminal_disposition` present ⇒ blocked/limited (terminal, stop polling, NOT "completed");
327
+ * else the raw `record_stage` ("completed" when the pipeline finished, otherwise in-progress).
328
+ */
329
+ export function reconstructTerminalStatus(record) {
330
+ return record.terminal_disposition ?? record.record_stage;
331
+ }
332
+ /**
333
+ * Record-validator invariant (design §16.7): a graceful terminal never reached the terminal stage,
334
+ * so `terminal_disposition` and `record_stage === "completed"` are mutually exclusive. Enforced at
335
+ * the write authority ({@link assembleReconstructRecord}) so no consumer can observe an incoherent
336
+ * pairing that {@link reconstructTerminalStatus} could not sensibly project.
337
+ */
338
+ export function assertReconstructTerminalDispositionCoherent(recordStage, terminalDisposition) {
339
+ if (terminalDisposition && recordStage === "completed") {
340
+ throw new Error(`reconstruct record invariant: terminal_disposition=${terminalDisposition} cannot pair with record_stage="completed"`);
341
+ }
342
+ }
306
343
  export async function assembleReconstructRecord(params) {
307
344
  const sessionRoot = path.resolve(params.sessionRoot);
308
345
  const sessionId = path.basename(sessionRoot);
@@ -377,17 +414,25 @@ export async function assembleReconstructRecord(params) {
377
414
  stopDecisionPresent: await exists(artifactRefs.stop_decision),
378
415
  finalOutputPresent: await exists(artifactRefs.final_output),
379
416
  });
417
+ assertReconstructTerminalDispositionCoherent(recordStage, params.terminalDisposition);
380
418
  const record = {
381
419
  schema_version: "1",
382
420
  reconstruct_record_id: `reconstruct-record:${sessionId}`,
383
421
  session_id: sessionId,
384
422
  entrypoint: "reconstruct",
385
423
  record_stage: recordStage,
424
+ // Present only on a graceful terminal; conditional spread keeps a normal record byte-identical.
425
+ ...(params.terminalDisposition
426
+ ? { terminal_disposition: params.terminalDisposition }
427
+ : {}),
386
428
  created_at: now,
387
429
  updated_at: now,
388
430
  target_material_kind: targetMaterialProfile?.target_material_kind ?? null,
389
431
  support_status: targetMaterialProfile?.support_status ?? null,
390
432
  artifact_refs: artifactRefs,
433
+ ...(params.dispatchFallback && recordStage === "completed"
434
+ ? { dispatch_fallback: structuredClone(params.dispatchFallback) }
435
+ : {}),
391
436
  artifact_integrity: artifactIntegrity,
392
437
  validation_summary: {
393
438
  target_material_profile_status: targetMaterialProfileStatus,
@@ -494,6 +539,7 @@ export async function assembleReconstructRecord(params) {
494
539
  "maturation_authority_response",
495
540
  "maturation_authority_response_validation",
496
541
  "answer_support_ledger_validation",
542
+ "answer_support_judgment_validation",
497
543
  "maturation_answer_claims_validation",
498
544
  "ontology_expansion_validation",
499
545
  "maturation_source_delta",
@@ -536,6 +582,7 @@ export async function assembleReconstructRecord(params) {
536
582
  "maturation_question_frontier",
537
583
  "maturation_closure_frontier",
538
584
  "answer_support_ledger",
585
+ "answer_support_judgment",
539
586
  "maturation_answer_claims",
540
587
  "ontology_expansion",
541
588
  "final_output",
@@ -556,7 +603,6 @@ export async function assembleReconstructRecord(params) {
556
603
  const outputPath = params.outputPath
557
604
  ? path.resolve(params.outputPath)
558
605
  : path.join(sessionRoot, "reconstruct-record.yaml");
559
- await fs.mkdir(path.dirname(outputPath), { recursive: true });
560
- await fs.writeFile(outputPath, stringifyYaml(record), "utf8");
606
+ await atomicWriteYamlDocument(outputPath, record);
561
607
  return record;
562
608
  }