onto-mcp 0.4.12 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +137 -0
- package/.onto/domains/accounting-kr/competency_qs.md +153 -153
- package/.onto/processes/reconstruct/obligation-coverage-ledger.yaml +842 -0
- package/.onto/processes/reconstruct/obligation-coverage-recorded.yaml +225 -0
- package/.onto/processes/reconstruct/ontology-seeding-and-maturation-design.md +93 -54
- package/.onto/processes/reconstruct/reconstruct-contract-registry.yaml +169 -8
- package/.onto/processes/reconstruct/reconstruct-execution-ux-contract.md +72 -1
- package/.onto/processes/reconstruct/source-profiles/spreadsheet.md +35 -0
- package/.onto/processes/review/productized-live-path.md +9 -5
- package/.onto/processes/review/prompt-execution-runner-contract.md +32 -8
- package/.onto/processes/review/review-target-profile-contract.md +47 -4
- package/.onto/processes/shared/pipeline-execution-ledger-contract.md +11 -0
- package/.onto/processes/shared/target-material-kind-contract.md +1 -1
- package/README.md +64 -1
- package/dist/cli.js +11 -1
- package/dist/core-api/reconstruct-api.js +575 -24
- package/dist/core-api/review-api.js +70 -5
- package/dist/core-api/review-progress.js +7 -0
- package/dist/core-api/runtime-observation.js +7 -0
- package/dist/core-runtime/artifact-io.js +75 -3
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +19 -8
- package/dist/core-runtime/cli/materialize-review-prompt-packets.js +68 -14
- package/dist/core-runtime/cli/prepare-review-session.js +34 -1
- package/dist/core-runtime/cli/render-review-final-output.js +2 -1
- package/dist/core-runtime/cli/run-review-prompt-execution.js +758 -48
- package/dist/core-runtime/cli/run-seats.js +18 -0
- package/dist/core-runtime/cli/runtime-submit-context.js +10 -2
- package/dist/core-runtime/cli/unit-resubmit.js +269 -0
- package/dist/core-runtime/cli/worker-structured-output.js +80 -2
- package/dist/core-runtime/discovery/host-detection.js +71 -22
- package/dist/core-runtime/discovery/review-cert-assemble.js +208 -0
- package/dist/core-runtime/discovery/review-cert-record.js +442 -0
- package/dist/core-runtime/discovery/review-execution-units.js +22 -0
- package/dist/core-runtime/discovery/seat-inventory.js +167 -0
- package/dist/core-runtime/discovery/settings-chain.js +311 -52
- package/dist/core-runtime/discovery/supported-models.js +282 -29
- package/dist/core-runtime/discovery/synthesize-cert-assemble.js +197 -0
- package/dist/core-runtime/discovery/synthesize-cert-capsule.js +399 -0
- package/dist/core-runtime/discovery/synthesize-cert-judge.js +340 -0
- package/dist/core-runtime/discovery/synthesize-cert-loop.js +234 -0
- package/dist/core-runtime/discovery/synthesize-cert-mutation.js +117 -0
- package/dist/core-runtime/discovery/synthesize-cert-packet.js +182 -0
- package/dist/core-runtime/discovery/synthesize-cert-record.js +831 -0
- package/dist/core-runtime/discovery/synthesize-cert-sampler.js +336 -0
- package/dist/core-runtime/effort-calibration-ingest.js +135 -0
- package/dist/core-runtime/llm/dispatch-breaker.js +412 -0
- package/dist/core-runtime/llm/dispatch-fallback-adapter-capabilities.js +33 -0
- package/dist/core-runtime/llm/llm-caller.js +188 -47
- package/dist/core-runtime/llm/mock-llm-realization.js +35 -4
- package/dist/core-runtime/llm/model-switcher.js +1 -1
- package/dist/core-runtime/llm/openai-responses-incomplete-error.js +46 -0
- package/dist/core-runtime/llm/sealed-dispatch-capability.js +359 -0
- package/dist/core-runtime/llm/structured-dispatch-error.js +15 -0
- package/dist/core-runtime/onboard/claude-profile-scan.js +65 -0
- package/dist/core-runtime/onboard/cli-host.js +5 -2
- package/dist/core-runtime/onboard/configure-provider.js +33 -1
- package/dist/core-runtime/onboard/host-target.js +28 -3
- package/dist/core-runtime/onboard/register.js +54 -12
- package/dist/core-runtime/pipeline-execution-ledger.js +10 -0
- package/dist/core-runtime/reconstruct/artifact-types.js +35 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +17 -0
- package/dist/core-runtime/reconstruct/competency-projection-contract.js +63 -0
- package/dist/core-runtime/reconstruct/comprehension-artifact.js +321 -0
- package/dist/core-runtime/reconstruct/comprehension-reduce.js +367 -0
- package/dist/core-runtime/reconstruct/comprehension-semantic-map.js +771 -0
- package/dist/core-runtime/reconstruct/contract-registry.js +20 -0
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +83 -9
- package/dist/core-runtime/reconstruct/final-output-sections.js +89 -0
- package/dist/core-runtime/reconstruct/governing-snapshot.js +36 -1
- package/dist/core-runtime/reconstruct/leaf-reader.js +305 -0
- package/dist/core-runtime/reconstruct/llm-dispatch-failure.js +270 -0
- package/dist/core-runtime/reconstruct/llm-touch-fingerprint.js +94 -0
- package/dist/core-runtime/reconstruct/material-admission-validation.js +18 -0
- package/dist/core-runtime/reconstruct/material-profile-validation.js +14 -0
- package/dist/core-runtime/reconstruct/materialize-preparation.js +304 -18
- package/dist/core-runtime/reconstruct/maturation-validation.js +1231 -53
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +107 -3
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +27 -0
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +87 -3
- package/dist/core-runtime/reconstruct/post-seed-validation.js +118 -5
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +117 -2
- package/dist/core-runtime/reconstruct/record.js +42 -0
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +16 -0
- package/dist/core-runtime/reconstruct/run-control-validation.js +1011 -34
- package/dist/core-runtime/reconstruct/run.js +5198 -536
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +54 -3
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +105 -2
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +41 -0
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +71 -183
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +37 -1
- package/dist/core-runtime/reconstruct/terminal-validation.js +132 -2
- package/dist/core-runtime/review/continuation-plan.js +12 -5
- package/dist/core-runtime/review/issue-artifact-runtime.js +4 -1
- package/dist/core-runtime/review/materializers.js +100 -11
- package/dist/core-runtime/review/nesting-batch.js +6 -2
- package/dist/core-runtime/review/obligation-shardability.js +138 -0
- package/dist/core-runtime/review/pipeline-execution-ledger.js +42 -2
- package/dist/core-runtime/review/review-artifact-utils.js +217 -5
- package/dist/core-runtime/review/review-execution-steps.js +32 -8
- package/dist/core-runtime/review/review-progress-contract.js +8 -0
- package/dist/core-runtime/review/review-prompt-budget.js +97 -0
- package/dist/core-runtime/review/semantic-quality-gate.js +33 -7
- package/dist/core-runtime/review/spreadsheet-review-disposition.js +94 -0
- package/dist/core-runtime/route-identity.js +182 -0
- package/dist/core-runtime/spreadsheet-header-escalation.js +204 -0
- package/dist/core-runtime/spreadsheet-structure-observer.js +2689 -0
- package/dist/core-runtime/target-material-kind.js +58 -0
- package/dist/mcp/server.js +12 -5
- package/dist/tui/app.js +167 -0
- package/dist/tui/data/event-follower.js +116 -0
- package/dist/tui/data/node-detail.js +42 -0
- package/dist/tui/data/projection-poll.js +26 -0
- package/dist/tui/data/session-discovery.js +65 -0
- package/dist/tui/index.js +108 -0
- package/dist/tui/screens/log.js +25 -0
- package/dist/tui/screens/node-detail.js +17 -0
- package/dist/tui/screens/session-selector.js +26 -0
- package/dist/tui/screens/workflow-tree.js +90 -0
- package/dist/tui/view-model/reconstruct-adapter.js +162 -0
- package/dist/tui/view-model/review-adapter.js +270 -0
- package/dist/tui/view-model/tree-view-model.js +19 -0
- package/package.json +18 -3
- package/settings.example.json +77 -16
|
@@ -5,6 +5,8 @@ import { parse as parseYaml } from "yaml";
|
|
|
5
5
|
import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
|
|
6
6
|
import { sourceSafetyRowIdForObservation } from "./source-safety-validation.js";
|
|
7
7
|
import { materialAdmissionIdForPurposeElement } from "./material-admission-validation.js";
|
|
8
|
+
import { isRevisionBlocker } from "./post-seed-validation.js";
|
|
9
|
+
import { assertObligation } from "./obligation-assertion.js";
|
|
8
10
|
const MATERIALITY_VALUES = [
|
|
9
11
|
"blocker",
|
|
10
12
|
"high",
|
|
@@ -255,12 +257,83 @@ function matrixRowNeedsFrontier(row) {
|
|
|
255
257
|
row.maturity_level !== "L4_validated_for_purpose" &&
|
|
256
258
|
row.limitation_refs.length === 0;
|
|
257
259
|
}
|
|
260
|
+
// Maturation value-read cut (design §13.3). Shared discharge→residual+readiness derivation
|
|
261
|
+
// used by BOTH the matrix builder and its validator (derive-and-assert). Pure/deterministic,
|
|
262
|
+
// no I/O.
|
|
263
|
+
// Baseline limitations cleared by VALIDATED, SATISFIED value-discharges, keyed by
|
|
264
|
+
// baseline_row_id. Only a discharge artifact whose validation is `valid` contributes (global
|
|
265
|
+
// gate, mirrors the answer-claims pattern), and only entries with satisfaction_status ===
|
|
266
|
+
// "satisfied" subtract — refuted/inconclusive never discharge (§4.3 four conditions). The
|
|
267
|
+
// validator builds this independently and never trusts the matrix's stamped residual.
|
|
268
|
+
function buildValidatedDischargeIndex(discharge, dischargeValidation) {
|
|
269
|
+
const byBaselineRowId = new Map();
|
|
270
|
+
if (dischargeValidation?.validation_status !== "valid")
|
|
271
|
+
return byBaselineRowId;
|
|
272
|
+
for (const entry of discharge?.discharges ?? []) {
|
|
273
|
+
if (entry.satisfaction_status !== "satisfied")
|
|
274
|
+
continue;
|
|
275
|
+
for (const baselineRowRef of entry.target_baseline_row_refs) {
|
|
276
|
+
const bucket = byBaselineRowId.get(baselineRowRef) ?? new Set();
|
|
277
|
+
for (const lim of entry.target_limitation_refs)
|
|
278
|
+
bucket.add(lim);
|
|
279
|
+
byBaselineRowId.set(baselineRowRef, bucket);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return byBaselineRowId;
|
|
283
|
+
}
|
|
284
|
+
// The terminal readiness for a material row whose value-dependent limitations were cleared to
|
|
285
|
+
// zero residual by value-read discharge. `dischargedForRow > 0` distinguishes a residual that
|
|
286
|
+
// reached zero BY discharge (→ value_resolved) from one that was always zero (→ the natural
|
|
287
|
+
// frontier_required / closed case). A forged value_resolved with no real discharge therefore
|
|
288
|
+
// falls through to frontier_required/closed and is rejected by the validator's readiness
|
|
289
|
+
// assert. matrixRowNeedsFrontier is unchanged (called with the residual).
|
|
290
|
+
function deriveMemberReadiness(args) {
|
|
291
|
+
if (args.residualLimitationRefs.length > 0)
|
|
292
|
+
return "limitation_backed";
|
|
293
|
+
if (isMaterialMaturationRow({ materiality: args.materiality }) &&
|
|
294
|
+
args.maturityLevel !== "L4_validated_for_purpose" &&
|
|
295
|
+
args.dischargedForRow > 0) {
|
|
296
|
+
return "value_resolved";
|
|
297
|
+
}
|
|
298
|
+
if (matrixRowNeedsFrontier({
|
|
299
|
+
materiality: args.materiality,
|
|
300
|
+
maturity_level: args.maturityLevel,
|
|
301
|
+
limitation_refs: args.residualLimitationRefs,
|
|
302
|
+
})) {
|
|
303
|
+
return "frontier_required";
|
|
304
|
+
}
|
|
305
|
+
return "closed";
|
|
306
|
+
}
|
|
258
307
|
function maturityLevelRank(level) {
|
|
259
308
|
return MATURITY_LEVELS.indexOf(level);
|
|
260
309
|
}
|
|
261
310
|
function higherMaturityLevel(current, next) {
|
|
262
311
|
return maturityLevelRank(next) > maturityLevelRank(current) ? next : current;
|
|
263
312
|
}
|
|
313
|
+
// Site-7 proportional terminal (design 20260706 §5): the SINGLE certification choke point.
|
|
314
|
+
// A judge-support-shortfall (degraded) claim must never certify — it is excluded from the
|
|
315
|
+
// positive sets that raise maturity (L3/L4), and every baseline row it matches carries a
|
|
316
|
+
// deterministic limitation token so member_readiness degrades to limitation_backed and the
|
|
317
|
+
// continuation decision weighs it by materiality. The matrix BUILDER and VALIDATOR both
|
|
318
|
+
// derive through these helpers (derive-and-assert lockstep); an expansion citing a degraded
|
|
319
|
+
// claim is likewise non-positive so it cannot co-lift a row to L4.
|
|
320
|
+
const JUDGE_SUPPORT_SHORTFALL_TOKEN_PREFIX = "judge_support_shortfall:";
|
|
321
|
+
function judgeSupportShortfallIds(validation) {
|
|
322
|
+
return new Set(validation?.judge_support_shortfall_claim_ids);
|
|
323
|
+
}
|
|
324
|
+
export function judgeSupportShortfallToken(claimId) {
|
|
325
|
+
return `${JUDGE_SUPPORT_SHORTFALL_TOKEN_PREFIX}${claimId}`;
|
|
326
|
+
}
|
|
327
|
+
function positiveAnswerClaim(claim, shortfallIds) {
|
|
328
|
+
return claim.answer_status === "answered" &&
|
|
329
|
+
claim.limitation_refs.length === 0 &&
|
|
330
|
+
!shortfallIds.has(claim.answer_claim_id);
|
|
331
|
+
}
|
|
332
|
+
function positiveExpansion(expansion, shortfallIds) {
|
|
333
|
+
return (expansion.operation === "add" || expansion.operation === "refine") &&
|
|
334
|
+
expansion.limitation_refs.length === 0 &&
|
|
335
|
+
!expansion.answer_claim_refs.some((ref) => shortfallIds.has(ref));
|
|
336
|
+
}
|
|
264
337
|
function answerClaimMatchesBaselineRow(claim, row) {
|
|
265
338
|
return claim.purpose_element_refs.includes(row.purpose_element_ref) &&
|
|
266
339
|
claim.target_surface_refs.includes(row.actionability_surface_ref) &&
|
|
@@ -306,6 +379,7 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
306
379
|
purpose_confirmation_validation_ref: args.purposeConfirmationValidationRef,
|
|
307
380
|
source_material_admission_ledger_ref: args.sourceMaterialAdmissionLedgerRef,
|
|
308
381
|
source_material_admission_validation_ref: args.sourceMaterialAdmissionValidationRef,
|
|
382
|
+
candidate_limitation_refs: [],
|
|
309
383
|
baseline_rows: [],
|
|
310
384
|
};
|
|
311
385
|
}
|
|
@@ -316,10 +390,12 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
316
390
|
const resolvedEvidence = rowEvidence.length > 0
|
|
317
391
|
? rowEvidence
|
|
318
392
|
: element.supporting_evidence_refs;
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
393
|
+
// Row-level limitations are the row's own seed-element limitations only.
|
|
394
|
+
// Candidate-level limitations are surfaced once at the baseline top-level
|
|
395
|
+
// (candidate_limitation_refs) so they constrain the actionable claim without
|
|
396
|
+
// forcing every surface×dimension row to limitation_backed (which would dead
|
|
397
|
+
// the maturation answer machine — frontier/closure/ledger/judge all skipped).
|
|
398
|
+
const limitationRefs = stringArray(seedElement?.limitation_refs);
|
|
323
399
|
const coverage = questionCoverage({
|
|
324
400
|
seedRefs,
|
|
325
401
|
competencyQuestions: args.competencyQuestions,
|
|
@@ -388,6 +464,7 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
388
464
|
purpose_confirmation_validation_ref: args.purposeConfirmationValidationRef,
|
|
389
465
|
source_material_admission_ledger_ref: args.sourceMaterialAdmissionLedgerRef,
|
|
390
466
|
source_material_admission_validation_ref: args.sourceMaterialAdmissionValidationRef,
|
|
467
|
+
candidate_limitation_refs: candidate.limitation_refs,
|
|
391
468
|
baseline_rows: rows.map((row) => ({
|
|
392
469
|
...row,
|
|
393
470
|
blocking_reason: needsFrontier(row)
|
|
@@ -396,8 +473,55 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
396
473
|
})),
|
|
397
474
|
};
|
|
398
475
|
}
|
|
476
|
+
// M1 conservation — derive-from-authority. The closed set of required maturation
|
|
477
|
+
// tuples is enumerated once from the selected purpose candidate's required_elements
|
|
478
|
+
// (the same element x surface x dimension enumeration the baseline builder performs),
|
|
479
|
+
// so the validator can prove every required tuple is present exactly once instead of
|
|
480
|
+
// only checking that the rows that ARE present resolve. Coverage authority is the
|
|
481
|
+
// TUPLE, not the (slug-derived) baseline_row_id, which is verified separately.
|
|
482
|
+
function baselineTupleKey(elementId, surfaceRef, dimensionRef) {
|
|
483
|
+
return JSON.stringify([elementId, surfaceRef, dimensionRef]);
|
|
484
|
+
}
|
|
485
|
+
function sameRefSet(a, b) {
|
|
486
|
+
if (a.length !== b.length)
|
|
487
|
+
return false;
|
|
488
|
+
const seen = new Set(a);
|
|
489
|
+
return b.every((ref) => seen.has(ref));
|
|
490
|
+
}
|
|
491
|
+
// Exact array equality (order- and multiplicity-sensitive). Used for matrix fields
|
|
492
|
+
// the builder copies VERBATIM from the baseline row, where set-equality would miss a
|
|
493
|
+
// reorder or a duplicate-occurrence swap (e.g. [a,a,b] -> [a,b,b]: same length+set).
|
|
494
|
+
function sameRefArray(a, b) {
|
|
495
|
+
return a.length === b.length && a.every((ref, index) => ref === b[index]);
|
|
496
|
+
}
|
|
497
|
+
// Cross-artifact lineage binding: a consumed validation must record the upstream ref that is
|
|
498
|
+
// actually supplied here, so a valid validation produced against a DIFFERENT upstream cannot
|
|
499
|
+
// bless this artifact. Returns a violation when the recorded and supplied refs disagree.
|
|
500
|
+
function validationLineageViolation(args) {
|
|
501
|
+
const recorded = args.recordedUpstreamRef ?? null;
|
|
502
|
+
const supplied = args.suppliedUpstreamRef ?? null;
|
|
503
|
+
if (recorded === supplied)
|
|
504
|
+
return null;
|
|
505
|
+
return violation({
|
|
506
|
+
code: "conflicting_state",
|
|
507
|
+
message: args.message,
|
|
508
|
+
subjectId: recorded ?? supplied,
|
|
509
|
+
});
|
|
510
|
+
}
|
|
511
|
+
function deriveExpectedBaselineTuples(selected) {
|
|
512
|
+
const tuples = new Set();
|
|
513
|
+
for (const element of selected?.adequacy_frame.required_elements ?? []) {
|
|
514
|
+
for (const surfaceRef of element.actionability_surface_refs) {
|
|
515
|
+
for (const dimensionRef of element.maturity_dimension_refs) {
|
|
516
|
+
tuples.add(baselineTupleKey(element.element_id, surfaceRef, dimensionRef));
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
return tuples;
|
|
521
|
+
}
|
|
399
522
|
export function validateMaturationBaseline(args) {
|
|
400
523
|
const violations = [];
|
|
524
|
+
const assertedObligationIds = [];
|
|
401
525
|
const baseline = args.maturationBaseline;
|
|
402
526
|
const selected = selectedPurposeCandidate({
|
|
403
527
|
sourcePurposeCandidates: args.sourcePurposeCandidates,
|
|
@@ -415,6 +539,25 @@ export function validateMaturationBaseline(args) {
|
|
|
415
539
|
subjectId: baseline.session_id,
|
|
416
540
|
}));
|
|
417
541
|
}
|
|
542
|
+
// candidate_limitation_refs are the source-level authority that holds an otherwise
|
|
543
|
+
// closed run at actionable_limited; a stale/edited baseline that drops them (then
|
|
544
|
+
// copied faithfully into the matrix) would pass the matrix check yet let
|
|
545
|
+
// continuation project actionable_ready. Anchor them to the selected candidate.
|
|
546
|
+
const expectedCandidateLimitations = new Set(selected?.limitation_refs ?? []);
|
|
547
|
+
const actualCandidateLimitations = new Set(baseline.candidate_limitation_refs);
|
|
548
|
+
const candidateLimitationsMatch = expectedCandidateLimitations.size === actualCandidateLimitations.size &&
|
|
549
|
+
[...expectedCandidateLimitations].every((ref) => actualCandidateLimitations.has(ref));
|
|
550
|
+
if (!candidateLimitationsMatch) {
|
|
551
|
+
violations.push(violation({
|
|
552
|
+
code: "conflicting_state",
|
|
553
|
+
message: "maturation baseline candidate_limitation_refs must match the selected purpose candidate's limitation_refs",
|
|
554
|
+
subjectId: "candidate_limitation_refs",
|
|
555
|
+
}));
|
|
556
|
+
}
|
|
557
|
+
// G(a) slice 2: record reaching the source-reconstruct-record enforcement UNCONDITIONALLY (this
|
|
558
|
+
// check has no data-dependent guard) so the obligation is proven wired on every call. Audited to a
|
|
559
|
+
// distinct enforcement site (source_reconstruct_record_missing) before recording — no laundering.
|
|
560
|
+
assertObligation(assertedObligationIds, "require_source_reconstruct_record_ref_and_sha256_before_maturation_baseline_consumption");
|
|
418
561
|
if (!baseline.source_reconstruct_record_ref || !args.sourceReconstructRecordSha256) {
|
|
419
562
|
violations.push(violation({
|
|
420
563
|
code: "source_reconstruct_record_missing",
|
|
@@ -444,6 +587,10 @@ export function validateMaturationBaseline(args) {
|
|
|
444
587
|
subjectId: "baseline_rows",
|
|
445
588
|
}));
|
|
446
589
|
}
|
|
590
|
+
// G(a) slice 2: record reaching the per-row mixed-lineage enforcement region UNCONDITIONALLY
|
|
591
|
+
// (before the row loop) so the obligation is proven wired even for a baseline with zero rows.
|
|
592
|
+
// Audited to a distinct enforcement site (mixed_lineage_missing, below) before recording.
|
|
593
|
+
assertObligation(assertedObligationIds, "validate_mixed_baseline_rows_preserve_member_material_and_cross_material_lineage");
|
|
447
594
|
for (const row of baseline.baseline_rows) {
|
|
448
595
|
if (seen.has(row.baseline_row_id)) {
|
|
449
596
|
violations.push(violation({
|
|
@@ -503,7 +650,19 @@ export function validateMaturationBaseline(args) {
|
|
|
503
650
|
row.member_target_material_kind !== null &&
|
|
504
651
|
row.member_source_refs.length > 0 &&
|
|
505
652
|
row.cross_material_ref_refs.length > 0;
|
|
506
|
-
|
|
653
|
+
// Mixed-target lineage is row-scoped by default: each row grounds itself by its
|
|
654
|
+
// own member lineage or its own limitation, because a stray candidate limitation
|
|
655
|
+
// does not name a row's member/source. The one exception is a candidate that is
|
|
656
|
+
// ITSELF limitation-backed — the upstream purpose validator already accepted its
|
|
657
|
+
// limitation_refs as justifying gaps (e.g. unrecoverable mixed-member lineage), so
|
|
658
|
+
// its rows may lean on that acknowledgment rather than failing baseline validation.
|
|
659
|
+
// (Candidate limitations still never mark a row limitation_backed, so frontier
|
|
660
|
+
// gating is unaffected.)
|
|
661
|
+
const candidateLimitationBacked = selected?.purpose_source_status === "limitation_backed" ||
|
|
662
|
+
selected?.adequacy_frame.frame_status === "limitation_backed";
|
|
663
|
+
const mixedLineageExempt = row.limitation_refs.length > 0 ||
|
|
664
|
+
(candidateLimitationBacked && baseline.candidate_limitation_refs.length > 0);
|
|
665
|
+
if (mixedTarget && !hasLineage && !mixedLineageExempt) {
|
|
507
666
|
violations.push(violation({
|
|
508
667
|
code: "mixed_lineage_missing",
|
|
509
668
|
message: "mixed-material baseline row must preserve member lineage or cite a limitation",
|
|
@@ -511,6 +670,39 @@ export function validateMaturationBaseline(args) {
|
|
|
511
670
|
}));
|
|
512
671
|
}
|
|
513
672
|
}
|
|
673
|
+
// M1 coverage conservation: derive the closed required-tuple set from the selected
|
|
674
|
+
// candidate and prove every required tuple is present exactly once. The per-row loop
|
|
675
|
+
// above only checks that PRESENT rows resolve, so a deleted required row (erasing
|
|
676
|
+
// blocker/high scope) or a duplicated tuple would otherwise pass silently.
|
|
677
|
+
// G(a): record reaching the coverage block UNCONDITIONALLY (before the `if (selected)`
|
|
678
|
+
// guard) so the obligation is proven wired even for inputs with no selected candidate.
|
|
679
|
+
assertObligation(assertedObligationIds, "validate_baseline_rows_cover_selected_purpose_frame_required_elements");
|
|
680
|
+
if (selected) {
|
|
681
|
+
const expectedTuples = deriveExpectedBaselineTuples(selected);
|
|
682
|
+
const presentTupleCounts = new Map();
|
|
683
|
+
for (const row of baseline.baseline_rows) {
|
|
684
|
+
const key = baselineTupleKey(row.purpose_element_ref, row.actionability_surface_ref, row.maturity_dimension_ref);
|
|
685
|
+
presentTupleCounts.set(key, (presentTupleCounts.get(key) ?? 0) + 1);
|
|
686
|
+
}
|
|
687
|
+
for (const tuple of expectedTuples) {
|
|
688
|
+
if (!presentTupleCounts.has(tuple)) {
|
|
689
|
+
violations.push(violation({
|
|
690
|
+
code: "missing_required_coverage",
|
|
691
|
+
message: `required maturation tuple has no baseline row: ${tuple}`,
|
|
692
|
+
subjectId: tuple,
|
|
693
|
+
}));
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
for (const [tuple, count] of presentTupleCounts) {
|
|
697
|
+
if (count > 1 && expectedTuples.has(tuple)) {
|
|
698
|
+
violations.push(violation({
|
|
699
|
+
code: "conflicting_state",
|
|
700
|
+
message: `multiple baseline rows cover one required maturation tuple: ${tuple}`,
|
|
701
|
+
subjectId: tuple,
|
|
702
|
+
}));
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
}
|
|
514
706
|
return {
|
|
515
707
|
schema_version: "1",
|
|
516
708
|
session_id: baseline.session_id,
|
|
@@ -527,6 +719,7 @@ export function validateMaturationBaseline(args) {
|
|
|
527
719
|
validation_results: violations.length === 0
|
|
528
720
|
? ["maturation_baseline_valid"]
|
|
529
721
|
: ["maturation_baseline_invalid"],
|
|
722
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
530
723
|
violations,
|
|
531
724
|
};
|
|
532
725
|
}
|
|
@@ -534,29 +727,60 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
534
727
|
const answerClaims = args.maturationAnswerClaimsValidation?.validation_status === "valid"
|
|
535
728
|
? args.maturationAnswerClaims?.answer_claims ?? []
|
|
536
729
|
: [];
|
|
730
|
+
const shortfallIds = judgeSupportShortfallIds(args.maturationAnswerClaimsValidation);
|
|
537
731
|
const expansions = args.ontologyExpansionValidation?.validation_status === "valid"
|
|
538
732
|
? args.ontologyExpansion?.expansions ?? []
|
|
539
733
|
: [];
|
|
734
|
+
// Maturation value-read cut: baseline limitations cleared by validated satisfied
|
|
735
|
+
// discharges, keyed by baseline_row_id (design §13.3). Empty when no discharge is threaded
|
|
736
|
+
// or its validation is not valid → the subtract below is a no-op (default-off).
|
|
737
|
+
const dischargedLimitationsByBaselineRow = buildValidatedDischargeIndex(args.maturationValueDischarge, args.maturationValueDischargeValidation);
|
|
738
|
+
// Reverse link: a row's blocking_question_refs name the maturation frontier
|
|
739
|
+
// question(s) authored for it. Questions only exist once the frontier is authored
|
|
740
|
+
// from the (pre-frontier) baseline matrix, so this is populated on the current-matrix
|
|
741
|
+
// recompute when a VALIDATED frontier is threaded in and left empty otherwise (the
|
|
742
|
+
// baseline matrix passes no frontier → []). Indexed by baseline_row_id, which the
|
|
743
|
+
// frontier question references via baseline_row_refs.
|
|
744
|
+
const frontierQuestions = args.maturationQuestionFrontierValidation?.validation_status === "valid"
|
|
745
|
+
? args.maturationQuestionFrontier?.questions ?? []
|
|
746
|
+
: [];
|
|
747
|
+
const blockingQuestionsByBaselineRow = new Map();
|
|
748
|
+
for (const question of frontierQuestions) {
|
|
749
|
+
for (const baselineRowRef of question.baseline_row_refs) {
|
|
750
|
+
const bucket = blockingQuestionsByBaselineRow.get(baselineRowRef);
|
|
751
|
+
if (bucket)
|
|
752
|
+
bucket.push(question.question_id);
|
|
753
|
+
else {
|
|
754
|
+
blockingQuestionsByBaselineRow.set(baselineRowRef, [question.question_id]);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
540
758
|
return {
|
|
541
759
|
schema_version: "1",
|
|
542
760
|
session_id: args.sessionId,
|
|
543
761
|
created_at: isoNow(),
|
|
544
762
|
maturation_baseline_ref: args.maturationBaselineRef,
|
|
545
763
|
maturation_baseline_validation_ref: args.maturationBaselineValidationRef,
|
|
764
|
+
candidate_limitation_refs: args.maturationBaseline.candidate_limitation_refs,
|
|
546
765
|
rows: args.maturationBaseline.baseline_rows.map((row) => {
|
|
547
766
|
const matchingAnswerClaims = answerClaims.filter((claim) => answerClaimMatchesBaselineRow(claim, row));
|
|
548
767
|
const matchingExpansions = expansions.filter((expansion) => expansionMatchesBaselineRow(expansion, row));
|
|
549
|
-
const positiveAnswerClaims = matchingAnswerClaims.filter((claim) => claim
|
|
550
|
-
|
|
551
|
-
const positiveExpansions = matchingExpansions.filter((expansion) => (expansion.operation === "add" || expansion.operation === "refine") &&
|
|
552
|
-
expansion.limitation_refs.length === 0);
|
|
768
|
+
const positiveAnswerClaims = matchingAnswerClaims.filter((claim) => positiveAnswerClaim(claim, shortfallIds));
|
|
769
|
+
const positiveExpansions = matchingExpansions.filter((expansion) => positiveExpansion(expansion, shortfallIds));
|
|
553
770
|
let maturityLevel = row.maturity_level;
|
|
554
771
|
const supportingRefs = [
|
|
555
772
|
...row.supporting_seed_refs,
|
|
556
773
|
...row.supporting_validation_refs,
|
|
557
774
|
...row.supporting_evidence_refs.map((ref) => ref.observation_id),
|
|
558
775
|
];
|
|
559
|
-
|
|
776
|
+
// Value-read discharge subtracts ONLY this baseline row's value-dependent limitations
|
|
777
|
+
// that a validated satisfied discharge targeted; claim/expansion limitations below are
|
|
778
|
+
// never discharged (they are separate evidence caveats). dischargedForRow counts the
|
|
779
|
+
// baseline limitations actually cleared — the value_resolved gate (design §13.3).
|
|
780
|
+
const dischargedLimsForRow = dischargedLimitationsByBaselineRow.get(row.baseline_row_id) ??
|
|
781
|
+
new Set();
|
|
782
|
+
const dischargedForRow = row.limitation_refs.filter((ref) => dischargedLimsForRow.has(ref)).length;
|
|
783
|
+
const limitationRefs = row.limitation_refs.filter((ref) => !dischargedLimsForRow.has(ref));
|
|
560
784
|
if (matchingAnswerClaims.length > 0 && args.maturationAnswerClaimsValidationRef) {
|
|
561
785
|
supportingRefs.push(args.maturationAnswerClaimsValidationRef);
|
|
562
786
|
}
|
|
@@ -569,6 +793,12 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
569
793
|
supportingRefs.push(ref.observation_id);
|
|
570
794
|
}
|
|
571
795
|
limitationRefs.push(...claim.limitation_refs);
|
|
796
|
+
// Site-7 degrade token: a matching judge-support-shortfall claim leaves a named
|
|
797
|
+
// residual limitation, so the row reads limitation_backed (not closed) and the
|
|
798
|
+
// shortfall reaches decision.limitation_refs — the honest-disclosure channel.
|
|
799
|
+
if (shortfallIds.has(claim.answer_claim_id)) {
|
|
800
|
+
limitationRefs.push(judgeSupportShortfallToken(claim.answer_claim_id));
|
|
801
|
+
}
|
|
572
802
|
}
|
|
573
803
|
for (const expansion of matchingExpansions) {
|
|
574
804
|
supportingRefs.push(expansion.expansion_id, ...expansion.answer_claim_refs);
|
|
@@ -583,16 +813,17 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
583
813
|
if (positiveAnswerClaims.length > 0 && positiveExpansions.length > 0) {
|
|
584
814
|
maturityLevel = higherMaturityLevel(maturityLevel, "L4_validated_for_purpose");
|
|
585
815
|
}
|
|
586
|
-
|
|
816
|
+
// Shared derivation (builder ↔ validator). A value_resolved row has residual 0 and
|
|
817
|
+
// dischargedForRow > 0, so matrixRowNeedsFrontier would return true for it — therefore
|
|
818
|
+
// blocking_question_refs / next_action below gate on memberReadiness, NOT on a raw
|
|
819
|
+
// frontierRequired boolean, or the validator's reverse-link check would reject it
|
|
820
|
+
// (design §13.1 latent-defect fix).
|
|
821
|
+
const memberReadiness = deriveMemberReadiness({
|
|
587
822
|
materiality: row.materiality,
|
|
588
|
-
|
|
589
|
-
|
|
823
|
+
maturityLevel,
|
|
824
|
+
residualLimitationRefs: limitationRefs,
|
|
825
|
+
dischargedForRow,
|
|
590
826
|
});
|
|
591
|
-
const memberReadiness = limitationRefs.length > 0
|
|
592
|
-
? "limitation_backed"
|
|
593
|
-
: frontierRequired
|
|
594
|
-
? "frontier_required"
|
|
595
|
-
: "closed";
|
|
596
827
|
return {
|
|
597
828
|
matrix_row_id: `matrix-${slug(row.baseline_row_id)}`,
|
|
598
829
|
baseline_row_refs: [row.baseline_row_id],
|
|
@@ -610,13 +841,19 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
610
841
|
competency_assessment_refs: row.competency_assessment_refs,
|
|
611
842
|
maturity_level: maturityLevel,
|
|
612
843
|
supporting_refs: [...new Set(supportingRefs)],
|
|
613
|
-
|
|
844
|
+
// Only an open (frontier_required) row is blocked; a closed, value_resolved, or
|
|
845
|
+
// limitation-backed row carries no open blocking questions.
|
|
846
|
+
blocking_question_refs: memberReadiness === "frontier_required"
|
|
847
|
+
? [...new Set(blockingQuestionsByBaselineRow.get(row.baseline_row_id) ?? [])]
|
|
848
|
+
: [],
|
|
614
849
|
limitation_refs: [...new Set(limitationRefs)],
|
|
615
|
-
next_action:
|
|
850
|
+
next_action: memberReadiness === "frontier_required"
|
|
616
851
|
? "Create a maturation frontier question for this row."
|
|
617
852
|
: memberReadiness === "limitation_backed"
|
|
618
853
|
? "Keep the limitation visible in continuation decisions."
|
|
619
|
-
:
|
|
854
|
+
: memberReadiness === "value_resolved"
|
|
855
|
+
? "Preserve the value-read discharge as current actionability support (value-grounded, not L4-validated)."
|
|
856
|
+
: "Preserve the closed row as current actionability support.",
|
|
620
857
|
};
|
|
621
858
|
}),
|
|
622
859
|
};
|
|
@@ -624,13 +861,43 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
624
861
|
export function validateActionabilityMatrix(args) {
|
|
625
862
|
const matrix = args.actionabilityMatrix;
|
|
626
863
|
const violations = [];
|
|
864
|
+
const assertedObligationIds = [];
|
|
627
865
|
const baselineRows = new Map(args.maturationBaseline.baseline_rows.map((row) => [row.baseline_row_id, row]));
|
|
628
866
|
const answerClaims = args.maturationAnswerClaimsValidation?.validation_status === "valid"
|
|
629
867
|
? args.maturationAnswerClaims?.answer_claims ?? []
|
|
630
868
|
: [];
|
|
869
|
+
const shortfallIds = judgeSupportShortfallIds(args.maturationAnswerClaimsValidation);
|
|
631
870
|
const expansions = args.ontologyExpansionValidation?.validation_status === "valid"
|
|
632
871
|
? args.ontologyExpansion?.expansions ?? []
|
|
633
872
|
: [];
|
|
873
|
+
// Maturation value-read cut: rebuild the validated discharge index from the consumed
|
|
874
|
+
// discharge artifact INDEPENDENTLY (derive-and-assert). The per-row readiness recompute and
|
|
875
|
+
// the dropped-baseline-limitation check below use this, never the matrix's stamped residual.
|
|
876
|
+
const dischargedLimitationsByBaselineRow = buildValidatedDischargeIndex(args.maturationValueDischarge, args.maturationValueDischargeValidation);
|
|
877
|
+
// Reverse-link conservation for blocking_question_refs: when a validated question
|
|
878
|
+
// frontier is threaded in (the current-matrix recompute), index each question by the
|
|
879
|
+
// baseline rows it names so the matrix's reverse link can be proven to mirror the
|
|
880
|
+
// frontier's forward link. Absent/invalid frontier (the baseline matrix) → the rows
|
|
881
|
+
// must carry no blocking questions yet.
|
|
882
|
+
const frontierAvailable = args.maturationQuestionFrontierValidation?.validation_status === "valid";
|
|
883
|
+
// qid -> the baseline rows it names (forward link), and the inverse baseline-row -> qids
|
|
884
|
+
// so the matrix's reverse link can be proven to be the EXACT set (not merely a subset)
|
|
885
|
+
// of the frontier questions naming each row.
|
|
886
|
+
const frontierQuestionBaselineRows = new Map();
|
|
887
|
+
const frontierQuestionsByBaselineRow = new Map();
|
|
888
|
+
if (frontierAvailable) {
|
|
889
|
+
for (const question of args.maturationQuestionFrontier?.questions ?? []) {
|
|
890
|
+
frontierQuestionBaselineRows.set(question.question_id, new Set(question.baseline_row_refs));
|
|
891
|
+
for (const baselineRowRef of question.baseline_row_refs) {
|
|
892
|
+
const bucket = frontierQuestionsByBaselineRow.get(baselineRowRef);
|
|
893
|
+
if (bucket)
|
|
894
|
+
bucket.add(question.question_id);
|
|
895
|
+
else {
|
|
896
|
+
frontierQuestionsByBaselineRow.set(baselineRowRef, new Set([question.question_id]));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
634
901
|
const seen = new Set();
|
|
635
902
|
if (matrix.session_id !== args.maturationBaseline.session_id) {
|
|
636
903
|
violations.push(violation({
|
|
@@ -639,6 +906,21 @@ export function validateActionabilityMatrix(args) {
|
|
|
639
906
|
subjectId: matrix.session_id,
|
|
640
907
|
}));
|
|
641
908
|
}
|
|
909
|
+
// Continuation trusts matrix.candidate_limitation_refs to keep an otherwise closed
|
|
910
|
+
// run at actionable_limited, so the matrix must faithfully carry the validated
|
|
911
|
+
// baseline's candidate limitations — a stale/edited matrix that drops them would
|
|
912
|
+
// otherwise let continuation project actionable_ready despite the source limitation.
|
|
913
|
+
const baselineCandidateLimitations = new Set(args.maturationBaseline.candidate_limitation_refs);
|
|
914
|
+
const matrixCandidateLimitations = new Set(matrix.candidate_limitation_refs);
|
|
915
|
+
const candidateLimitationsMatch = baselineCandidateLimitations.size === matrixCandidateLimitations.size &&
|
|
916
|
+
[...baselineCandidateLimitations].every((ref) => matrixCandidateLimitations.has(ref));
|
|
917
|
+
if (!candidateLimitationsMatch) {
|
|
918
|
+
violations.push(violation({
|
|
919
|
+
code: "conflicting_state",
|
|
920
|
+
message: "actionability matrix candidate_limitation_refs must match the validated maturation baseline",
|
|
921
|
+
subjectId: "candidate_limitation_refs",
|
|
922
|
+
}));
|
|
923
|
+
}
|
|
642
924
|
if (args.maturationBaselineValidation.validation_status !== "valid") {
|
|
643
925
|
violations.push(violation({
|
|
644
926
|
code: "prior_validation_invalid",
|
|
@@ -646,6 +928,132 @@ export function validateActionabilityMatrix(args) {
|
|
|
646
928
|
subjectId: args.maturationBaselineValidationRef ?? null,
|
|
647
929
|
}));
|
|
648
930
|
}
|
|
931
|
+
// The question frontier is a PAIR of declared inputs (the artifact and its validation);
|
|
932
|
+
// they must be supplied together. A half-threaded call (one side only) would silently
|
|
933
|
+
// fall back to the pre-frontier rules and accept an unlinked current matrix, so fail loud.
|
|
934
|
+
const frontierProvided = args.maturationQuestionFrontier != null;
|
|
935
|
+
const frontierValidationProvided = args.maturationQuestionFrontierValidation != null;
|
|
936
|
+
if (frontierProvided !== frontierValidationProvided) {
|
|
937
|
+
violations.push(violation({
|
|
938
|
+
code: "missing_required_ref",
|
|
939
|
+
message: "actionability matrix question frontier and its validation must be supplied together",
|
|
940
|
+
subjectId: args.maturationQuestionFrontierValidationRef ??
|
|
941
|
+
args.actionabilityMatrixRef ?? null,
|
|
942
|
+
}));
|
|
943
|
+
}
|
|
944
|
+
// Current-matrix mode is signalled by post-frontier authority inputs (answer claims and
|
|
945
|
+
// ontology expansion are authored AFTER the frontier). Whenever any are present, the
|
|
946
|
+
// frontier pair is required — otherwise a current matrix with frontier_required rows and
|
|
947
|
+
// empty blocking_question_refs would validate under the pre-frontier (baseline) rules.
|
|
948
|
+
// Only the true baseline matrix (no post-frontier inputs AND no frontier) may omit it.
|
|
949
|
+
const postFrontierInputsPresent = args.maturationAnswerClaims != null ||
|
|
950
|
+
args.maturationAnswerClaimsValidation != null ||
|
|
951
|
+
args.ontologyExpansion != null ||
|
|
952
|
+
args.ontologyExpansionValidation != null;
|
|
953
|
+
if (postFrontierInputsPresent &&
|
|
954
|
+
!(frontierProvided && frontierValidationProvided)) {
|
|
955
|
+
violations.push(violation({
|
|
956
|
+
code: "missing_required_ref",
|
|
957
|
+
message: "current-matrix actionability validation (carrying answer-claim or expansion inputs) requires the question frontier and its validation",
|
|
958
|
+
subjectId: args.maturationQuestionFrontierValidationRef ??
|
|
959
|
+
args.actionabilityMatrixRef ?? null,
|
|
960
|
+
}));
|
|
961
|
+
}
|
|
962
|
+
// A supplied question-frontier validation is a declared input authority: distinguish
|
|
963
|
+
// "no frontier supplied" (the pre-frontier baseline matrix, legitimately empty) from
|
|
964
|
+
// "supplied but invalid". The latter must fail rather than silently fall back to the
|
|
965
|
+
// pre-frontier rules (which would let an empty blocking_question_refs pass even though
|
|
966
|
+
// the required frontier authority failed validation).
|
|
967
|
+
if (args.maturationQuestionFrontierValidation &&
|
|
968
|
+
args.maturationQuestionFrontierValidation.validation_status !== "valid") {
|
|
969
|
+
violations.push(violation({
|
|
970
|
+
code: "prior_validation_invalid",
|
|
971
|
+
message: "actionability matrix requires valid maturation question frontier validation when one is supplied",
|
|
972
|
+
subjectId: args.maturationQuestionFrontierValidationRef ?? null,
|
|
973
|
+
}));
|
|
974
|
+
}
|
|
975
|
+
// Bind the frontier validation to the frontier artifact it is consumed with: a valid
|
|
976
|
+
// validation of a DIFFERENT frontier must not bless the supplied (possibly stale/edited)
|
|
977
|
+
// frontier, whose questions would otherwise satisfy blocking_question_refs unvalidated.
|
|
978
|
+
if (frontierProvided && frontierAvailable) {
|
|
979
|
+
const recordedFrontierRef = args.maturationQuestionFrontierValidation?.maturation_question_frontier_ref ?? null;
|
|
980
|
+
if (recordedFrontierRef !== (args.maturationQuestionFrontierRef ?? null)) {
|
|
981
|
+
violations.push(violation({
|
|
982
|
+
code: "conflicting_state",
|
|
983
|
+
message: "actionability matrix question frontier validation must validate the supplied question frontier",
|
|
984
|
+
subjectId: args.maturationQuestionFrontierValidationRef ??
|
|
985
|
+
args.maturationQuestionFrontierRef ?? null,
|
|
986
|
+
}));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
// Cross-artifact lineage chain: the consumed validations must form a consistent lineage —
|
|
990
|
+
// each must have been produced against the upstream validation that is actually supplied
|
|
991
|
+
// here (baseline-validation <- frontier-validation <- answer-claims-validation <-
|
|
992
|
+
// ontology-expansion-validation). A valid validation produced against a different upstream
|
|
993
|
+
// cannot bless this matrix. (Each check is gated on the consumed validation being present &
|
|
994
|
+
// valid, so the pre-frontier baseline matrix trips none of them. Binding each validation to
|
|
995
|
+
// its OWN source artifact — beyond the frontier above — is a smaller residual; see the
|
|
996
|
+
// follow-up note.)
|
|
997
|
+
for (const lineage of [
|
|
998
|
+
frontierAvailable
|
|
999
|
+
? validationLineageViolation({
|
|
1000
|
+
recordedUpstreamRef: args.maturationQuestionFrontierValidation?.maturation_baseline_validation_ref,
|
|
1001
|
+
suppliedUpstreamRef: args.maturationBaselineValidationRef,
|
|
1002
|
+
message: "actionability matrix question frontier validation must be produced against the supplied maturation baseline validation",
|
|
1003
|
+
})
|
|
1004
|
+
: null,
|
|
1005
|
+
args.maturationAnswerClaimsValidation?.validation_status === "valid"
|
|
1006
|
+
? validationLineageViolation({
|
|
1007
|
+
recordedUpstreamRef: args.maturationAnswerClaimsValidation.maturation_question_frontier_validation_ref,
|
|
1008
|
+
suppliedUpstreamRef: args.maturationQuestionFrontierValidationRef,
|
|
1009
|
+
message: "actionability matrix answer-claims validation must be produced against the supplied question frontier validation",
|
|
1010
|
+
})
|
|
1011
|
+
: null,
|
|
1012
|
+
args.ontologyExpansionValidation?.validation_status === "valid"
|
|
1013
|
+
? validationLineageViolation({
|
|
1014
|
+
recordedUpstreamRef: args.ontologyExpansionValidation.maturation_answer_claims_validation_ref,
|
|
1015
|
+
suppliedUpstreamRef: args.maturationAnswerClaimsValidationRef,
|
|
1016
|
+
message: "actionability matrix ontology expansion validation must be produced against the supplied answer-claims validation",
|
|
1017
|
+
})
|
|
1018
|
+
: null,
|
|
1019
|
+
]) {
|
|
1020
|
+
if (lineage)
|
|
1021
|
+
violations.push(lineage);
|
|
1022
|
+
}
|
|
1023
|
+
// G(a): record reaching the matrix row-id/baseline-ref-close block UNCONDITIONALLY (before
|
|
1024
|
+
// the per-row loop) so the obligation is proven wired even for an empty matrix. One fn serves
|
|
1025
|
+
// two validators; the recorded id attributes to whichever validator this mode is (below).
|
|
1026
|
+
assertObligation(assertedObligationIds, "validate_matrix_row_ids_are_stable_and_baseline_row_refs_close");
|
|
1027
|
+
// G(a) slice 3: mode-ALIGNED recording. This fn serves two validators by mode
|
|
1028
|
+
// (postFrontierInputsPresent → validator_id) and a shared enforcement region satisfies a
|
|
1029
|
+
// DIFFERENTLY-NAMED obligation per mode, so each stamp's obligation_id is mode-conditional and is
|
|
1030
|
+
// emitted ONLY when the mode (and, for the frontier-gated pair, the frontier branch) matches the
|
|
1031
|
+
// registry's validator attribution — never minting a (validator_id, obligation_id) pair the
|
|
1032
|
+
// registry lacks. Each recorded pair was audited to a distinct enforcement region (no laundering);
|
|
1033
|
+
// obligations with no distinct enforcement stay parked with ledger audit notes (the current-mode
|
|
1034
|
+
// expansion-alt is absent, the blocker/high-L4 rule is missing, "support" rules push no violation,
|
|
1035
|
+
// the maturity-level rule is distributed across other regions, the preserve-seed rule is defensive).
|
|
1036
|
+
if (postFrontierInputsPresent) {
|
|
1037
|
+
// CURRENT mode → actionability-matrix-validator.
|
|
1038
|
+
// identity preservation + maturity-upgrade citation region (conflicting_state identity /
|
|
1039
|
+
// missing_required_ref upgrade, below).
|
|
1040
|
+
assertObligation(assertedObligationIds, "validate_matrix_rows_derive_from_validated_baseline_and_any_applicable_validated_deltas");
|
|
1041
|
+
if (frontierAvailable) {
|
|
1042
|
+
// the frontier reverse-link validation (the frontierAvailable `else` branch below).
|
|
1043
|
+
assertObligation(assertedObligationIds, "validate_blocking_question_refs_against_validated_question_frontier");
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
else {
|
|
1047
|
+
// BASELINE mode → baseline-actionability-matrix-validator.
|
|
1048
|
+
// strict baseline-row-ref resolution (unknown_id / conflicting_state on baseline_row_refs).
|
|
1049
|
+
assertObligation(assertedObligationIds, "reject_matrix_rows_without_baseline_row_ref");
|
|
1050
|
+
// baseline payload conservation (conflicting_state on identity / maturity-no-reduce).
|
|
1051
|
+
assertObligation(assertedObligationIds, "validate_matrix_rows_derive_from_validated_baseline_without_maturation_deltas");
|
|
1052
|
+
if (!frontierAvailable) {
|
|
1053
|
+
// reject blocking refs before the frontier exists (the `!frontierAvailable` branch below).
|
|
1054
|
+
assertObligation(assertedObligationIds, "reject_blocking_question_refs_before_question_frontier_authoring");
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
649
1057
|
for (const row of matrix.rows) {
|
|
650
1058
|
if (seen.has(row.matrix_row_id)) {
|
|
651
1059
|
violations.push(violation({
|
|
@@ -655,6 +1063,13 @@ export function validateActionabilityMatrix(args) {
|
|
|
655
1063
|
}));
|
|
656
1064
|
}
|
|
657
1065
|
seen.add(row.matrix_row_id);
|
|
1066
|
+
if (row.baseline_row_refs.length !== 1) {
|
|
1067
|
+
violations.push(violation({
|
|
1068
|
+
code: "conflicting_state",
|
|
1069
|
+
message: "matrix row must reference exactly one baseline row",
|
|
1070
|
+
subjectId: row.matrix_row_id,
|
|
1071
|
+
}));
|
|
1072
|
+
}
|
|
658
1073
|
const baselineRef = row.baseline_row_refs[0] ?? null;
|
|
659
1074
|
const baselineRow = baselineRef ? baselineRows.get(baselineRef) : null;
|
|
660
1075
|
if (!baselineRow) {
|
|
@@ -672,6 +1087,31 @@ export function validateActionabilityMatrix(args) {
|
|
|
672
1087
|
subjectId: row.matrix_row_id,
|
|
673
1088
|
}));
|
|
674
1089
|
}
|
|
1090
|
+
// M1 payload conservation: a matrix row can cite baseline A while mutating the
|
|
1091
|
+
// identity/lineage fields it must inherit. Assert the baseline-immutable set is
|
|
1092
|
+
// preserved (materiality is checked above; maturity/support/limitation/readiness
|
|
1093
|
+
// legitimately change by validated rules, so are NOT asserted). The builder copies
|
|
1094
|
+
// member-lineage AND competency refs VERBATIM from the baseline row, so they are
|
|
1095
|
+
// compared with exact array equality (multiplicity- and order-sensitive): a
|
|
1096
|
+
// set-only check would let a duplicate-occurrence swap or a competency-ref tamper
|
|
1097
|
+
// pass undetected.
|
|
1098
|
+
const identityPreserved = row.purpose_element_ref === baselineRow.purpose_element_ref &&
|
|
1099
|
+
row.actionability_surface_ref === baselineRow.actionability_surface_ref &&
|
|
1100
|
+
row.maturity_dimension_ref === baselineRow.maturity_dimension_ref &&
|
|
1101
|
+
row.materiality_ref === baselineRow.materiality_ref &&
|
|
1102
|
+
row.member_target_material_kind === baselineRow.member_target_material_kind &&
|
|
1103
|
+
sameRefArray(row.member_scope_refs, baselineRow.member_scope_refs) &&
|
|
1104
|
+
sameRefArray(row.member_source_refs, baselineRow.member_source_refs) &&
|
|
1105
|
+
sameRefArray(row.cross_material_ref_refs, baselineRow.cross_material_ref_refs) &&
|
|
1106
|
+
sameRefArray(row.competency_question_refs, baselineRow.competency_question_refs) &&
|
|
1107
|
+
sameRefArray(row.competency_assessment_refs, baselineRow.competency_assessment_refs);
|
|
1108
|
+
if (!identityPreserved) {
|
|
1109
|
+
violations.push(violation({
|
|
1110
|
+
code: "conflicting_state",
|
|
1111
|
+
message: "matrix row must preserve the baseline row's identity and member-lineage fields",
|
|
1112
|
+
subjectId: row.matrix_row_id,
|
|
1113
|
+
}));
|
|
1114
|
+
}
|
|
675
1115
|
if (maturityLevelRank(row.maturity_level) <
|
|
676
1116
|
maturityLevelRank(baselineRow.maturity_level)) {
|
|
677
1117
|
violations.push(violation({
|
|
@@ -681,11 +1121,9 @@ export function validateActionabilityMatrix(args) {
|
|
|
681
1121
|
}));
|
|
682
1122
|
}
|
|
683
1123
|
const matchingAnswerClaims = answerClaims.filter((claim) => answerClaimMatchesBaselineRow(claim, baselineRow) &&
|
|
684
|
-
claim
|
|
685
|
-
claim.limitation_refs.length === 0);
|
|
1124
|
+
positiveAnswerClaim(claim, shortfallIds));
|
|
686
1125
|
const matchingExpansions = expansions.filter((expansion) => expansionMatchesBaselineRow(expansion, baselineRow) &&
|
|
687
|
-
(expansion
|
|
688
|
-
expansion.limitation_refs.length === 0);
|
|
1126
|
+
positiveExpansion(expansion, shortfallIds));
|
|
689
1127
|
if (row.maturity_level !== baselineRow.maturity_level) {
|
|
690
1128
|
const claimsCanRaiseToL3 = matchingAnswerClaims.length > 0;
|
|
691
1129
|
const claimsCanRaiseToL4 = matchingAnswerClaims.length > 0 && matchingExpansions.length > 0;
|
|
@@ -725,18 +1163,157 @@ export function validateActionabilityMatrix(args) {
|
|
|
725
1163
|
}));
|
|
726
1164
|
}
|
|
727
1165
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1166
|
+
// Derive-and-assert (design §13.3 F2): recompute the discharge effect from the
|
|
1167
|
+
// INDEPENDENTLY rebuilt validated discharge index — never trust the matrix's stamped
|
|
1168
|
+
// residual. (a) every baseline limitation the matrix dropped from this row must have
|
|
1169
|
+
// been cleared by a validated satisfied discharge; (b) member_readiness must equal the
|
|
1170
|
+
// shared derivation using the recomputed dischargedForRow, so a forged value_resolved
|
|
1171
|
+
// with no real discharge is rejected (it falls through to frontier_required/closed). The
|
|
1172
|
+
// residual LENGTH still reads the stamped row.limitation_refs (claim/expansion caveats are
|
|
1173
|
+
// trusted as before), but a forged shrink is caught by (a).
|
|
1174
|
+
const dischargedLimsForRow = dischargedLimitationsByBaselineRow.get(baselineRow.baseline_row_id) ??
|
|
1175
|
+
new Set();
|
|
1176
|
+
const stampedLimitationRefs = new Set(row.limitation_refs);
|
|
1177
|
+
for (const baselineLim of baselineRow.limitation_refs) {
|
|
1178
|
+
if (!stampedLimitationRefs.has(baselineLim) &&
|
|
1179
|
+
!dischargedLimsForRow.has(baselineLim)) {
|
|
1180
|
+
violations.push(violation({
|
|
1181
|
+
code: "conflicting_state",
|
|
1182
|
+
message: "matrix row dropped a baseline limitation without a validated satisfied value-discharge",
|
|
1183
|
+
subjectId: row.matrix_row_id,
|
|
1184
|
+
}));
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
// Site-7 token conservation (design 20260706 §5): stamped claim/expansion caveats are
|
|
1188
|
+
// otherwise trusted, so a stale/edited matrix could silently DROP the judge-support-
|
|
1189
|
+
// shortfall token and un-exclude the row. Re-derive the expected tokens from the claims
|
|
1190
|
+
// + validation authority and require each on the stamped row (mirror of the dropped-
|
|
1191
|
+
// baseline-limitation check above).
|
|
1192
|
+
for (const claim of answerClaims) {
|
|
1193
|
+
if (!shortfallIds.has(claim.answer_claim_id))
|
|
1194
|
+
continue;
|
|
1195
|
+
if (!answerClaimMatchesBaselineRow(claim, baselineRow))
|
|
1196
|
+
continue;
|
|
1197
|
+
if (!stampedLimitationRefs.has(judgeSupportShortfallToken(claim.answer_claim_id))) {
|
|
1198
|
+
violations.push(violation({
|
|
1199
|
+
code: "conflicting_state",
|
|
1200
|
+
message: "matrix row dropped the judge-support-shortfall limitation token for a matching degraded claim",
|
|
1201
|
+
subjectId: row.matrix_row_id,
|
|
1202
|
+
}));
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const dischargedForRow = baselineRow.limitation_refs.filter((ref) => dischargedLimsForRow.has(ref)).length;
|
|
1206
|
+
const expectedReadiness = deriveMemberReadiness({
|
|
1207
|
+
materiality: row.materiality,
|
|
1208
|
+
maturityLevel: row.maturity_level,
|
|
1209
|
+
residualLimitationRefs: row.limitation_refs,
|
|
1210
|
+
dischargedForRow,
|
|
1211
|
+
});
|
|
733
1212
|
if (row.member_readiness !== expectedReadiness) {
|
|
734
1213
|
violations.push(violation({
|
|
735
1214
|
code: "conflicting_state",
|
|
736
|
-
message: "matrix member_readiness must follow material L4, frontier, or limitation state",
|
|
1215
|
+
message: "matrix member_readiness must follow material L4, frontier, value-discharge, or limitation state",
|
|
737
1216
|
subjectId: row.matrix_row_id,
|
|
738
1217
|
}));
|
|
739
1218
|
}
|
|
1219
|
+
// blocking_question_refs reverse-link conservation (G track):
|
|
1220
|
+
// before the frontier exists (baseline matrix) the row must cite no questions;
|
|
1221
|
+
// once the validated frontier exists (current matrix) an open frontier_required row
|
|
1222
|
+
// must cite its blocking question(s), a closed/limitation-backed row must cite none,
|
|
1223
|
+
// and every cited ref must resolve to a frontier question that names this row.
|
|
1224
|
+
if (!frontierAvailable) {
|
|
1225
|
+
if (row.blocking_question_refs.length > 0) {
|
|
1226
|
+
violations.push(violation({
|
|
1227
|
+
code: "conflicting_state",
|
|
1228
|
+
message: "matrix row cannot cite blocking questions before the question frontier exists",
|
|
1229
|
+
subjectId: row.matrix_row_id,
|
|
1230
|
+
}));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
else {
|
|
1234
|
+
const rowIsFrontier = row.member_readiness === "frontier_required";
|
|
1235
|
+
const citedQuestions = new Set(row.blocking_question_refs);
|
|
1236
|
+
if (rowIsFrontier) {
|
|
1237
|
+
// Exact reverse link: an open row must cite EVERY validated frontier question that
|
|
1238
|
+
// names it, not merely one of them — otherwise a stale/edited matrix could drop one
|
|
1239
|
+
// of several questions for a row and still hide an open blocking question.
|
|
1240
|
+
const expectedQuestions = new Set();
|
|
1241
|
+
for (const baselineRowRef of row.baseline_row_refs) {
|
|
1242
|
+
for (const questionId of frontierQuestionsByBaselineRow.get(baselineRowRef) ??
|
|
1243
|
+
[]) {
|
|
1244
|
+
expectedQuestions.add(questionId);
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
// The supplied frontier must actually name this open material row. An empty
|
|
1248
|
+
// expected set means a stale/edited/mismatched frontier (e.g. a valid validation
|
|
1249
|
+
// paired with a different frontier artifact) — without this guard the coverage
|
|
1250
|
+
// loop below would be vacuous and an unresolved row would validate with empty refs.
|
|
1251
|
+
if (expectedQuestions.size === 0) {
|
|
1252
|
+
violations.push(violation({
|
|
1253
|
+
code: "missing_required_coverage",
|
|
1254
|
+
message: "frontier-required matrix row has no matching question in the supplied frontier",
|
|
1255
|
+
subjectId: row.matrix_row_id,
|
|
1256
|
+
}));
|
|
1257
|
+
}
|
|
1258
|
+
for (const expectedQuestionId of expectedQuestions) {
|
|
1259
|
+
if (!citedQuestions.has(expectedQuestionId)) {
|
|
1260
|
+
violations.push(violation({
|
|
1261
|
+
code: "missing_required_coverage",
|
|
1262
|
+
message: "frontier-required matrix row must cite every blocking maturation question that names it",
|
|
1263
|
+
subjectId: row.matrix_row_id,
|
|
1264
|
+
}));
|
|
1265
|
+
}
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
if (!rowIsFrontier && row.blocking_question_refs.length > 0) {
|
|
1269
|
+
violations.push(violation({
|
|
1270
|
+
code: "conflicting_state",
|
|
1271
|
+
message: "closed or limitation-backed matrix row cannot cite open blocking questions",
|
|
1272
|
+
subjectId: row.matrix_row_id,
|
|
1273
|
+
}));
|
|
1274
|
+
}
|
|
1275
|
+
for (const questionRef of row.blocking_question_refs) {
|
|
1276
|
+
const questionBaselineRows = frontierQuestionBaselineRows.get(questionRef);
|
|
1277
|
+
if (!questionBaselineRows) {
|
|
1278
|
+
violations.push(violation({
|
|
1279
|
+
code: "unknown_id",
|
|
1280
|
+
message: "matrix blocking_question_refs must resolve to the validated question frontier",
|
|
1281
|
+
subjectId: questionRef,
|
|
1282
|
+
}));
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const namesRow = row.baseline_row_refs.some((ref) => questionBaselineRows.has(ref));
|
|
1286
|
+
if (!namesRow) {
|
|
1287
|
+
violations.push(violation({
|
|
1288
|
+
code: "conflicting_state",
|
|
1289
|
+
message: "matrix blocking question must name this row's baseline ref (reverse link must mirror the frontier)",
|
|
1290
|
+
subjectId: row.matrix_row_id,
|
|
1291
|
+
}));
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
// M1 coverage conservation: every baseline row must map to exactly one matrix row.
|
|
1297
|
+
// The loop above only checks that PRESENT matrix rows resolve to a baseline row, so a
|
|
1298
|
+
// matrix that DROPS a baseline row (erasing its scope from the downstream claim) would
|
|
1299
|
+
// otherwise pass silently.
|
|
1300
|
+
const matrixCoverageCounts = new Map();
|
|
1301
|
+
for (const row of matrix.rows) {
|
|
1302
|
+
for (const ref of row.baseline_row_refs) {
|
|
1303
|
+
matrixCoverageCounts.set(ref, (matrixCoverageCounts.get(ref) ?? 0) + 1);
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
for (const baselineRowId of baselineRows.keys()) {
|
|
1307
|
+
const count = matrixCoverageCounts.get(baselineRowId) ?? 0;
|
|
1308
|
+
if (count !== 1) {
|
|
1309
|
+
violations.push(violation({
|
|
1310
|
+
code: count === 0 ? "missing_required_coverage" : "conflicting_state",
|
|
1311
|
+
message: count === 0
|
|
1312
|
+
? `baseline row has no actionability matrix row: ${baselineRowId}`
|
|
1313
|
+
: `baseline row is covered by multiple matrix rows: ${baselineRowId}`,
|
|
1314
|
+
subjectId: baselineRowId,
|
|
1315
|
+
}));
|
|
1316
|
+
}
|
|
740
1317
|
}
|
|
741
1318
|
return {
|
|
742
1319
|
schema_version: "1",
|
|
@@ -746,18 +1323,26 @@ export function validateActionabilityMatrix(args) {
|
|
|
746
1323
|
maturation_baseline_validation_ref: args.maturationBaselineValidationRef ?? null,
|
|
747
1324
|
maturation_answer_claims_validation_ref: args.maturationAnswerClaimsValidationRef ?? null,
|
|
748
1325
|
ontology_expansion_validation_ref: args.ontologyExpansionValidationRef ?? null,
|
|
1326
|
+
maturation_question_frontier_validation_ref: args.maturationQuestionFrontierValidationRef ?? null,
|
|
749
1327
|
validation_status: violations.length === 0 ? "valid" : "invalid",
|
|
750
1328
|
matrix_row_count: matrix.rows.length,
|
|
751
1329
|
frontier_required_row_count: matrix.rows.filter((row) => row.member_readiness === "frontier_required").length,
|
|
752
1330
|
validation_results: violations.length === 0
|
|
753
1331
|
? ["actionability_matrix_valid"]
|
|
754
1332
|
: ["actionability_matrix_invalid"],
|
|
1333
|
+
// G(a) attribution: the mode (computed above as postFrontierInputsPresent) selects which of
|
|
1334
|
+
// the two validators served by this fn owns the recorded obligation pair.
|
|
1335
|
+
validator_id: postFrontierInputsPresent
|
|
1336
|
+
? "actionability-matrix-validator"
|
|
1337
|
+
: "baseline-actionability-matrix-validator",
|
|
1338
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
755
1339
|
violations,
|
|
756
1340
|
};
|
|
757
1341
|
}
|
|
758
1342
|
export function validateMaturationQuestionFrontier(args) {
|
|
759
1343
|
const frontier = args.maturationQuestionFrontier;
|
|
760
1344
|
const violations = [];
|
|
1345
|
+
const assertedObligationIds = [];
|
|
761
1346
|
const baselineRows = new Map(args.maturationBaseline.baseline_rows.map((row) => [row.baseline_row_id, row]));
|
|
762
1347
|
const materialFrontierBaselineRows = new Set(args.actionabilityMatrix.rows
|
|
763
1348
|
.filter((row) => row.member_readiness === "frontier_required")
|
|
@@ -785,6 +1370,13 @@ export function validateMaturationQuestionFrontier(args) {
|
|
|
785
1370
|
subjectId: args.actionabilityMatrixValidationRef ?? null,
|
|
786
1371
|
}));
|
|
787
1372
|
}
|
|
1373
|
+
// G(a) slice 6: record the two question-frontier obligations with a distinct, audited enforcement
|
|
1374
|
+
// region, before the per-question loop so they fire on a zero-question frontier. The other four
|
|
1375
|
+
// obligations stay parked with ledger audit notes — authority-need first-class scoping and the
|
|
1376
|
+
// competency/domain-trace refs are NOT validated here (NOT_FOUND), and the answer_status enum and
|
|
1377
|
+
// the seed+limitation authority clauses are unenforced (the obligation names are broader than code).
|
|
1378
|
+
assertObligation(assertedObligationIds, "require_blocker_and_high_questions_to_have_closure_frontier_limitation_or_authority_need");
|
|
1379
|
+
assertObligation(assertedObligationIds, "require_unique_question_id");
|
|
788
1380
|
for (const question of frontier.questions) {
|
|
789
1381
|
if (seen.has(question.question_id)) {
|
|
790
1382
|
violations.push(violation({
|
|
@@ -862,6 +1454,7 @@ export function validateMaturationQuestionFrontier(args) {
|
|
|
862
1454
|
validation_results: violations.length === 0
|
|
863
1455
|
? ["maturation_question_frontier_valid"]
|
|
864
1456
|
: ["maturation_question_frontier_invalid"],
|
|
1457
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
865
1458
|
violations,
|
|
866
1459
|
};
|
|
867
1460
|
}
|
|
@@ -899,6 +1492,19 @@ function questionMap(frontier) {
|
|
|
899
1492
|
export function validateMaturationClosureFrontier(args) {
|
|
900
1493
|
const frontier = args.maturationClosureFrontier;
|
|
901
1494
|
const violations = [];
|
|
1495
|
+
// G(a) slice 25 — RECORD only the 3 obligations that fully enforce their named scope with no
|
|
1496
|
+
// runtime/registry/edge gaps (codex R1 found this validator is NOT cleanly structural — 7 obligations
|
|
1497
|
+
// have deeper gaps, see obligation-coverage-ledger.yaml notes): #2 dup-source only de-dupes the id, not
|
|
1498
|
+
// same-target-ref duplicates (runtime re-entry throws); #4 accepts exists:false/skipped inventory rows
|
|
1499
|
+
// (runtime buildReconstructSourceObservation returns null); #10 mixed-lineage never fires (source
|
|
1500
|
+
// requests carry concrete per-ref kinds, never "mixed"); #1 authority-dedup keys on the whole
|
|
1501
|
+
// question_refs set, missing per-question overlap; #8/#9 don't reject empty question_refs; and #6/#8/#9
|
|
1502
|
+
// read the maturation-question-frontier ARTIFACT which the registry does not declare as an input
|
|
1503
|
+
// authority (declared≠wired, slice-14 pattern). Stamped before any per-request guard (zero-row input).
|
|
1504
|
+
const assertedObligationIds = [];
|
|
1505
|
+
assertObligation(assertedObligationIds, "reject_semantic_only_locations");
|
|
1506
|
+
assertObligation(assertedObligationIds, "require_unique_authority_request_id");
|
|
1507
|
+
assertObligation(assertedObligationIds, "validate_authority_request_kind_expected_response_kind_and_scope");
|
|
902
1508
|
const materialQuestions = materialQuestionIds(args.maturationQuestionFrontier);
|
|
903
1509
|
const questions = questionMap(args.maturationQuestionFrontier);
|
|
904
1510
|
const sourceRequestsSeen = new Set();
|
|
@@ -1103,6 +1709,7 @@ export function validateMaturationClosureFrontier(args) {
|
|
|
1103
1709
|
validation_results: violations.length === 0
|
|
1104
1710
|
? ["maturation_closure_frontier_valid"]
|
|
1105
1711
|
: ["maturation_closure_frontier_invalid"],
|
|
1712
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1106
1713
|
violations,
|
|
1107
1714
|
};
|
|
1108
1715
|
}
|
|
@@ -1221,9 +1828,225 @@ export function validateMaturationAuthorityResponse(args) {
|
|
|
1221
1828
|
violations,
|
|
1222
1829
|
};
|
|
1223
1830
|
}
|
|
1831
|
+
// Maturation value-read cut (design §13.5 F4/F5). Governance + structural validator for the
|
|
1832
|
+
// value-discharge artifact. Mirrors the answer-support source-safety precondition gate and the
|
|
1833
|
+
// per-evidence consumption_allowed check, and adds the F4 read-path constraint: value evidence
|
|
1834
|
+
// may come ONLY from an already-observed, runtime-target (basis A) source — reading a non-target
|
|
1835
|
+
// source would leak its raw values into the discharge prompt. Structural: each discharge may only
|
|
1836
|
+
// target a limitation that actually exists on its baseline row. The matrix derive-and-assert
|
|
1837
|
+
// consumes ONLY discharges whose validation here is `valid`.
|
|
1838
|
+
export function validateMaturationValueDischarge(args) {
|
|
1839
|
+
const discharge = args.maturationValueDischarge;
|
|
1840
|
+
const violations = [];
|
|
1841
|
+
const baselineRows = new Map(args.maturationBaseline.baseline_rows.map((row) => [row.baseline_row_id, row]));
|
|
1842
|
+
const sourceObservationsById = new Map(args.sourceObservations.observations.map((o) => [o.observation_id, o]));
|
|
1843
|
+
const safetyRowsById = new Map((args.sourceSafetyLedger?.safety_rows ?? []).map((r) => [r.safety_row_id, r]));
|
|
1844
|
+
if (discharge.session_id !== args.maturationBaseline.session_id) {
|
|
1845
|
+
violations.push(violation({
|
|
1846
|
+
code: "session_id_mismatch",
|
|
1847
|
+
message: "value discharge session_id must match maturation baseline",
|
|
1848
|
+
subjectId: discharge.session_id,
|
|
1849
|
+
}));
|
|
1850
|
+
}
|
|
1851
|
+
if (args.maturationBaselineValidation.validation_status !== "valid") {
|
|
1852
|
+
violations.push(violation({
|
|
1853
|
+
code: "prior_validation_invalid",
|
|
1854
|
+
message: "value discharge requires valid maturation baseline validation",
|
|
1855
|
+
subjectId: args.maturationBaselineValidationRef ?? null,
|
|
1856
|
+
}));
|
|
1857
|
+
}
|
|
1858
|
+
const hasDischarges = discharge.discharges.length > 0;
|
|
1859
|
+
// Source-safety precondition (replicates the answer-support gate, F5): discharges make
|
|
1860
|
+
// material claims, so the safety ledger and its valid validation must be supplied and
|
|
1861
|
+
// certify the consumed ledger before the per-evidence consumption checks run.
|
|
1862
|
+
if (hasDischarges && !args.sourceSafetyLedger) {
|
|
1863
|
+
violations.push(violation({
|
|
1864
|
+
code: "missing_required_ref",
|
|
1865
|
+
message: "value discharge requires the source safety ledger",
|
|
1866
|
+
subjectId: args.sourceSafetyLedgerRef ?? null,
|
|
1867
|
+
}));
|
|
1868
|
+
}
|
|
1869
|
+
if (hasDischarges && !args.sourceSafetyLedgerValidation) {
|
|
1870
|
+
violations.push(violation({
|
|
1871
|
+
code: "prior_validation_invalid",
|
|
1872
|
+
message: "value discharge requires source safety ledger validation",
|
|
1873
|
+
subjectId: args.sourceSafetyLedgerValidationRef ?? null,
|
|
1874
|
+
}));
|
|
1875
|
+
}
|
|
1876
|
+
if (args.sourceSafetyLedgerValidation &&
|
|
1877
|
+
args.sourceSafetyLedgerValidation.validation_status !== "valid") {
|
|
1878
|
+
violations.push(violation({
|
|
1879
|
+
code: "prior_validation_invalid",
|
|
1880
|
+
message: "value discharge requires valid source safety ledger validation",
|
|
1881
|
+
subjectId: args.sourceSafetyLedgerValidationRef ?? null,
|
|
1882
|
+
}));
|
|
1883
|
+
}
|
|
1884
|
+
if (args.sourceSafetyLedger &&
|
|
1885
|
+
args.sourceSafetyLedgerValidation &&
|
|
1886
|
+
args.sourceSafetyLedgerValidationRef &&
|
|
1887
|
+
args.sourceSafetyLedgerValidation.source_safety_ledger_ref &&
|
|
1888
|
+
path.resolve(args.sourceSafetyLedgerValidation.source_safety_ledger_ref) !==
|
|
1889
|
+
path.resolve(args.sourceSafetyLedgerRef ?? "")) {
|
|
1890
|
+
violations.push(violation({
|
|
1891
|
+
code: "prior_validation_invalid",
|
|
1892
|
+
message: "value discharge source safety ledger validation must certify the consumed source safety ledger",
|
|
1893
|
+
subjectId: args.sourceSafetyLedgerValidationRef ?? null,
|
|
1894
|
+
}));
|
|
1895
|
+
}
|
|
1896
|
+
const seen = new Set();
|
|
1897
|
+
let satisfiedCount = 0;
|
|
1898
|
+
for (const entry of discharge.discharges) {
|
|
1899
|
+
if (seen.has(entry.discharge_id)) {
|
|
1900
|
+
violations.push(violation({
|
|
1901
|
+
code: "duplicate_id",
|
|
1902
|
+
message: "value discharge entries require a unique discharge_id",
|
|
1903
|
+
subjectId: entry.discharge_id,
|
|
1904
|
+
}));
|
|
1905
|
+
}
|
|
1906
|
+
seen.add(entry.discharge_id);
|
|
1907
|
+
if (entry.satisfaction_status === "satisfied")
|
|
1908
|
+
satisfiedCount += 1;
|
|
1909
|
+
// Structural: each targeted limitation must actually exist on the targeted baseline row —
|
|
1910
|
+
// a discharge cannot subtract a phantom limitation (matrix derive-and-assert depends on this).
|
|
1911
|
+
for (const baselineRowRef of entry.target_baseline_row_refs) {
|
|
1912
|
+
const baselineRow = baselineRows.get(baselineRowRef);
|
|
1913
|
+
if (!baselineRow) {
|
|
1914
|
+
violations.push(violation({
|
|
1915
|
+
code: "unknown_id",
|
|
1916
|
+
message: "value discharge target_baseline_row_refs must resolve to maturation baseline rows",
|
|
1917
|
+
subjectId: baselineRowRef,
|
|
1918
|
+
}));
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
const baselineLimitations = new Set(baselineRow.limitation_refs);
|
|
1922
|
+
for (const limitationRef of entry.target_limitation_refs) {
|
|
1923
|
+
if (!baselineLimitations.has(limitationRef)) {
|
|
1924
|
+
violations.push(violation({
|
|
1925
|
+
code: "missing_required_ref",
|
|
1926
|
+
message: "value discharge target_limitation_refs must exist on the targeted baseline row's limitation_refs",
|
|
1927
|
+
subjectId: `${baselineRowRef}:${limitationRef}`,
|
|
1928
|
+
}));
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
}
|
|
1932
|
+
// Governance (F4/F5): the value evidence must come from an ALREADY-OBSERVED, runtime-target
|
|
1933
|
+
// (basis A) source whose material_claim safety row is consumption_allowed. A non-target
|
|
1934
|
+
// source is rejected outright — reading it would leak its raw values into the prompt.
|
|
1935
|
+
const observationId = entry.value_evidence_ref.observation_id;
|
|
1936
|
+
const observation = sourceObservationsById.get(observationId);
|
|
1937
|
+
if (!observation) {
|
|
1938
|
+
violations.push(violation({
|
|
1939
|
+
code: "unknown_id",
|
|
1940
|
+
message: "value discharge value_evidence_ref.observation_id must resolve to an already-observed source",
|
|
1941
|
+
subjectId: observationId,
|
|
1942
|
+
}));
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (observation.is_runtime_target_source !== true) {
|
|
1946
|
+
violations.push(violation({
|
|
1947
|
+
code: "conflicting_state",
|
|
1948
|
+
message: "value discharge may read only runtime-target sources (basis A); a non-target source cannot be read",
|
|
1949
|
+
subjectId: observationId,
|
|
1950
|
+
}));
|
|
1951
|
+
}
|
|
1952
|
+
const expectedAuthorizationRef = `${observationId}:material_claim`;
|
|
1953
|
+
if (entry.value_evidence_authorization_ref !== expectedAuthorizationRef) {
|
|
1954
|
+
violations.push(violation({
|
|
1955
|
+
code: "conflicting_state",
|
|
1956
|
+
message: "value discharge value_evidence_authorization_ref must be the canonical <observation_id>:material_claim ref",
|
|
1957
|
+
subjectId: entry.value_evidence_authorization_ref,
|
|
1958
|
+
}));
|
|
1959
|
+
}
|
|
1960
|
+
if (args.sourceSafetyLedger) {
|
|
1961
|
+
const materialClaimRowId = sourceSafetyRowIdForObservation(observation, "material_claim");
|
|
1962
|
+
const materialClaimRow = safetyRowsById.get(materialClaimRowId);
|
|
1963
|
+
if (!materialClaimRow ||
|
|
1964
|
+
materialClaimRow.proof_sufficiency_state !== "sufficient_for_claim" ||
|
|
1965
|
+
materialClaimRow.visibility_tier !== "consumption_allowed") {
|
|
1966
|
+
violations.push(violation({
|
|
1967
|
+
code: "missing_required_ref",
|
|
1968
|
+
message: "value discharge evidence must have an observation-specific material_claim source-safety row that is consumption_allowed and sufficient for claim",
|
|
1969
|
+
subjectId: observationId,
|
|
1970
|
+
}));
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
// Provenance floor (design §15.4): a `satisfied` discharge must rest on a REAL, COMPLETE,
|
|
1974
|
+
// content-bound read — otherwise it cannot drive value_resolved. A 0-cell read (dead/empty), a
|
|
1975
|
+
// truncated read (partial view), or a content-hash mismatch vs the authorized observation's
|
|
1976
|
+
// observed bytes (file changed between observation and the re-read) → reject. This makes
|
|
1977
|
+
// cells_read / read_truncated / read_content_sha256 REAL consumers, not inert provenance
|
|
1978
|
+
// (FRP-1 / issue-008 / GL-1). refuted/inconclusive discharges never subtract, so they are exempt.
|
|
1979
|
+
if (entry.satisfaction_status === "satisfied") {
|
|
1980
|
+
const evidence = entry.value_evidence_ref;
|
|
1981
|
+
if (evidence.cells_read <= 0) {
|
|
1982
|
+
violations.push(violation({
|
|
1983
|
+
code: "conflicting_state",
|
|
1984
|
+
message: "a satisfied value discharge must be backed by a non-empty cell read (cells_read > 0)",
|
|
1985
|
+
subjectId: entry.discharge_id,
|
|
1986
|
+
}));
|
|
1987
|
+
}
|
|
1988
|
+
if (evidence.read_truncated) {
|
|
1989
|
+
violations.push(violation({
|
|
1990
|
+
code: "conflicting_state",
|
|
1991
|
+
message: "a satisfied value discharge cannot rest on a truncated (incomplete) cell read",
|
|
1992
|
+
subjectId: entry.discharge_id,
|
|
1993
|
+
}));
|
|
1994
|
+
}
|
|
1995
|
+
const observedContentSha256 = observation.structural_data
|
|
1996
|
+
?.workbook_inventory?.content_sha256;
|
|
1997
|
+
// Fail-closed (design §16.2, onto issue-013): a satisfied discharge must be content-bound to the
|
|
1998
|
+
// authorized observation's observed bytes. If the observation carries NO observed content hash to
|
|
1999
|
+
// bind against, the binding cannot be proven → reject rather than silently skip the check.
|
|
2000
|
+
if (typeof observedContentSha256 !== "string" || observedContentSha256.length === 0) {
|
|
2001
|
+
violations.push(violation({
|
|
2002
|
+
code: "conflicting_state",
|
|
2003
|
+
message: "a satisfied value discharge requires the authorized observation to carry an observed content_sha256 to content-bind against",
|
|
2004
|
+
subjectId: entry.discharge_id,
|
|
2005
|
+
}));
|
|
2006
|
+
}
|
|
2007
|
+
else if (evidence.read_content_sha256 !== observedContentSha256) {
|
|
2008
|
+
violations.push(violation({
|
|
2009
|
+
code: "conflicting_state",
|
|
2010
|
+
message: "value discharge read_content_sha256 must match the authorized observation's observed content_sha256 (the source changed between observation and the maturation re-read)",
|
|
2011
|
+
subjectId: entry.discharge_id,
|
|
2012
|
+
}));
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
}
|
|
2016
|
+
return {
|
|
2017
|
+
schema_version: "1",
|
|
2018
|
+
session_id: discharge.session_id,
|
|
2019
|
+
created_at: isoNow(),
|
|
2020
|
+
maturation_value_discharge_ref: args.maturationValueDischargeRef ?? null,
|
|
2021
|
+
source_safety_ledger_validation_ref: args.sourceSafetyLedgerValidationRef ?? null,
|
|
2022
|
+
source_observation_reentry_validation_ref: null,
|
|
2023
|
+
maturation_baseline_validation_ref: args.maturationBaselineValidationRef ?? null,
|
|
2024
|
+
validation_status: violations.length === 0 ? "valid" : "invalid",
|
|
2025
|
+
discharge_count: discharge.discharges.length,
|
|
2026
|
+
satisfied_discharge_count: satisfiedCount,
|
|
2027
|
+
validation_results: violations.length === 0
|
|
2028
|
+
? ["maturation_value_discharge_valid"]
|
|
2029
|
+
: ["maturation_value_discharge_invalid"],
|
|
2030
|
+
asserted_obligation_ids: [],
|
|
2031
|
+
violations,
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
1224
2034
|
export function validateAnswerSupportLedger(args) {
|
|
1225
2035
|
const ledger = args.answerSupportLedger;
|
|
1226
2036
|
const violations = [];
|
|
2037
|
+
// G(a) slice 21 — record the obligations this validator genuinely enforces (RECORD 4/9). The
|
|
2038
|
+
// other 5 stay parked (see obligation-coverage-ledger.yaml notes): contradictions-bounded is
|
|
2039
|
+
// enforced by validateMaturationAnswerClaims (DELEGATED); the frontier→valid-lineage-index-
|
|
2040
|
+
// validation check is an unscoped global precondition (PARTIAL); the external/runtime + generic
|
|
2041
|
+
// support-mode-required-refs obligations have no distinct, non-overlapping enforcement here; and
|
|
2042
|
+
// user_confirmation is presence-only (it never resolves user_confirmation_refs to the confirmation
|
|
2043
|
+
// authority — an arbitrary ref passes whenever a valid purpose-confirmation exists; PARTIAL, codex
|
|
2044
|
+
// R1). Stamped before any per-cluster guard so the recorder fires on zero-row input.
|
|
2045
|
+
const assertedObligationIds = [];
|
|
2046
|
+
assertObligation(assertedObligationIds, "validate_evidence_cluster_question_refs");
|
|
2047
|
+
assertObligation(assertedObligationIds, "require_two_independent_evidence_refs_for_convergent_source_evidence_unless_direct_authority");
|
|
2048
|
+
assertObligation(assertedObligationIds, "require_frontier_triggered_evidence_to_resolve_to_valid_reentry_validation");
|
|
2049
|
+
assertObligation(assertedObligationIds, "require_observation_specific_evidence_support_source_safety_row_with_claim_sufficiency_and_replay");
|
|
1227
2050
|
const questions = questionMap(args.maturationQuestionFrontier);
|
|
1228
2051
|
const evidenceIndex = evidenceRefIndex(args.sourceObservations);
|
|
1229
2052
|
const lineageObservationIds = new Set(args.sourceObservationLineageIndex?.lineage_rows.flatMap((row) => row.added_observation_ids) ?? []);
|
|
@@ -1477,9 +2300,17 @@ export function validateAnswerSupportLedger(args) {
|
|
|
1477
2300
|
const observation = sourceObservationsById.get(ref.observation_id);
|
|
1478
2301
|
const lineageRow = lineageRowByObservationId.get(ref.observation_id) ??
|
|
1479
2302
|
null;
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
2303
|
+
// Defect-3 G1: a per-ref lineage-index row only exists for genuine frontier
|
|
2304
|
+
// RE-ENTRY observations, which the producer stamps with a
|
|
2305
|
+
// triggering_frontier_validation_ref. The INITIAL observation carries the
|
|
2306
|
+
// sentinel round_id "initial_source_frontier" and an "...:initial" batch id
|
|
2307
|
+
// but is NOT a re-entry and never appears in the lineage index, so keying
|
|
2308
|
+
// this per-ref resolution on round_id/observation_batch_id over-fired on
|
|
2309
|
+
// every single-source run. triggering_frontier_validation_ref is the
|
|
2310
|
+
// canonical lineage-bearing marker (set at run.ts re-observation callers).
|
|
2311
|
+
// The broader index-PRESENCE predicate sourceBackedEvidenceCarriesLineage
|
|
2312
|
+
// (above) is intentionally LEFT BROAD — do not narrow it here.
|
|
2313
|
+
const refCarriesLineage = Boolean(observation?.triggering_frontier_validation_ref);
|
|
1483
2314
|
if (refCarriesLineage &&
|
|
1484
2315
|
args.sourceObservationLineageIndex &&
|
|
1485
2316
|
!lineageRow) {
|
|
@@ -1546,8 +2377,7 @@ export function validateAnswerSupportLedger(args) {
|
|
|
1546
2377
|
}
|
|
1547
2378
|
if (!observation ||
|
|
1548
2379
|
!publicOutputRow ||
|
|
1549
|
-
|
|
1550
|
-
publicOutputRow.visibility_tier !== "redacted_output_only")) {
|
|
2380
|
+
publicOutputRow.visibility_tier !== "consumption_allowed") {
|
|
1551
2381
|
violations.push(violation({
|
|
1552
2382
|
code: "missing_required_ref",
|
|
1553
2383
|
message: "answer support evidence used in public output must have an observation-specific public_output source-safety row",
|
|
@@ -1634,12 +2464,15 @@ export function validateAnswerSupportLedger(args) {
|
|
|
1634
2464
|
validation_results: violations.length === 0
|
|
1635
2465
|
? ["answer_support_ledger_valid"]
|
|
1636
2466
|
: ["answer_support_ledger_invalid"],
|
|
2467
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1637
2468
|
violations,
|
|
1638
2469
|
};
|
|
1639
2470
|
}
|
|
1640
2471
|
export function validateMaturationAnswerClaims(args) {
|
|
1641
2472
|
const artifact = args.maturationAnswerClaims;
|
|
1642
2473
|
const violations = [];
|
|
2474
|
+
const assertedObligationIds = [];
|
|
2475
|
+
const judgeSupportShortfallClaimIds = new Set();
|
|
1643
2476
|
const questions = questionMap(args.maturationQuestionFrontier);
|
|
1644
2477
|
const clusters = new Map(args.answerSupportLedger.evidence_clusters.map((cluster) => [
|
|
1645
2478
|
cluster.evidence_cluster_id,
|
|
@@ -1685,6 +2518,15 @@ export function validateMaturationAnswerClaims(args) {
|
|
|
1685
2518
|
subjectId: args.maturationQuestionFrontierValidationRef ?? null,
|
|
1686
2519
|
}));
|
|
1687
2520
|
}
|
|
2521
|
+
// G(a) slice 4: record the two answer-claims obligations with a distinct, audited enforcement region,
|
|
2522
|
+
// UNCONDITIONALLY before the per-claim loop so they are proven wired even on a zero-claim input. The
|
|
2523
|
+
// judge pair is the live-enforced #57/#58 gate (insufficient_independent_evidence / fail-closed
|
|
2524
|
+
// prior_validation_invalid in the convergent-source block below) — recording it here closes its
|
|
2525
|
+
// enforced_pending_instrumentation tier. The other three obligations stay parked with ledger audit
|
|
2526
|
+
// notes (the "OR frontier"/"OR authority" alternatives are unimplemented; dimension/purpose refs are
|
|
2527
|
+
// presence-checked, not resolved against the question), rather than being laundered into recorded.
|
|
2528
|
+
assertObligation(assertedObligationIds, "require_convergent_source_evidence_claims_to_have_two_independent_judge_confirmed_supports");
|
|
2529
|
+
assertObligation(assertedObligationIds, "validate_answer_claim_question_refs");
|
|
1688
2530
|
for (const claim of artifact.answer_claims) {
|
|
1689
2531
|
if (seen.has(claim.answer_claim_id)) {
|
|
1690
2532
|
violations.push(violation({
|
|
@@ -1786,11 +2628,40 @@ export function validateMaturationAnswerClaims(args) {
|
|
|
1786
2628
|
}
|
|
1787
2629
|
}
|
|
1788
2630
|
if (independentConfirmed.size < 2) {
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
2631
|
+
// Site-7 proportional terminal (design 20260706 §4.1): split the shortfall
|
|
2632
|
+
// disposition by a QUESTION-scoped pool computed directly from the judgment
|
|
2633
|
+
// artifact's supported verdicts (never joined through the claim's own refs, so
|
|
2634
|
+
// a future ref-serialization divergence cannot mass-degrade):
|
|
2635
|
+
// - pool >= 2 → the question COULD be certified; the author under-cited refs
|
|
2636
|
+
// or clusters → violation stays (bug catcher, crash).
|
|
2637
|
+
// - pool < 2 AND the judge supported >= 1 ref somewhere in the run (functioned
|
|
2638
|
+
// contrast control) → the source cannot certify this question → degrade:
|
|
2639
|
+
// recorded in judge_support_shortfall_claim_ids, the artifact stays valid,
|
|
2640
|
+
// and the actionability matrix blocks certification downstream.
|
|
2641
|
+
// - pool < 2 AND the judge supported NOTHING run-wide → indistinguishable from
|
|
2642
|
+
// judge dysfunction (an all-not_supported judgment is schema-valid), so the
|
|
2643
|
+
// loud violation is kept.
|
|
2644
|
+
const questionClusterIds = new Set(args.answerSupportLedger.evidence_clusters
|
|
2645
|
+
.filter((cluster) => cluster.support_mode === "convergent_source_evidence" &&
|
|
2646
|
+
cluster.question_refs.includes(claim.question_id))
|
|
2647
|
+
.map((cluster) => cluster.evidence_cluster_id));
|
|
2648
|
+
const questionPoolIndependent = new Set();
|
|
2649
|
+
for (const judgment of args.answerSupportJudgment?.judgments ?? []) {
|
|
2650
|
+
if (judgment.supports === "supported" &&
|
|
2651
|
+
questionClusterIds.has(judgment.evidence_cluster_ref)) {
|
|
2652
|
+
questionPoolIndependent.add(`${normalizedPathRef(judgment.evidence_ref.source_ref)}:${normalizedPathRef(judgment.evidence_ref.location)}`);
|
|
2653
|
+
}
|
|
2654
|
+
}
|
|
2655
|
+
if (questionPoolIndependent.size < 2 && judgeSupported.size > 0) {
|
|
2656
|
+
judgeSupportShortfallClaimIds.add(claim.answer_claim_id);
|
|
2657
|
+
}
|
|
2658
|
+
else {
|
|
2659
|
+
violations.push(violation({
|
|
2660
|
+
code: "insufficient_independent_evidence",
|
|
2661
|
+
message: "convergent answer claim requires at least two independent judge-confirmed supports",
|
|
2662
|
+
subjectId: claim.answer_claim_id,
|
|
2663
|
+
}));
|
|
2664
|
+
}
|
|
1794
2665
|
}
|
|
1795
2666
|
}
|
|
1796
2667
|
}
|
|
@@ -1820,9 +2691,11 @@ export function validateMaturationAnswerClaims(args) {
|
|
|
1820
2691
|
validation_status: violations.length === 0 ? "valid" : "invalid",
|
|
1821
2692
|
answer_claim_count: artifact.answer_claims.length,
|
|
1822
2693
|
answered_question_count: answeredQuestions.size,
|
|
2694
|
+
judge_support_shortfall_claim_ids: [...judgeSupportShortfallClaimIds].sort(),
|
|
1823
2695
|
validation_results: violations.length === 0
|
|
1824
2696
|
? ["maturation_answer_claims_valid"]
|
|
1825
2697
|
: ["maturation_answer_claims_invalid"],
|
|
2698
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1826
2699
|
violations,
|
|
1827
2700
|
};
|
|
1828
2701
|
}
|
|
@@ -1840,6 +2713,25 @@ export function validateOntologyExpansion(args) {
|
|
|
1840
2713
|
defer: 0,
|
|
1841
2714
|
reject: 0,
|
|
1842
2715
|
};
|
|
2716
|
+
// G(a) slice 9: record the two obligations whose enforcement matches the authoritative contract
|
|
2717
|
+
// (ontology-seeding-and-maturation-design.md §"ontology-expansion-validation.yaml must enforce"),
|
|
2718
|
+
// before the per-expansion loop so they fire on a zero-expansion artifact:
|
|
2719
|
+
// - validate_expansion_answer_claim_refs — every answer_claim_refs[] item resolves to a valid
|
|
2720
|
+
// answer claim (unknown_id) and add/refine cites ≥1 (missing_required_ref).
|
|
2721
|
+
// - require_concept_economy_rationale_when_surface_increases — the contract scopes this to
|
|
2722
|
+
// `operation: add` with `increases_surface`, which the per-expansion check enforces exactly
|
|
2723
|
+
// (missing_required_ref when rationale.trim().length < 24). A refine row that sets
|
|
2724
|
+
// increases_surface is OUT of the contract clause, not an enforcement gap.
|
|
2725
|
+
// PARKED (not recorded): prevent_in_place_seed_authority_rewrite — the contract clause is unscoped
|
|
2726
|
+
// ("no expansion rewrites seed authority in place") but the check only rejects refs whose
|
|
2727
|
+
// path.basename(ref) === "ontology-seed.yaml", so an LLM-authored anchored ref (e.g.
|
|
2728
|
+
// "ontology-seed.yaml#semantic_layer/object-new") bypasses it; narrower than the contract.
|
|
2729
|
+
// Also parked: the evidence-refs obligation — evidence_refs are resolved against the cited answer
|
|
2730
|
+
// claims' carried supporting_evidence_refs (a proxy), not the answer-support-ledger/seed authority
|
|
2731
|
+
// the name names. See obligation-coverage-ledger.yaml notes. No laundering.
|
|
2732
|
+
const assertedObligationIds = [];
|
|
2733
|
+
assertObligation(assertedObligationIds, "validate_expansion_answer_claim_refs");
|
|
2734
|
+
assertObligation(assertedObligationIds, "require_concept_economy_rationale_when_surface_increases");
|
|
1843
2735
|
if (artifact.session_id !== args.maturationAnswerClaims.session_id) {
|
|
1844
2736
|
violations.push(violation({
|
|
1845
2737
|
code: "session_id_mismatch",
|
|
@@ -1944,6 +2836,7 @@ export function validateOntologyExpansion(args) {
|
|
|
1944
2836
|
validation_results: violations.length === 0
|
|
1945
2837
|
? ["ontology_expansion_valid"]
|
|
1946
2838
|
: ["ontology_expansion_invalid"],
|
|
2839
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1947
2840
|
violations,
|
|
1948
2841
|
};
|
|
1949
2842
|
}
|
|
@@ -2153,6 +3046,14 @@ export function validateMaturationConvergenceLedger(args) {
|
|
|
2153
3046
|
const answerClaimIds = new Set(args.maturationAnswerClaims.answer_claims.map((claim) => claim.answer_claim_id));
|
|
2154
3047
|
const answerSupportIds = new Set(args.answerSupportLedger.evidence_clusters.map((cluster) => cluster.evidence_cluster_id));
|
|
2155
3048
|
const expansionIds = new Set(args.ontologyExpansion.expansions.map((expansion) => expansion.expansion_id));
|
|
3049
|
+
// G(a) obligation recorder (INV-OBLIGATION-COVERAGE-1). Only the closure-row delta-ref match is a
|
|
3050
|
+
// clean, fully-proving structural enforcer; it is stamped before the round loop so it records on
|
|
3051
|
+
// zero-round input too (vacuously true when no closure rows exist, like the slice-2/15 row checks).
|
|
3052
|
+
// The other six convergence obligations are parked with audit notes — see obligation-coverage-ledger.yaml
|
|
3053
|
+
// (ready-projection gate deferred per contract h; positive-support exclusion / disposition-value /
|
|
3054
|
+
// carried-forward-or-blocked-with-refs / source-delta validation-status all under-enforced).
|
|
3055
|
+
const assertedObligationIds = [];
|
|
3056
|
+
assertObligation(assertedObligationIds, "validate_closure_source_observation_delta_refs_match_source_observation_delta_validation");
|
|
2156
3057
|
if (ledger.session_id !== args.maturationQuestionFrontier.session_id) {
|
|
2157
3058
|
violations.push(violation({
|
|
2158
3059
|
code: "session_id_mismatch",
|
|
@@ -2494,6 +3395,7 @@ export function validateMaturationConvergenceLedger(args) {
|
|
|
2494
3395
|
validation_results: violations.length === 0
|
|
2495
3396
|
? ["maturation_convergence_ledger_valid"]
|
|
2496
3397
|
: ["maturation_convergence_ledger_invalid"],
|
|
3398
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
2497
3399
|
violations,
|
|
2498
3400
|
};
|
|
2499
3401
|
}
|
|
@@ -2719,15 +3621,50 @@ export async function writeMaturationSourceDeltaValidationArtifact(args) {
|
|
|
2719
3621
|
await writeYamlDocument(args.outputPath, validation);
|
|
2720
3622
|
return validation;
|
|
2721
3623
|
}
|
|
3624
|
+
// M4b: revision blockers (reject/defer) carried to the next maturation round, derived from
|
|
3625
|
+
// revision-proposal.yaml and GATED on a valid revision-proposal-validation. Shared by the
|
|
3626
|
+
// continuation builder and validator so the two derive the identical set from the same
|
|
3627
|
+
// validated authority (no asymmetry → no spurious conservation mismatch on the invalid path).
|
|
3628
|
+
function revisionBlockerLimitationRefs(revisionProposal, revisionProposalValidation) {
|
|
3629
|
+
if (revisionProposalValidation.validation_status !== "valid")
|
|
3630
|
+
return [];
|
|
3631
|
+
return revisionProposal.proposals
|
|
3632
|
+
.filter(isRevisionBlocker)
|
|
3633
|
+
.map((proposal) => `revision-blocker:${proposal.proposal_id}`);
|
|
3634
|
+
}
|
|
2722
3635
|
export function buildMaturationContinuationDecisionArtifact(args) {
|
|
2723
3636
|
const materialRows = args.actionabilityMatrix.rows.filter((row) => row.materiality === "blocker" || row.materiality === "high");
|
|
2724
3637
|
const frontierRows = materialRows.filter((row) => row.member_readiness === "frontier_required");
|
|
2725
3638
|
const limitationRows = materialRows.filter((row) => row.member_readiness === "limitation_backed");
|
|
3639
|
+
// Maturation value-read cut (design §13.2). value_resolved rows had their value-dependent
|
|
3640
|
+
// limitations discharged to zero residual — they are a non-blocking anchor: like a closed
|
|
3641
|
+
// row they can support a bounded actionable claim, so the two `closedRows === 0` blocked
|
|
3642
|
+
// arms below also gate on `valueResolvedRows.length === 0`, and a value_resolved-only run
|
|
3643
|
+
// routes to actionable_limited (not actionable_ready — these rows are value-grounded, not
|
|
3644
|
+
// L4-validated). deriveMemberReadiness gates value_resolved on `material`, so every
|
|
3645
|
+
// value_resolved row is material → this equals the all-rows value_resolved set.
|
|
3646
|
+
const valueResolvedRows = materialRows.filter((row) => row.member_readiness === "value_resolved");
|
|
2726
3647
|
const blockingRowRefs = frontierRows.map((row) => row.matrix_row_id);
|
|
2727
3648
|
const authorityRequestRefs = args.maturationClosureFrontier.authority_requests.map((request) => request.authority_request_id);
|
|
2728
3649
|
const unresolvedAuthorityResponses = args.maturationAuthorityResponse.responses.filter((response) => response.response_status !== "provided");
|
|
2729
3650
|
const finalRequestionStatus = args.maturationConvergenceLedgerValidation.final_requestion_pass_status;
|
|
2730
3651
|
const closedRows = args.actionabilityMatrix.rows.filter((row) => row.member_readiness === "closed");
|
|
3652
|
+
// Purpose-candidate-level limitations bound the overall claim but do not gate any
|
|
3653
|
+
// single row's frontier; when present they keep the claim at actionable_limited
|
|
3654
|
+
// even if every row is closed (the source itself is acknowledged as partial).
|
|
3655
|
+
const candidateLimitationRefs = args.actionabilityMatrix.candidate_limitation_refs;
|
|
3656
|
+
const hasCandidateLimitations = candidateLimitationRefs.length > 0;
|
|
3657
|
+
// An unproven final re-question convergence is its own limitation on the claim,
|
|
3658
|
+
// independent of candidate or row limitations — so it must be recorded whenever it
|
|
3659
|
+
// holds, not only when it is the sole reason for actionable_limited. Otherwise the
|
|
3660
|
+
// candidate-limitation branch below would preempt it and the public claim (which
|
|
3661
|
+
// projects only decision.limitation_refs) would silently drop it.
|
|
3662
|
+
const convergenceUnproven = finalRequestionStatus !== "no_new_material_question";
|
|
3663
|
+
// M4b: unresolved reject/defer proposals are carried-forward scope that must keep the
|
|
3664
|
+
// continuation below actionable_ready, independent of row readiness. Computed once here;
|
|
3665
|
+
// the field/fold below is unconditional (a higher-priority branch can win while blockers
|
|
3666
|
+
// exist), only the decision_state downgrade is branch-gated.
|
|
3667
|
+
const revisionBlockerRefs = revisionBlockerLimitationRefs(args.revisionProposal, args.revisionProposalValidation);
|
|
2731
3668
|
let decisionState;
|
|
2732
3669
|
let rationale;
|
|
2733
3670
|
const convergenceLimitationRefs = [];
|
|
@@ -2739,23 +3676,52 @@ export function buildMaturationContinuationDecisionArtifact(args) {
|
|
|
2739
3676
|
decisionState = "blocked";
|
|
2740
3677
|
rationale = "Material rows remain frontier-required, but no validated next source or authority response can advance them.";
|
|
2741
3678
|
}
|
|
2742
|
-
else if (limitationRows.length > 0 &&
|
|
3679
|
+
else if (limitationRows.length > 0 &&
|
|
3680
|
+
closedRows.length === 0 &&
|
|
3681
|
+
valueResolvedRows.length === 0) {
|
|
2743
3682
|
decisionState = "blocked";
|
|
2744
|
-
rationale = "Material rows remain limitation-backed and no closed row can support a bounded actionable claim.";
|
|
3683
|
+
rationale = "Material rows remain limitation-backed and no closed or value-resolved row can support a bounded actionable claim.";
|
|
2745
3684
|
}
|
|
2746
3685
|
else if (limitationRows.length > 0) {
|
|
2747
3686
|
decisionState = "actionable_limited";
|
|
2748
3687
|
rationale = "No material frontier remains, but named limitations constrain the actionability claim.";
|
|
2749
3688
|
}
|
|
2750
|
-
else if (
|
|
3689
|
+
else if (revisionBlockerRefs.length > 0 &&
|
|
3690
|
+
closedRows.length === 0 &&
|
|
3691
|
+
valueResolvedRows.length === 0) {
|
|
3692
|
+
// Unresolved revision blockers with no closed or value-resolved row cannot support a
|
|
3693
|
+
// bounded actionable claim, and actionable_limited with zero included rows is itself
|
|
3694
|
+
// invalid → blocked.
|
|
3695
|
+
decisionState = "blocked";
|
|
3696
|
+
rationale = "Unresolved reject/defer revision proposals remain and no closed or value-resolved row can support a bounded actionable claim.";
|
|
3697
|
+
}
|
|
3698
|
+
else if (revisionBlockerRefs.length > 0) {
|
|
3699
|
+
decisionState = "actionable_limited";
|
|
3700
|
+
rationale = "All material rows are closed or value-resolved, but unresolved reject/defer revision proposals carry scope to the next maturation round and constrain the actionability claim.";
|
|
3701
|
+
}
|
|
3702
|
+
else if (hasCandidateLimitations) {
|
|
3703
|
+
decisionState = "actionable_limited";
|
|
3704
|
+
rationale = "All material rows are closed or value-resolved, but purpose-candidate-level limitations constrain the actionability claim and signal next-round source frontier.";
|
|
3705
|
+
}
|
|
3706
|
+
else if (convergenceUnproven) {
|
|
2751
3707
|
decisionState = "actionable_limited";
|
|
2752
|
-
convergenceLimitationRefs.push(`maturation-final-requestion:${finalRequestionStatus}`);
|
|
2753
3708
|
rationale = "No material frontier remains, but final re-question convergence has not proven actionable readiness.";
|
|
2754
3709
|
}
|
|
3710
|
+
else if (valueResolvedRows.length > 0) {
|
|
3711
|
+
// Value-read discharge cleared the value-dependent limitations on these material rows,
|
|
3712
|
+
// but they are value-grounded (not L4-validated) — a bounded actionable claim, not full
|
|
3713
|
+
// readiness. Reached only when no blocker/limitation/candidate/convergence constraint
|
|
3714
|
+
// already routed to actionable_limited above (design §13.2 branch 8.5).
|
|
3715
|
+
decisionState = "actionable_limited";
|
|
3716
|
+
rationale = "No material frontier remains; value-read discharge resolved the value-dependent limitations, supporting a bounded actionable claim on a value-grounded (not L4-validated) basis.";
|
|
3717
|
+
}
|
|
2755
3718
|
else {
|
|
2756
3719
|
decisionState = "actionable_ready";
|
|
2757
3720
|
rationale = "All material rows are closed for the declared purpose.";
|
|
2758
3721
|
}
|
|
3722
|
+
if (convergenceUnproven) {
|
|
3723
|
+
convergenceLimitationRefs.push(`maturation-final-requestion:${finalRequestionStatus}`);
|
|
3724
|
+
}
|
|
2759
3725
|
const nextFrontierRefs = [
|
|
2760
3726
|
...args.maturationQuestionFrontier.questions
|
|
2761
3727
|
.filter((question) => question.current_answer_status !== "answerable" &&
|
|
@@ -2777,19 +3743,38 @@ export function buildMaturationContinuationDecisionArtifact(args) {
|
|
|
2777
3743
|
authority_request_refs: authorityRequestRefs,
|
|
2778
3744
|
authority_response_refs: args.maturationAuthorityResponse.responses.map((response) => response.authority_response_id),
|
|
2779
3745
|
claim_scope: {
|
|
2780
|
-
|
|
3746
|
+
// Value-read cut (design §13.4 F6③): the claimable set is closed ∪ value_resolved —
|
|
3747
|
+
// a value_resolved row is value-grounded but claimable. The continuation validator and
|
|
3748
|
+
// the actionable-ontology validator mirror this exact partition (disjoint by
|
|
3749
|
+
// construction: included ∩ excluded = ∅).
|
|
3750
|
+
included_row_refs: args.actionabilityMatrix.rows
|
|
3751
|
+
.filter((row) => row.member_readiness === "closed" ||
|
|
3752
|
+
row.member_readiness === "value_resolved")
|
|
3753
|
+
.map((row) => row.matrix_row_id),
|
|
2781
3754
|
excluded_row_refs: args.actionabilityMatrix.rows
|
|
2782
|
-
.filter((row) => row.member_readiness !== "closed"
|
|
3755
|
+
.filter((row) => row.member_readiness !== "closed" &&
|
|
3756
|
+
row.member_readiness !== "value_resolved")
|
|
2783
3757
|
.map((row) => row.matrix_row_id),
|
|
2784
3758
|
exclusion_rationale: limitationRows.length > 0 || frontierRows.length > 0
|
|
2785
3759
|
? "Rows outside the trusted claim remain limitation-backed or frontier-required."
|
|
2786
3760
|
: null,
|
|
2787
3761
|
},
|
|
3762
|
+
// M4b: unconditional — recorded regardless of which decision_state branch won, so the
|
|
3763
|
+
// validator's superset+conservation hold even when an earlier branch (ask_user/blocked)
|
|
3764
|
+
// is chosen while blockers exist (mirrors convergenceLimitationRefs).
|
|
3765
|
+
revision_blocker_limitation_refs: revisionBlockerRefs,
|
|
2788
3766
|
limitation_refs: [
|
|
2789
3767
|
...new Set([
|
|
2790
3768
|
...args.actionabilityMatrix.rows.flatMap((row) => row.limitation_refs),
|
|
3769
|
+
...candidateLimitationRefs,
|
|
2791
3770
|
...args.ontologyExpansionValidation.violations.map((item) => item.subject_id ?? "ontology_expansion_validation"),
|
|
2792
3771
|
...convergenceLimitationRefs,
|
|
3772
|
+
...revisionBlockerRefs,
|
|
3773
|
+
// Value-read cut (design §13.4 F6④): each value_resolved row contributes an explicit
|
|
3774
|
+
// value-read basis ref so the public claim honestly records that its actionability
|
|
3775
|
+
// rests on value-read discharge (not L4 validation), and a pure value_resolved-only
|
|
3776
|
+
// run satisfies the "actionable_limited needs excluded refs or limitation refs" gate.
|
|
3777
|
+
...valueResolvedRows.map((row) => `maturation-value-read-basis:${row.matrix_row_id}`),
|
|
2793
3778
|
]),
|
|
2794
3779
|
],
|
|
2795
3780
|
};
|
|
@@ -2797,6 +3782,24 @@ export function buildMaturationContinuationDecisionArtifact(args) {
|
|
|
2797
3782
|
export function validateMaturationContinuationDecision(args) {
|
|
2798
3783
|
const decision = args.maturationContinuationDecision;
|
|
2799
3784
|
const violations = [];
|
|
3785
|
+
// G(a) slice 22 — record the obligations this validator genuinely enforces (RECORD 4/9, all clean
|
|
3786
|
+
// derive-and-assert / single-facet gates from the M1/M4b conservation work). The other 5 stay parked
|
|
3787
|
+
// (see obligation-coverage-ledger.yaml notes): "continuation_state against validated matrix and
|
|
3788
|
+
// frontier_state" is AMBIGUOUS (intent split across the blanket prior-validation loop / claim_scope
|
|
3789
|
+
// partition / material-rows gate); "continue/ask_user/blocked against available next authority" is
|
|
3790
|
+
// PARTIAL (the named `blocked` state has no enforcement); the revision-proposal binding is gated on the
|
|
3791
|
+
// caller-supplied optional `revisionProposalRef` (no internal guarantee — codex R1, slice-18); the
|
|
3792
|
+
// "material blocker/high row remains unclosed" gate only catches `frontier_required`, not the
|
|
3793
|
+
// `limitation_backed` half of "unclosed" (codex R1, subset-of-scope); and "limitation_refs and
|
|
3794
|
+
// row_scope for actionable_limited" only firmly enforces row_scope — the limitation-ref facet is a weak
|
|
3795
|
+
// excluded-OR-limitation presence check that a limitation_backed excluded row with dropped
|
|
3796
|
+
// limitation_refs satisfies (codex R2, name-broader-than-code). Stamped before any per-row/per-state
|
|
3797
|
+
// guard so the recorder fires on zero-row input.
|
|
3798
|
+
const assertedObligationIds = [];
|
|
3799
|
+
assertObligation(assertedObligationIds, "reject_actionable_ready_until_final_requestion_convergence_is_proven");
|
|
3800
|
+
assertObligation(assertedObligationIds, "reject_actionable_ready_when_unresolved_revision_blockers_remain");
|
|
3801
|
+
assertObligation(assertedObligationIds, "require_revision_blocker_refs_in_continuation_limitation_refs");
|
|
3802
|
+
assertObligation(assertedObligationIds, "validate_revision_blocker_limitation_refs_against_validated_revision_proposal");
|
|
2800
3803
|
const matrixRows = new Map(args.actionabilityMatrix.rows.map((row) => [
|
|
2801
3804
|
row.matrix_row_id,
|
|
2802
3805
|
row,
|
|
@@ -2827,6 +3830,7 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2827
3830
|
maturation_authority_response: args.maturationAuthorityResponseValidation.validation_status,
|
|
2828
3831
|
ontology_expansion: args.ontologyExpansionValidation.validation_status,
|
|
2829
3832
|
maturation_convergence_ledger: args.maturationConvergenceLedgerValidation.validation_status,
|
|
3833
|
+
revision_proposal: args.revisionProposalValidation.validation_status,
|
|
2830
3834
|
})) {
|
|
2831
3835
|
if (status !== "valid") {
|
|
2832
3836
|
violations.push(violation({
|
|
@@ -2856,6 +3860,33 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2856
3860
|
}));
|
|
2857
3861
|
}
|
|
2858
3862
|
}
|
|
3863
|
+
// M1 partition conservation: recompute the claim_scope partition from the matrix and
|
|
3864
|
+
// require the decision to match it. The resolve check above only verifies the listed refs
|
|
3865
|
+
// exist, so a claim_scope that OMITS rows would pass and the public claim would preserve
|
|
3866
|
+
// the omission. Value-read cut (design §13.4 F6③): the claimable (included) set is
|
|
3867
|
+
// closed ∪ value_resolved; everything else is excluded (mirrors the builder exactly).
|
|
3868
|
+
const expectedIncluded = args.actionabilityMatrix.rows
|
|
3869
|
+
.filter((row) => row.member_readiness === "closed" ||
|
|
3870
|
+
row.member_readiness === "value_resolved")
|
|
3871
|
+
.map((row) => row.matrix_row_id);
|
|
3872
|
+
const expectedExcluded = args.actionabilityMatrix.rows
|
|
3873
|
+
.filter((row) => row.member_readiness !== "closed" &&
|
|
3874
|
+
row.member_readiness !== "value_resolved")
|
|
3875
|
+
.map((row) => row.matrix_row_id);
|
|
3876
|
+
if (!sameRefSet(decision.claim_scope.included_row_refs, expectedIncluded)) {
|
|
3877
|
+
violations.push(violation({
|
|
3878
|
+
code: "conflicting_state",
|
|
3879
|
+
message: "continuation claim_scope included_row_refs must equal the matrix's closed and value-resolved rows",
|
|
3880
|
+
subjectId: "claim_scope.included_row_refs",
|
|
3881
|
+
}));
|
|
3882
|
+
}
|
|
3883
|
+
if (!sameRefSet(decision.claim_scope.excluded_row_refs, expectedExcluded)) {
|
|
3884
|
+
violations.push(violation({
|
|
3885
|
+
code: "conflicting_state",
|
|
3886
|
+
message: "continuation claim_scope excluded_row_refs must equal the matrix's non-closed, non-value-resolved rows",
|
|
3887
|
+
subjectId: "claim_scope.excluded_row_refs",
|
|
3888
|
+
}));
|
|
3889
|
+
}
|
|
2859
3890
|
const hasAuthorityNeed = decision.authority_request_refs.length > 0;
|
|
2860
3891
|
if (decision.decision_state === "actionable_ready" && materialOpenRows.length > 0) {
|
|
2861
3892
|
violations.push(violation({
|
|
@@ -2864,6 +3895,85 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2864
3895
|
subjectId: "actionable_ready",
|
|
2865
3896
|
}));
|
|
2866
3897
|
}
|
|
3898
|
+
// Value-read cut (design §13.4): value_resolved rows are value-grounded, not L4-validated,
|
|
3899
|
+
// so a saved/edited decision cannot project actionable_ready while any remain — the bounded
|
|
3900
|
+
// claim is at most actionable_limited (mirrors the builder's branch ordering).
|
|
3901
|
+
if (decision.decision_state === "actionable_ready" &&
|
|
3902
|
+
args.actionabilityMatrix.rows.some((row) => row.member_readiness === "value_resolved")) {
|
|
3903
|
+
violations.push(violation({
|
|
3904
|
+
code: "conflicting_state",
|
|
3905
|
+
message: "actionable_ready cannot be projected while value-resolved (value-grounded, not L4) rows remain",
|
|
3906
|
+
subjectId: "actionable_ready",
|
|
3907
|
+
}));
|
|
3908
|
+
}
|
|
3909
|
+
// Mirror the builder: purpose-candidate-level limitations constrain the overall
|
|
3910
|
+
// claim, so a saved/edited decision cannot project actionable_ready while the
|
|
3911
|
+
// matrix carries them (the bounded claim is at most actionable_limited).
|
|
3912
|
+
if (decision.decision_state === "actionable_ready" &&
|
|
3913
|
+
args.actionabilityMatrix.candidate_limitation_refs.length > 0) {
|
|
3914
|
+
violations.push(violation({
|
|
3915
|
+
code: "conflicting_state",
|
|
3916
|
+
message: "actionable_ready cannot be projected while purpose-candidate-level limitations remain",
|
|
3917
|
+
subjectId: "actionable_ready",
|
|
3918
|
+
}));
|
|
3919
|
+
}
|
|
3920
|
+
// The public claim projects decision.limitation_refs, so every matrix candidate
|
|
3921
|
+
// limitation must survive into it — a saved/edited limited decision that drops them
|
|
3922
|
+
// (keeping only excluded rows or other limitation refs) would erase the source-level
|
|
3923
|
+
// limitation from the downstream claim even though the validated matrix still carries it.
|
|
3924
|
+
const decisionLimitationRefs = new Set(decision.limitation_refs);
|
|
3925
|
+
if (args.actionabilityMatrix.candidate_limitation_refs.some((ref) => !decisionLimitationRefs.has(ref))) {
|
|
3926
|
+
violations.push(violation({
|
|
3927
|
+
code: "missing_required_ref",
|
|
3928
|
+
message: "continuation decision limitation_refs must include every matrix candidate limitation",
|
|
3929
|
+
subjectId: "candidate_limitation_refs",
|
|
3930
|
+
}));
|
|
3931
|
+
}
|
|
3932
|
+
// M4b — revision-blocker conservation gate (derive-and-assert, not trust the field).
|
|
3933
|
+
// onto finding-002 + codex: bind the consumed revision-proposal-validation to the consumed
|
|
3934
|
+
// revision-proposal so a stale/mismatched/unbound valid validation cannot certify a
|
|
3935
|
+
// different proposal set (resume/manual substitution). When the consumed proposal ref is
|
|
3936
|
+
// known and the validation is being trusted (valid), require a matching NON-NULL ref —
|
|
3937
|
+
// a null revision_proposal_ref (pure/manual validations) is not acceptable certification.
|
|
3938
|
+
if (args.revisionProposalRef &&
|
|
3939
|
+
args.revisionProposalValidation.validation_status === "valid" &&
|
|
3940
|
+
(!args.revisionProposalValidation.revision_proposal_ref ||
|
|
3941
|
+
path.resolve(args.revisionProposalValidation.revision_proposal_ref) !==
|
|
3942
|
+
path.resolve(args.revisionProposalRef))) {
|
|
3943
|
+
violations.push(violation({
|
|
3944
|
+
code: "conflicting_state",
|
|
3945
|
+
message: "continuation decision must consume the revision-proposal validation that certifies the consumed revision-proposal",
|
|
3946
|
+
subjectId: "revision_proposal_ref",
|
|
3947
|
+
}));
|
|
3948
|
+
}
|
|
3949
|
+
// Recompute the blocker set from the validated authority (gated on valid, symmetric with
|
|
3950
|
+
// the builder) and assert the decision field equals it — the field is not trusted.
|
|
3951
|
+
// Normalize a missing field ([] for pre-M4b persisted decisions) so revalidation produces
|
|
3952
|
+
// an invalid result rather than throwing.
|
|
3953
|
+
const decisionRevisionBlockerRefs = decision.revision_blocker_limitation_refs ?? [];
|
|
3954
|
+
const expectedRevisionBlockerRefs = revisionBlockerLimitationRefs(args.revisionProposal, args.revisionProposalValidation);
|
|
3955
|
+
if (!sameRefSet(decisionRevisionBlockerRefs, expectedRevisionBlockerRefs)) {
|
|
3956
|
+
violations.push(violation({
|
|
3957
|
+
code: "conflicting_state",
|
|
3958
|
+
message: "continuation decision revision_blocker_limitation_refs must equal the reject/defer proposals derived from the validated revision-proposal",
|
|
3959
|
+
subjectId: "revision_blocker_limitation_refs",
|
|
3960
|
+
}));
|
|
3961
|
+
}
|
|
3962
|
+
if (expectedRevisionBlockerRefs.some((ref) => !decisionLimitationRefs.has(ref))) {
|
|
3963
|
+
violations.push(violation({
|
|
3964
|
+
code: "missing_required_ref",
|
|
3965
|
+
message: "continuation decision limitation_refs must include every revision blocker ref",
|
|
3966
|
+
subjectId: "revision_blocker_limitation_refs",
|
|
3967
|
+
}));
|
|
3968
|
+
}
|
|
3969
|
+
if (decision.decision_state === "actionable_ready" &&
|
|
3970
|
+
expectedRevisionBlockerRefs.length > 0) {
|
|
3971
|
+
violations.push(violation({
|
|
3972
|
+
code: "conflicting_state",
|
|
3973
|
+
message: "actionable_ready cannot be projected while unresolved revision blockers remain",
|
|
3974
|
+
subjectId: "actionable_ready",
|
|
3975
|
+
}));
|
|
3976
|
+
}
|
|
2867
3977
|
if (decision.decision_state === "actionable_ready" &&
|
|
2868
3978
|
args.maturationConvergenceLedgerValidation.final_requestion_pass_status !==
|
|
2869
3979
|
"no_new_material_question") {
|
|
@@ -2924,6 +4034,7 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2924
4034
|
validation_results: violations.length === 0
|
|
2925
4035
|
? ["maturation_continuation_decision_valid"]
|
|
2926
4036
|
: ["maturation_continuation_decision_invalid"],
|
|
4037
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
2927
4038
|
violations,
|
|
2928
4039
|
};
|
|
2929
4040
|
}
|
|
@@ -3022,6 +4133,17 @@ export function validateActionableOntology(args) {
|
|
|
3022
4133
|
const expansionIds = new Set(args.ontologyExpansion.expansions.map((expansion) => expansion.expansion_id));
|
|
3023
4134
|
const projectedMatrixRefs = new Set();
|
|
3024
4135
|
const projectionIds = new Set();
|
|
4136
|
+
// G(a) obligation recorder (INV-OBLIGATION-COVERAGE-1). The four obligations below have a distinct,
|
|
4137
|
+
// name-matching enforcer reached unconditionally (the per-row trace check fires per projected row, so
|
|
4138
|
+
// its stamp also records on zero-row input). The other three convergence/projection obligations are
|
|
4139
|
+
// parked with audit notes — see obligation-coverage-ledger.yaml (material blocker/high closure is
|
|
4140
|
+
// delegated to the continuation-decision validator; per-surface static/kinetic/dynamic closure is not
|
|
4141
|
+
// checked here; proof-authority recheck is a blanket downstream-claim reject, not a validation).
|
|
4142
|
+
const assertedObligationIds = [];
|
|
4143
|
+
assertObligation(assertedObligationIds, "reject_actionable_ready_until_final_requestion_convergence_is_proven");
|
|
4144
|
+
assertObligation(assertedObligationIds, "validate_actionability_claim_against_maturation_continuation_decision");
|
|
4145
|
+
assertObligation(assertedObligationIds, "validate_actionable_limited_claim_scope_rows");
|
|
4146
|
+
assertObligation(assertedObligationIds, "require_every_projected_row_to_trace_to_seed_expansion_or_limitation");
|
|
3025
4147
|
if (artifact.session_id !== args.actionabilityMatrix.session_id) {
|
|
3026
4148
|
violations.push(violation({
|
|
3027
4149
|
code: "session_id_mismatch",
|
|
@@ -3127,10 +4249,12 @@ export function validateActionableOntology(args) {
|
|
|
3127
4249
|
subjectId: row.projection_row_id,
|
|
3128
4250
|
}));
|
|
3129
4251
|
}
|
|
3130
|
-
if (row.claim_scope === "included" &&
|
|
4252
|
+
if (row.claim_scope === "included" &&
|
|
4253
|
+
matrixRow.member_readiness !== "closed" &&
|
|
4254
|
+
matrixRow.member_readiness !== "value_resolved") {
|
|
3131
4255
|
violations.push(violation({
|
|
3132
4256
|
code: "conflicting_state",
|
|
3133
|
-
message: "included actionable ontology rows must be closed in the actionability matrix",
|
|
4257
|
+
message: "included actionable ontology rows must be closed or value-resolved in the actionability matrix",
|
|
3134
4258
|
subjectId: row.matrix_row_ref,
|
|
3135
4259
|
}));
|
|
3136
4260
|
}
|
|
@@ -3180,6 +4304,7 @@ export function validateActionableOntology(args) {
|
|
|
3180
4304
|
validation_results: violations.length === 0
|
|
3181
4305
|
? ["actionable_ontology_valid"]
|
|
3182
4306
|
: ["actionable_ontology_invalid"],
|
|
4307
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
3183
4308
|
violations,
|
|
3184
4309
|
};
|
|
3185
4310
|
}
|
|
@@ -3307,7 +4432,7 @@ export async function writeMaturationBaselineValidationArtifact(args) {
|
|
|
3307
4432
|
}
|
|
3308
4433
|
export async function writeActionabilityMatrixArtifact(args) {
|
|
3309
4434
|
const maturationBaseline = await readYamlDocument(args.maturationBaselinePath);
|
|
3310
|
-
const [maturationAnswerClaims, maturationAnswerClaimsValidation, ontologyExpansion, ontologyExpansionValidation,] = await Promise.all([
|
|
4435
|
+
const [maturationAnswerClaims, maturationAnswerClaimsValidation, ontologyExpansion, ontologyExpansionValidation, maturationQuestionFrontier, maturationQuestionFrontierValidation, maturationValueDischarge, maturationValueDischargeValidation,] = await Promise.all([
|
|
3311
4436
|
args.maturationAnswerClaimsPath
|
|
3312
4437
|
? readYamlDocument(args.maturationAnswerClaimsPath)
|
|
3313
4438
|
: Promise.resolve(null),
|
|
@@ -3320,6 +4445,18 @@ export async function writeActionabilityMatrixArtifact(args) {
|
|
|
3320
4445
|
args.ontologyExpansionValidationPath
|
|
3321
4446
|
? readYamlDocument(args.ontologyExpansionValidationPath)
|
|
3322
4447
|
: Promise.resolve(null),
|
|
4448
|
+
args.maturationQuestionFrontierPath
|
|
4449
|
+
? readYamlDocument(args.maturationQuestionFrontierPath)
|
|
4450
|
+
: Promise.resolve(null),
|
|
4451
|
+
args.maturationQuestionFrontierValidationPath
|
|
4452
|
+
? readYamlDocument(args.maturationQuestionFrontierValidationPath)
|
|
4453
|
+
: Promise.resolve(null),
|
|
4454
|
+
args.maturationValueDischargePath
|
|
4455
|
+
? readYamlDocument(args.maturationValueDischargePath)
|
|
4456
|
+
: Promise.resolve(null),
|
|
4457
|
+
args.maturationValueDischargeValidationPath
|
|
4458
|
+
? readYamlDocument(args.maturationValueDischargeValidationPath)
|
|
4459
|
+
: Promise.resolve(null),
|
|
3323
4460
|
]);
|
|
3324
4461
|
const artifact = buildActionabilityMatrixArtifact({
|
|
3325
4462
|
sessionId: args.sessionId,
|
|
@@ -3329,15 +4466,19 @@ export async function writeActionabilityMatrixArtifact(args) {
|
|
|
3329
4466
|
maturationAnswerClaims,
|
|
3330
4467
|
maturationAnswerClaimsValidation,
|
|
3331
4468
|
maturationAnswerClaimsValidationRef: args.maturationAnswerClaimsValidationPath ?? null,
|
|
4469
|
+
maturationValueDischarge,
|
|
4470
|
+
maturationValueDischargeValidation,
|
|
3332
4471
|
ontologyExpansion,
|
|
3333
4472
|
ontologyExpansionValidation,
|
|
3334
4473
|
ontologyExpansionValidationRef: args.ontologyExpansionValidationPath ?? null,
|
|
4474
|
+
maturationQuestionFrontier,
|
|
4475
|
+
maturationQuestionFrontierValidation,
|
|
3335
4476
|
});
|
|
3336
4477
|
await writeYamlDocument(args.outputPath, artifact);
|
|
3337
4478
|
return artifact;
|
|
3338
4479
|
}
|
|
3339
4480
|
export async function writeActionabilityMatrixValidationArtifact(args) {
|
|
3340
|
-
const [actionabilityMatrix, maturationBaseline, maturationBaselineValidation, maturationAnswerClaims, maturationAnswerClaimsValidation, ontologyExpansion, ontologyExpansionValidation,] = await Promise.all([
|
|
4481
|
+
const [actionabilityMatrix, maturationBaseline, maturationBaselineValidation, maturationAnswerClaims, maturationAnswerClaimsValidation, ontologyExpansion, ontologyExpansionValidation, maturationQuestionFrontier, maturationQuestionFrontierValidation, maturationValueDischarge, maturationValueDischargeValidation,] = await Promise.all([
|
|
3341
4482
|
readYamlDocument(args.actionabilityMatrixPath),
|
|
3342
4483
|
readYamlDocument(args.maturationBaselinePath),
|
|
3343
4484
|
readYamlDocument(args.maturationBaselineValidationPath),
|
|
@@ -3353,6 +4494,18 @@ export async function writeActionabilityMatrixValidationArtifact(args) {
|
|
|
3353
4494
|
args.ontologyExpansionValidationPath
|
|
3354
4495
|
? readYamlDocument(args.ontologyExpansionValidationPath)
|
|
3355
4496
|
: Promise.resolve(null),
|
|
4497
|
+
args.maturationQuestionFrontierPath
|
|
4498
|
+
? readYamlDocument(args.maturationQuestionFrontierPath)
|
|
4499
|
+
: Promise.resolve(null),
|
|
4500
|
+
args.maturationQuestionFrontierValidationPath
|
|
4501
|
+
? readYamlDocument(args.maturationQuestionFrontierValidationPath)
|
|
4502
|
+
: Promise.resolve(null),
|
|
4503
|
+
args.maturationValueDischargePath
|
|
4504
|
+
? readYamlDocument(args.maturationValueDischargePath)
|
|
4505
|
+
: Promise.resolve(null),
|
|
4506
|
+
args.maturationValueDischargeValidationPath
|
|
4507
|
+
? readYamlDocument(args.maturationValueDischargeValidationPath)
|
|
4508
|
+
: Promise.resolve(null),
|
|
3356
4509
|
]);
|
|
3357
4510
|
const validation = validateActionabilityMatrix({
|
|
3358
4511
|
actionabilityMatrix,
|
|
@@ -3363,9 +4516,15 @@ export async function writeActionabilityMatrixValidationArtifact(args) {
|
|
|
3363
4516
|
maturationAnswerClaims,
|
|
3364
4517
|
maturationAnswerClaimsValidation,
|
|
3365
4518
|
maturationAnswerClaimsValidationRef: args.maturationAnswerClaimsValidationPath ?? null,
|
|
4519
|
+
maturationValueDischarge,
|
|
4520
|
+
maturationValueDischargeValidation,
|
|
3366
4521
|
ontologyExpansion,
|
|
3367
4522
|
ontologyExpansionValidation,
|
|
3368
4523
|
ontologyExpansionValidationRef: args.ontologyExpansionValidationPath ?? null,
|
|
4524
|
+
maturationQuestionFrontier,
|
|
4525
|
+
maturationQuestionFrontierValidation,
|
|
4526
|
+
maturationQuestionFrontierValidationRef: args.maturationQuestionFrontierValidationPath ?? null,
|
|
4527
|
+
maturationQuestionFrontierRef: args.maturationQuestionFrontierPath ?? null,
|
|
3369
4528
|
});
|
|
3370
4529
|
await writeYamlDocument(args.outputPath, validation);
|
|
3371
4530
|
return validation;
|
|
@@ -3550,6 +4709,15 @@ export function validateAnswerSupportJudgment(args) {
|
|
|
3550
4709
|
const judgedKeysByCluster = new Map();
|
|
3551
4710
|
const seen = new Set();
|
|
3552
4711
|
let supportedJudgmentCount = 0;
|
|
4712
|
+
// G(a) slice 8: record the four judgment obligations before the per-judgment loop (and the D
|
|
4713
|
+
// convergent-coverage loop) so they fire on a zero-judgment artifact. Each maps to a distinct
|
|
4714
|
+
// audited block — A ref resolution (unknown_id) / B supports enum (invalid_enum) / C rationale_ref
|
|
4715
|
+
// (missing_required_ref) / D convergent coverage (missing_required_coverage). No laundering.
|
|
4716
|
+
const assertedObligationIds = [];
|
|
4717
|
+
assertObligation(assertedObligationIds, "validate_judgment_refs_resolve_to_answer_support_ledger_clusters_and_evidence");
|
|
4718
|
+
assertObligation(assertedObligationIds, "require_supports_enum_for_each_judgment");
|
|
4719
|
+
assertObligation(assertedObligationIds, "require_rationale_ref_for_each_judgment");
|
|
4720
|
+
assertObligation(assertedObligationIds, "require_convergent_clusters_to_judge_every_cited_evidence_ref");
|
|
3553
4721
|
for (const judgment of artifact.judgments) {
|
|
3554
4722
|
if (seen.has(judgment.judgment_id)) {
|
|
3555
4723
|
violations.push(violation({
|
|
@@ -3650,6 +4818,7 @@ export function validateAnswerSupportJudgment(args) {
|
|
|
3650
4818
|
validation_results: violations.length === 0
|
|
3651
4819
|
? ["answer_support_judgment_valid"]
|
|
3652
4820
|
: ["answer_support_judgment_invalid"],
|
|
4821
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
3653
4822
|
violations,
|
|
3654
4823
|
};
|
|
3655
4824
|
}
|
|
@@ -3792,7 +4961,7 @@ export async function writeMaturationConvergenceLedgerValidationArtifact(args) {
|
|
|
3792
4961
|
return validation;
|
|
3793
4962
|
}
|
|
3794
4963
|
export async function writeMaturationContinuationDecisionArtifact(args) {
|
|
3795
|
-
const [actionabilityMatrix, maturationQuestionFrontier, maturationClosureFrontier, maturationClosureFrontierValidation, maturationAuthorityResponse, ontologyExpansionValidation, maturationConvergenceLedgerValidation,] = await Promise.all([
|
|
4964
|
+
const [actionabilityMatrix, maturationQuestionFrontier, maturationClosureFrontier, maturationClosureFrontierValidation, maturationAuthorityResponse, ontologyExpansionValidation, maturationConvergenceLedgerValidation, revisionProposal, revisionProposalValidation,] = await Promise.all([
|
|
3796
4965
|
readYamlDocument(args.actionabilityMatrixPath),
|
|
3797
4966
|
readYamlDocument(args.maturationQuestionFrontierPath),
|
|
3798
4967
|
readYamlDocument(args.maturationClosureFrontierPath),
|
|
@@ -3800,6 +4969,8 @@ export async function writeMaturationContinuationDecisionArtifact(args) {
|
|
|
3800
4969
|
readYamlDocument(args.maturationAuthorityResponsePath),
|
|
3801
4970
|
readYamlDocument(args.ontologyExpansionValidationPath),
|
|
3802
4971
|
readYamlDocument(args.maturationConvergenceLedgerValidationPath),
|
|
4972
|
+
readYamlDocument(args.revisionProposalPath),
|
|
4973
|
+
readYamlDocument(args.revisionProposalValidationPath),
|
|
3803
4974
|
]);
|
|
3804
4975
|
const artifact = buildMaturationContinuationDecisionArtifact({
|
|
3805
4976
|
sessionId: args.sessionId,
|
|
@@ -3812,12 +4983,14 @@ export async function writeMaturationContinuationDecisionArtifact(args) {
|
|
|
3812
4983
|
maturationClosureFrontierValidation,
|
|
3813
4984
|
maturationAuthorityResponse,
|
|
3814
4985
|
ontologyExpansionValidation,
|
|
4986
|
+
revisionProposal,
|
|
4987
|
+
revisionProposalValidation,
|
|
3815
4988
|
});
|
|
3816
4989
|
await writeYamlDocument(args.outputPath, artifact);
|
|
3817
4990
|
return artifact;
|
|
3818
4991
|
}
|
|
3819
4992
|
export async function writeMaturationContinuationDecisionValidationArtifact(args) {
|
|
3820
|
-
const [maturationContinuationDecision, actionabilityMatrix, actionabilityMatrixValidation, maturationQuestionFrontierValidation, maturationClosureFrontierValidation, answerSupportLedgerValidation, maturationAuthorityResponseValidation, ontologyExpansionValidation, maturationConvergenceLedgerValidation,] = await Promise.all([
|
|
4993
|
+
const [maturationContinuationDecision, actionabilityMatrix, actionabilityMatrixValidation, maturationQuestionFrontierValidation, maturationClosureFrontierValidation, answerSupportLedgerValidation, maturationAuthorityResponseValidation, ontologyExpansionValidation, maturationConvergenceLedgerValidation, revisionProposal, revisionProposalValidation,] = await Promise.all([
|
|
3821
4994
|
readYamlDocument(args.maturationContinuationDecisionPath),
|
|
3822
4995
|
readYamlDocument(args.actionabilityMatrixPath),
|
|
3823
4996
|
readYamlDocument(args.actionabilityMatrixValidationPath),
|
|
@@ -3827,6 +5000,8 @@ export async function writeMaturationContinuationDecisionValidationArtifact(args
|
|
|
3827
5000
|
readYamlDocument(args.maturationAuthorityResponseValidationPath),
|
|
3828
5001
|
readYamlDocument(args.ontologyExpansionValidationPath),
|
|
3829
5002
|
readYamlDocument(args.maturationConvergenceLedgerValidationPath),
|
|
5003
|
+
readYamlDocument(args.revisionProposalPath),
|
|
5004
|
+
readYamlDocument(args.revisionProposalValidationPath),
|
|
3830
5005
|
]);
|
|
3831
5006
|
const validation = validateMaturationContinuationDecision({
|
|
3832
5007
|
maturationContinuationDecision,
|
|
@@ -3846,6 +5021,9 @@ export async function writeMaturationContinuationDecisionValidationArtifact(args
|
|
|
3846
5021
|
ontologyExpansionValidationRef: args.ontologyExpansionValidationPath,
|
|
3847
5022
|
maturationConvergenceLedgerValidation,
|
|
3848
5023
|
maturationConvergenceLedgerValidationRef: args.maturationConvergenceLedgerValidationPath,
|
|
5024
|
+
revisionProposal,
|
|
5025
|
+
revisionProposalValidation,
|
|
5026
|
+
revisionProposalRef: args.revisionProposalPath,
|
|
3849
5027
|
});
|
|
3850
5028
|
await writeYamlDocument(args.outputPath, validation);
|
|
3851
5029
|
return validation;
|