onto-mcp 0.4.11 → 0.4.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.onto/authority/core-lexicon.yaml +1 -1
- package/.onto/authority/supported-models.yaml +153 -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 +202 -49
- 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/nesting-batch-worker-contract.md +1 -1
- 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 +679 -18
- 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 +131 -0
- package/dist/core-runtime/cli/claude-code-review-unit-executor.js +21 -9
- package/dist/core-runtime/cli/claude-nesting-batch-worker.js +4 -2
- 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 +289 -25
- 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 +326 -0
- package/dist/core-runtime/effort-calibration-reconstruct.js +47 -0
- package/dist/core-runtime/effort-calibration-review.js +22 -0
- package/dist/core-runtime/effort-calibration-sweep.js +110 -0
- package/dist/core-runtime/effort-frontier.js +134 -0
- package/dist/core-runtime/llm/claude-bin.js +77 -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 +316 -35
- 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 +37 -0
- package/dist/core-runtime/reconstruct/benchmark-evidence.js +16 -0
- package/dist/core-runtime/reconstruct/claim-projection-validation.js +19 -5
- 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/directive-validation.js +4 -3
- package/dist/core-runtime/reconstruct/dispatch-fallback-artifacts.js +640 -0
- package/dist/core-runtime/reconstruct/execution-telemetry.js +84 -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 +20 -5
- package/dist/core-runtime/reconstruct/material-profile-validation.js +24 -6
- package/dist/core-runtime/reconstruct/materialize-preparation.js +380 -23
- package/dist/core-runtime/reconstruct/maturation-validation.js +1522 -67
- package/dist/core-runtime/reconstruct/mock-llm-realization.js +120 -1
- package/dist/core-runtime/reconstruct/obligation-assertion.js +18 -0
- package/dist/core-runtime/reconstruct/ontology-seed-validation.js +39 -7
- package/dist/core-runtime/reconstruct/output-budget.js +6 -0
- package/dist/core-runtime/reconstruct/pipeline-execution-ledger.js +112 -5
- package/dist/core-runtime/reconstruct/post-seed-validation.js +122 -11
- package/dist/core-runtime/reconstruct/proof-authority-validation.js +2 -6
- package/dist/core-runtime/reconstruct/purpose-authority-validation.js +119 -7
- package/dist/core-runtime/reconstruct/record.js +49 -3
- package/dist/core-runtime/reconstruct/registry-verification-validation.js +41 -5
- package/dist/core-runtime/reconstruct/run-control-validation.js +1019 -37
- package/dist/core-runtime/reconstruct/run.js +5409 -517
- package/dist/core-runtime/reconstruct/seed-authoring-readiness-validation.js +56 -8
- package/dist/core-runtime/reconstruct/semantic-quality-gate.js +112 -7
- package/dist/core-runtime/reconstruct/source-observation-delta-validation.js +53 -5
- package/dist/core-runtime/reconstruct/source-observations.js +166 -0
- package/dist/core-runtime/reconstruct/source-safety-validation.js +77 -188
- package/dist/core-runtime/reconstruct/source-scout-pack-validation.js +50 -6
- package/dist/core-runtime/reconstruct/terminal-validation.js +145 -13
- 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 +219 -7
- 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 +27 -5
- package/dist/mcp/tool-schemas.js +8 -0
- 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 +20 -3
- package/settings.example.json +77 -16
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
import { parse as parseYaml
|
|
4
|
+
import { parse as parseYaml } from "yaml";
|
|
5
|
+
import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
|
|
5
6
|
import { sourceSafetyRowIdForObservation } from "./source-safety-validation.js";
|
|
6
7
|
import { materialAdmissionIdForPurposeElement } from "./material-admission-validation.js";
|
|
8
|
+
import { isRevisionBlocker } from "./post-seed-validation.js";
|
|
9
|
+
import { assertObligation } from "./obligation-assertion.js";
|
|
7
10
|
const MATERIALITY_VALUES = [
|
|
8
11
|
"blocker",
|
|
9
12
|
"high",
|
|
@@ -52,10 +55,6 @@ function isoNow() {
|
|
|
52
55
|
async function readYamlDocument(filePath) {
|
|
53
56
|
return parseYaml(await fs.readFile(filePath, "utf8"));
|
|
54
57
|
}
|
|
55
|
-
async function writeYamlDocument(filePath, value) {
|
|
56
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
57
|
-
await fs.writeFile(filePath, stringifyYaml(value), "utf8");
|
|
58
|
-
}
|
|
59
58
|
async function sha256FileIfPresent(filePath) {
|
|
60
59
|
if (!filePath)
|
|
61
60
|
return null;
|
|
@@ -135,6 +134,59 @@ function duplicateIds(values) {
|
|
|
135
134
|
function sameResolvedRef(left, right) {
|
|
136
135
|
return path.resolve(left) === path.resolve(right);
|
|
137
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Index actionability matrix rows by the dimensions used to compute a delta's
|
|
139
|
+
* affected_matrix_row_refs, so the build/validate intersection is O(rows + refs)
|
|
140
|
+
* per delta instead of a per-delta linear scan over the whole matrix.
|
|
141
|
+
*
|
|
142
|
+
* Row indices (not matrix_row_ids) are stored so that the downstream projection
|
|
143
|
+
* reproduces the exact multiplicity and ordering of
|
|
144
|
+
* `matrixRows.filter(...).map((row) => row.matrix_row_id)`.
|
|
145
|
+
*/
|
|
146
|
+
function indexActionabilityRowsByDelta(matrixRows) {
|
|
147
|
+
const bySourceRef = new Map();
|
|
148
|
+
const byObservationId = new Map();
|
|
149
|
+
const add = (map, key, index) => {
|
|
150
|
+
const bucket = map.get(key);
|
|
151
|
+
if (bucket)
|
|
152
|
+
bucket.add(index);
|
|
153
|
+
else
|
|
154
|
+
map.set(key, new Set([index]));
|
|
155
|
+
};
|
|
156
|
+
matrixRows.forEach((row, index) => {
|
|
157
|
+
for (const ref of row.member_source_refs) {
|
|
158
|
+
add(bySourceRef, path.resolve(ref), index);
|
|
159
|
+
}
|
|
160
|
+
for (const ref of row.cross_material_ref_refs) {
|
|
161
|
+
add(bySourceRef, path.resolve(ref), index);
|
|
162
|
+
}
|
|
163
|
+
for (const ref of row.supporting_refs) {
|
|
164
|
+
add(byObservationId, ref, index);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
return { bySourceRef, byObservationId };
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Resolve the sorted matrix_row_ids affected by a delta row, equivalent to
|
|
171
|
+
* `matrixRows.filter((row) => member/cross resolves to source_ref OR
|
|
172
|
+
* supporting_refs includes observation_id).map((row) => row.matrix_row_id).sort()`.
|
|
173
|
+
*/
|
|
174
|
+
function affectedMatrixRowRefsForDelta(matrixRows, index, deltaRow) {
|
|
175
|
+
const matched = new Set();
|
|
176
|
+
for (const rowIndex of index.bySourceRef.get(path.resolve(deltaRow.source_ref)) ??
|
|
177
|
+
[]) {
|
|
178
|
+
matched.add(rowIndex);
|
|
179
|
+
}
|
|
180
|
+
for (const rowIndex of index.byObservationId.get(deltaRow.observation_id) ?? []) {
|
|
181
|
+
matched.add(rowIndex);
|
|
182
|
+
}
|
|
183
|
+
const refs = [];
|
|
184
|
+
matrixRows.forEach((row, rowIndex) => {
|
|
185
|
+
if (matched.has(rowIndex))
|
|
186
|
+
refs.push(row.matrix_row_id);
|
|
187
|
+
});
|
|
188
|
+
return refs.sort();
|
|
189
|
+
}
|
|
138
190
|
function violation(args) {
|
|
139
191
|
return {
|
|
140
192
|
code: args.code,
|
|
@@ -205,12 +257,83 @@ function matrixRowNeedsFrontier(row) {
|
|
|
205
257
|
row.maturity_level !== "L4_validated_for_purpose" &&
|
|
206
258
|
row.limitation_refs.length === 0;
|
|
207
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
|
+
}
|
|
208
307
|
function maturityLevelRank(level) {
|
|
209
308
|
return MATURITY_LEVELS.indexOf(level);
|
|
210
309
|
}
|
|
211
310
|
function higherMaturityLevel(current, next) {
|
|
212
311
|
return maturityLevelRank(next) > maturityLevelRank(current) ? next : current;
|
|
213
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
|
+
}
|
|
214
337
|
function answerClaimMatchesBaselineRow(claim, row) {
|
|
215
338
|
return claim.purpose_element_refs.includes(row.purpose_element_ref) &&
|
|
216
339
|
claim.target_surface_refs.includes(row.actionability_surface_ref) &&
|
|
@@ -256,6 +379,7 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
256
379
|
purpose_confirmation_validation_ref: args.purposeConfirmationValidationRef,
|
|
257
380
|
source_material_admission_ledger_ref: args.sourceMaterialAdmissionLedgerRef,
|
|
258
381
|
source_material_admission_validation_ref: args.sourceMaterialAdmissionValidationRef,
|
|
382
|
+
candidate_limitation_refs: [],
|
|
259
383
|
baseline_rows: [],
|
|
260
384
|
};
|
|
261
385
|
}
|
|
@@ -266,10 +390,12 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
266
390
|
const resolvedEvidence = rowEvidence.length > 0
|
|
267
391
|
? rowEvidence
|
|
268
392
|
: element.supporting_evidence_refs;
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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);
|
|
273
399
|
const coverage = questionCoverage({
|
|
274
400
|
seedRefs,
|
|
275
401
|
competencyQuestions: args.competencyQuestions,
|
|
@@ -338,6 +464,7 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
338
464
|
purpose_confirmation_validation_ref: args.purposeConfirmationValidationRef,
|
|
339
465
|
source_material_admission_ledger_ref: args.sourceMaterialAdmissionLedgerRef,
|
|
340
466
|
source_material_admission_validation_ref: args.sourceMaterialAdmissionValidationRef,
|
|
467
|
+
candidate_limitation_refs: candidate.limitation_refs,
|
|
341
468
|
baseline_rows: rows.map((row) => ({
|
|
342
469
|
...row,
|
|
343
470
|
blocking_reason: needsFrontier(row)
|
|
@@ -346,8 +473,55 @@ export function buildMaturationBaselineArtifact(args) {
|
|
|
346
473
|
})),
|
|
347
474
|
};
|
|
348
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
|
+
}
|
|
349
522
|
export function validateMaturationBaseline(args) {
|
|
350
523
|
const violations = [];
|
|
524
|
+
const assertedObligationIds = [];
|
|
351
525
|
const baseline = args.maturationBaseline;
|
|
352
526
|
const selected = selectedPurposeCandidate({
|
|
353
527
|
sourcePurposeCandidates: args.sourcePurposeCandidates,
|
|
@@ -365,6 +539,25 @@ export function validateMaturationBaseline(args) {
|
|
|
365
539
|
subjectId: baseline.session_id,
|
|
366
540
|
}));
|
|
367
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");
|
|
368
561
|
if (!baseline.source_reconstruct_record_ref || !args.sourceReconstructRecordSha256) {
|
|
369
562
|
violations.push(violation({
|
|
370
563
|
code: "source_reconstruct_record_missing",
|
|
@@ -394,6 +587,10 @@ export function validateMaturationBaseline(args) {
|
|
|
394
587
|
subjectId: "baseline_rows",
|
|
395
588
|
}));
|
|
396
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");
|
|
397
594
|
for (const row of baseline.baseline_rows) {
|
|
398
595
|
if (seen.has(row.baseline_row_id)) {
|
|
399
596
|
violations.push(violation({
|
|
@@ -453,7 +650,19 @@ export function validateMaturationBaseline(args) {
|
|
|
453
650
|
row.member_target_material_kind !== null &&
|
|
454
651
|
row.member_source_refs.length > 0 &&
|
|
455
652
|
row.cross_material_ref_refs.length > 0;
|
|
456
|
-
|
|
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) {
|
|
457
666
|
violations.push(violation({
|
|
458
667
|
code: "mixed_lineage_missing",
|
|
459
668
|
message: "mixed-material baseline row must preserve member lineage or cite a limitation",
|
|
@@ -461,6 +670,39 @@ export function validateMaturationBaseline(args) {
|
|
|
461
670
|
}));
|
|
462
671
|
}
|
|
463
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
|
+
}
|
|
464
706
|
return {
|
|
465
707
|
schema_version: "1",
|
|
466
708
|
session_id: baseline.session_id,
|
|
@@ -477,6 +719,7 @@ export function validateMaturationBaseline(args) {
|
|
|
477
719
|
validation_results: violations.length === 0
|
|
478
720
|
? ["maturation_baseline_valid"]
|
|
479
721
|
: ["maturation_baseline_invalid"],
|
|
722
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
480
723
|
violations,
|
|
481
724
|
};
|
|
482
725
|
}
|
|
@@ -484,29 +727,60 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
484
727
|
const answerClaims = args.maturationAnswerClaimsValidation?.validation_status === "valid"
|
|
485
728
|
? args.maturationAnswerClaims?.answer_claims ?? []
|
|
486
729
|
: [];
|
|
730
|
+
const shortfallIds = judgeSupportShortfallIds(args.maturationAnswerClaimsValidation);
|
|
487
731
|
const expansions = args.ontologyExpansionValidation?.validation_status === "valid"
|
|
488
732
|
? args.ontologyExpansion?.expansions ?? []
|
|
489
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
|
+
}
|
|
490
758
|
return {
|
|
491
759
|
schema_version: "1",
|
|
492
760
|
session_id: args.sessionId,
|
|
493
761
|
created_at: isoNow(),
|
|
494
762
|
maturation_baseline_ref: args.maturationBaselineRef,
|
|
495
763
|
maturation_baseline_validation_ref: args.maturationBaselineValidationRef,
|
|
764
|
+
candidate_limitation_refs: args.maturationBaseline.candidate_limitation_refs,
|
|
496
765
|
rows: args.maturationBaseline.baseline_rows.map((row) => {
|
|
497
766
|
const matchingAnswerClaims = answerClaims.filter((claim) => answerClaimMatchesBaselineRow(claim, row));
|
|
498
767
|
const matchingExpansions = expansions.filter((expansion) => expansionMatchesBaselineRow(expansion, row));
|
|
499
|
-
const positiveAnswerClaims = matchingAnswerClaims.filter((claim) => claim
|
|
500
|
-
|
|
501
|
-
const positiveExpansions = matchingExpansions.filter((expansion) => (expansion.operation === "add" || expansion.operation === "refine") &&
|
|
502
|
-
expansion.limitation_refs.length === 0);
|
|
768
|
+
const positiveAnswerClaims = matchingAnswerClaims.filter((claim) => positiveAnswerClaim(claim, shortfallIds));
|
|
769
|
+
const positiveExpansions = matchingExpansions.filter((expansion) => positiveExpansion(expansion, shortfallIds));
|
|
503
770
|
let maturityLevel = row.maturity_level;
|
|
504
771
|
const supportingRefs = [
|
|
505
772
|
...row.supporting_seed_refs,
|
|
506
773
|
...row.supporting_validation_refs,
|
|
507
774
|
...row.supporting_evidence_refs.map((ref) => ref.observation_id),
|
|
508
775
|
];
|
|
509
|
-
|
|
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));
|
|
510
784
|
if (matchingAnswerClaims.length > 0 && args.maturationAnswerClaimsValidationRef) {
|
|
511
785
|
supportingRefs.push(args.maturationAnswerClaimsValidationRef);
|
|
512
786
|
}
|
|
@@ -519,6 +793,12 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
519
793
|
supportingRefs.push(ref.observation_id);
|
|
520
794
|
}
|
|
521
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
|
+
}
|
|
522
802
|
}
|
|
523
803
|
for (const expansion of matchingExpansions) {
|
|
524
804
|
supportingRefs.push(expansion.expansion_id, ...expansion.answer_claim_refs);
|
|
@@ -533,16 +813,17 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
533
813
|
if (positiveAnswerClaims.length > 0 && positiveExpansions.length > 0) {
|
|
534
814
|
maturityLevel = higherMaturityLevel(maturityLevel, "L4_validated_for_purpose");
|
|
535
815
|
}
|
|
536
|
-
|
|
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({
|
|
537
822
|
materiality: row.materiality,
|
|
538
|
-
|
|
539
|
-
|
|
823
|
+
maturityLevel,
|
|
824
|
+
residualLimitationRefs: limitationRefs,
|
|
825
|
+
dischargedForRow,
|
|
540
826
|
});
|
|
541
|
-
const memberReadiness = limitationRefs.length > 0
|
|
542
|
-
? "limitation_backed"
|
|
543
|
-
: frontierRequired
|
|
544
|
-
? "frontier_required"
|
|
545
|
-
: "closed";
|
|
546
827
|
return {
|
|
547
828
|
matrix_row_id: `matrix-${slug(row.baseline_row_id)}`,
|
|
548
829
|
baseline_row_refs: [row.baseline_row_id],
|
|
@@ -560,13 +841,19 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
560
841
|
competency_assessment_refs: row.competency_assessment_refs,
|
|
561
842
|
maturity_level: maturityLevel,
|
|
562
843
|
supporting_refs: [...new Set(supportingRefs)],
|
|
563
|
-
|
|
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
|
+
: [],
|
|
564
849
|
limitation_refs: [...new Set(limitationRefs)],
|
|
565
|
-
next_action:
|
|
850
|
+
next_action: memberReadiness === "frontier_required"
|
|
566
851
|
? "Create a maturation frontier question for this row."
|
|
567
852
|
: memberReadiness === "limitation_backed"
|
|
568
853
|
? "Keep the limitation visible in continuation decisions."
|
|
569
|
-
:
|
|
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.",
|
|
570
857
|
};
|
|
571
858
|
}),
|
|
572
859
|
};
|
|
@@ -574,13 +861,43 @@ export function buildActionabilityMatrixArtifact(args) {
|
|
|
574
861
|
export function validateActionabilityMatrix(args) {
|
|
575
862
|
const matrix = args.actionabilityMatrix;
|
|
576
863
|
const violations = [];
|
|
864
|
+
const assertedObligationIds = [];
|
|
577
865
|
const baselineRows = new Map(args.maturationBaseline.baseline_rows.map((row) => [row.baseline_row_id, row]));
|
|
578
866
|
const answerClaims = args.maturationAnswerClaimsValidation?.validation_status === "valid"
|
|
579
867
|
? args.maturationAnswerClaims?.answer_claims ?? []
|
|
580
868
|
: [];
|
|
869
|
+
const shortfallIds = judgeSupportShortfallIds(args.maturationAnswerClaimsValidation);
|
|
581
870
|
const expansions = args.ontologyExpansionValidation?.validation_status === "valid"
|
|
582
871
|
? args.ontologyExpansion?.expansions ?? []
|
|
583
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
|
+
}
|
|
584
901
|
const seen = new Set();
|
|
585
902
|
if (matrix.session_id !== args.maturationBaseline.session_id) {
|
|
586
903
|
violations.push(violation({
|
|
@@ -589,6 +906,21 @@ export function validateActionabilityMatrix(args) {
|
|
|
589
906
|
subjectId: matrix.session_id,
|
|
590
907
|
}));
|
|
591
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
|
+
}
|
|
592
924
|
if (args.maturationBaselineValidation.validation_status !== "valid") {
|
|
593
925
|
violations.push(violation({
|
|
594
926
|
code: "prior_validation_invalid",
|
|
@@ -596,6 +928,132 @@ export function validateActionabilityMatrix(args) {
|
|
|
596
928
|
subjectId: args.maturationBaselineValidationRef ?? null,
|
|
597
929
|
}));
|
|
598
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
|
+
}
|
|
599
1057
|
for (const row of matrix.rows) {
|
|
600
1058
|
if (seen.has(row.matrix_row_id)) {
|
|
601
1059
|
violations.push(violation({
|
|
@@ -605,6 +1063,13 @@ export function validateActionabilityMatrix(args) {
|
|
|
605
1063
|
}));
|
|
606
1064
|
}
|
|
607
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
|
+
}
|
|
608
1073
|
const baselineRef = row.baseline_row_refs[0] ?? null;
|
|
609
1074
|
const baselineRow = baselineRef ? baselineRows.get(baselineRef) : null;
|
|
610
1075
|
if (!baselineRow) {
|
|
@@ -622,6 +1087,31 @@ export function validateActionabilityMatrix(args) {
|
|
|
622
1087
|
subjectId: row.matrix_row_id,
|
|
623
1088
|
}));
|
|
624
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
|
+
}
|
|
625
1115
|
if (maturityLevelRank(row.maturity_level) <
|
|
626
1116
|
maturityLevelRank(baselineRow.maturity_level)) {
|
|
627
1117
|
violations.push(violation({
|
|
@@ -631,11 +1121,9 @@ export function validateActionabilityMatrix(args) {
|
|
|
631
1121
|
}));
|
|
632
1122
|
}
|
|
633
1123
|
const matchingAnswerClaims = answerClaims.filter((claim) => answerClaimMatchesBaselineRow(claim, baselineRow) &&
|
|
634
|
-
claim
|
|
635
|
-
claim.limitation_refs.length === 0);
|
|
1124
|
+
positiveAnswerClaim(claim, shortfallIds));
|
|
636
1125
|
const matchingExpansions = expansions.filter((expansion) => expansionMatchesBaselineRow(expansion, baselineRow) &&
|
|
637
|
-
(expansion
|
|
638
|
-
expansion.limitation_refs.length === 0);
|
|
1126
|
+
positiveExpansion(expansion, shortfallIds));
|
|
639
1127
|
if (row.maturity_level !== baselineRow.maturity_level) {
|
|
640
1128
|
const claimsCanRaiseToL3 = matchingAnswerClaims.length > 0;
|
|
641
1129
|
const claimsCanRaiseToL4 = matchingAnswerClaims.length > 0 && matchingExpansions.length > 0;
|
|
@@ -675,18 +1163,157 @@ export function validateActionabilityMatrix(args) {
|
|
|
675
1163
|
}));
|
|
676
1164
|
}
|
|
677
1165
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
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
|
+
});
|
|
683
1212
|
if (row.member_readiness !== expectedReadiness) {
|
|
684
1213
|
violations.push(violation({
|
|
685
1214
|
code: "conflicting_state",
|
|
686
|
-
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",
|
|
687
1216
|
subjectId: row.matrix_row_id,
|
|
688
1217
|
}));
|
|
689
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
|
+
}
|
|
690
1317
|
}
|
|
691
1318
|
return {
|
|
692
1319
|
schema_version: "1",
|
|
@@ -696,18 +1323,26 @@ export function validateActionabilityMatrix(args) {
|
|
|
696
1323
|
maturation_baseline_validation_ref: args.maturationBaselineValidationRef ?? null,
|
|
697
1324
|
maturation_answer_claims_validation_ref: args.maturationAnswerClaimsValidationRef ?? null,
|
|
698
1325
|
ontology_expansion_validation_ref: args.ontologyExpansionValidationRef ?? null,
|
|
1326
|
+
maturation_question_frontier_validation_ref: args.maturationQuestionFrontierValidationRef ?? null,
|
|
699
1327
|
validation_status: violations.length === 0 ? "valid" : "invalid",
|
|
700
1328
|
matrix_row_count: matrix.rows.length,
|
|
701
1329
|
frontier_required_row_count: matrix.rows.filter((row) => row.member_readiness === "frontier_required").length,
|
|
702
1330
|
validation_results: violations.length === 0
|
|
703
1331
|
? ["actionability_matrix_valid"]
|
|
704
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,
|
|
705
1339
|
violations,
|
|
706
1340
|
};
|
|
707
1341
|
}
|
|
708
1342
|
export function validateMaturationQuestionFrontier(args) {
|
|
709
1343
|
const frontier = args.maturationQuestionFrontier;
|
|
710
1344
|
const violations = [];
|
|
1345
|
+
const assertedObligationIds = [];
|
|
711
1346
|
const baselineRows = new Map(args.maturationBaseline.baseline_rows.map((row) => [row.baseline_row_id, row]));
|
|
712
1347
|
const materialFrontierBaselineRows = new Set(args.actionabilityMatrix.rows
|
|
713
1348
|
.filter((row) => row.member_readiness === "frontier_required")
|
|
@@ -735,6 +1370,13 @@ export function validateMaturationQuestionFrontier(args) {
|
|
|
735
1370
|
subjectId: args.actionabilityMatrixValidationRef ?? null,
|
|
736
1371
|
}));
|
|
737
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");
|
|
738
1380
|
for (const question of frontier.questions) {
|
|
739
1381
|
if (seen.has(question.question_id)) {
|
|
740
1382
|
violations.push(violation({
|
|
@@ -812,6 +1454,7 @@ export function validateMaturationQuestionFrontier(args) {
|
|
|
812
1454
|
validation_results: violations.length === 0
|
|
813
1455
|
? ["maturation_question_frontier_valid"]
|
|
814
1456
|
: ["maturation_question_frontier_invalid"],
|
|
1457
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
815
1458
|
violations,
|
|
816
1459
|
};
|
|
817
1460
|
}
|
|
@@ -849,6 +1492,19 @@ function questionMap(frontier) {
|
|
|
849
1492
|
export function validateMaturationClosureFrontier(args) {
|
|
850
1493
|
const frontier = args.maturationClosureFrontier;
|
|
851
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");
|
|
852
1508
|
const materialQuestions = materialQuestionIds(args.maturationQuestionFrontier);
|
|
853
1509
|
const questions = questionMap(args.maturationQuestionFrontier);
|
|
854
1510
|
const sourceRequestsSeen = new Set();
|
|
@@ -1053,6 +1709,7 @@ export function validateMaturationClosureFrontier(args) {
|
|
|
1053
1709
|
validation_results: violations.length === 0
|
|
1054
1710
|
? ["maturation_closure_frontier_valid"]
|
|
1055
1711
|
: ["maturation_closure_frontier_invalid"],
|
|
1712
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1056
1713
|
violations,
|
|
1057
1714
|
};
|
|
1058
1715
|
}
|
|
@@ -1171,12 +1828,239 @@ export function validateMaturationAuthorityResponse(args) {
|
|
|
1171
1828
|
violations,
|
|
1172
1829
|
};
|
|
1173
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
|
+
}
|
|
1174
2034
|
export function validateAnswerSupportLedger(args) {
|
|
1175
2035
|
const ledger = args.answerSupportLedger;
|
|
1176
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");
|
|
1177
2050
|
const questions = questionMap(args.maturationQuestionFrontier);
|
|
1178
2051
|
const evidenceIndex = evidenceRefIndex(args.sourceObservations);
|
|
1179
2052
|
const lineageObservationIds = new Set(args.sourceObservationLineageIndex?.lineage_rows.flatMap((row) => row.added_observation_ids) ?? []);
|
|
2053
|
+
// Index added_observation_id -> first lineage row, mirroring the `.find`
|
|
2054
|
+
// (first-match) semantics so per-ref lookups are O(1) instead of a nested
|
|
2055
|
+
// linear scan over every lineage row.
|
|
2056
|
+
const lineageRowByObservationId = new Map();
|
|
2057
|
+
for (const row of args.sourceObservationLineageIndex?.lineage_rows ?? []) {
|
|
2058
|
+
for (const observationId of row.added_observation_ids) {
|
|
2059
|
+
if (!lineageRowByObservationId.has(observationId)) {
|
|
2060
|
+
lineageRowByObservationId.set(observationId, row);
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
1180
2064
|
const reentryValidationsByRef = new Map((args.sourceObservationReentryValidations ?? []).map((item) => [
|
|
1181
2065
|
item.ref,
|
|
1182
2066
|
item.validation,
|
|
@@ -1414,10 +2298,19 @@ export function validateAnswerSupportLedger(args) {
|
|
|
1414
2298
|
}));
|
|
1415
2299
|
}
|
|
1416
2300
|
const observation = sourceObservationsById.get(ref.observation_id);
|
|
1417
|
-
const lineageRow =
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
2301
|
+
const lineageRow = lineageRowByObservationId.get(ref.observation_id) ??
|
|
2302
|
+
null;
|
|
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);
|
|
1421
2314
|
if (refCarriesLineage &&
|
|
1422
2315
|
args.sourceObservationLineageIndex &&
|
|
1423
2316
|
!lineageRow) {
|
|
@@ -1484,8 +2377,7 @@ export function validateAnswerSupportLedger(args) {
|
|
|
1484
2377
|
}
|
|
1485
2378
|
if (!observation ||
|
|
1486
2379
|
!publicOutputRow ||
|
|
1487
|
-
|
|
1488
|
-
publicOutputRow.visibility_tier !== "redacted_output_only")) {
|
|
2380
|
+
publicOutputRow.visibility_tier !== "consumption_allowed") {
|
|
1489
2381
|
violations.push(violation({
|
|
1490
2382
|
code: "missing_required_ref",
|
|
1491
2383
|
message: "answer support evidence used in public output must have an observation-specific public_output source-safety row",
|
|
@@ -1572,17 +2464,37 @@ export function validateAnswerSupportLedger(args) {
|
|
|
1572
2464
|
validation_results: violations.length === 0
|
|
1573
2465
|
? ["answer_support_ledger_valid"]
|
|
1574
2466
|
: ["answer_support_ledger_invalid"],
|
|
2467
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1575
2468
|
violations,
|
|
1576
2469
|
};
|
|
1577
2470
|
}
|
|
1578
2471
|
export function validateMaturationAnswerClaims(args) {
|
|
1579
2472
|
const artifact = args.maturationAnswerClaims;
|
|
1580
2473
|
const violations = [];
|
|
2474
|
+
const assertedObligationIds = [];
|
|
2475
|
+
const judgeSupportShortfallClaimIds = new Set();
|
|
1581
2476
|
const questions = questionMap(args.maturationQuestionFrontier);
|
|
1582
2477
|
const clusters = new Map(args.answerSupportLedger.evidence_clusters.map((cluster) => [
|
|
1583
2478
|
cluster.evidence_cluster_id,
|
|
1584
2479
|
cluster,
|
|
1585
2480
|
]));
|
|
2481
|
+
// B-6 judge gate. judgeActive = orchestrator supplied a non-null judgment whose
|
|
2482
|
+
// validation is valid. judgeSupported keys a confirmed support by IDENTITY
|
|
2483
|
+
// (`${evidence_cluster_ref}#${evidenceRefKey}`) so the per-claim sufficiency
|
|
2484
|
+
// count below can re-key it by INDEPENDENCE. With the judge gate active (R4) a
|
|
2485
|
+
// convergent_source_evidence claim is FAIL-CLOSED on judgeActive: an
|
|
2486
|
+
// absent/invalid judgment makes such a claim invalid (see the per-claim branch).
|
|
2487
|
+
// Non-convergent claims are unaffected.
|
|
2488
|
+
const judgeActive = Boolean(args.answerSupportJudgment) &&
|
|
2489
|
+
args.answerSupportJudgmentValidation?.validation_status === "valid";
|
|
2490
|
+
const judgeSupported = new Set();
|
|
2491
|
+
if (judgeActive && args.answerSupportJudgment) {
|
|
2492
|
+
for (const judgment of args.answerSupportJudgment.judgments) {
|
|
2493
|
+
if (judgment.supports === "supported") {
|
|
2494
|
+
judgeSupported.add(`${judgment.evidence_cluster_ref}#${evidenceRefKey(judgment.evidence_ref)}`);
|
|
2495
|
+
}
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
1586
2498
|
const seen = new Set();
|
|
1587
2499
|
const answeredQuestions = new Set();
|
|
1588
2500
|
if (artifact.session_id !== args.maturationQuestionFrontier.session_id) {
|
|
@@ -1606,6 +2518,15 @@ export function validateMaturationAnswerClaims(args) {
|
|
|
1606
2518
|
subjectId: args.maturationQuestionFrontierValidationRef ?? null,
|
|
1607
2519
|
}));
|
|
1608
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");
|
|
1609
2530
|
for (const claim of artifact.answer_claims) {
|
|
1610
2531
|
if (seen.has(claim.answer_claim_id)) {
|
|
1611
2532
|
violations.push(violation({
|
|
@@ -1677,6 +2598,73 @@ export function validateMaturationAnswerClaims(args) {
|
|
|
1677
2598
|
}));
|
|
1678
2599
|
}
|
|
1679
2600
|
}
|
|
2601
|
+
// B-6 sufficiency (composes with the contradiction-bounded check above; a
|
|
2602
|
+
// convergent claim must pass BOTH). Count INDEPENDENT judge-confirmed
|
|
2603
|
+
// supports across all cited clusters: IDENTITY key joins to the judge
|
|
2604
|
+
// verdict, INDEPENDENCE key (source_ref:location, byte-identical to the
|
|
2605
|
+
// ledger envelope) is counted so same-source refs collapse to one.
|
|
2606
|
+
if (claim.support_mode === "convergent_source_evidence") {
|
|
2607
|
+
if (!judgeActive) {
|
|
2608
|
+
// Fail-closed: the answer-support judge gate is active (R4), so a
|
|
2609
|
+
// convergent-source claim REQUIRES a valid judgment. judgeActive is
|
|
2610
|
+
// false exactly when the judgment artifact is absent or its validation
|
|
2611
|
+
// is not valid — either way the convergent claim cannot be trusted.
|
|
2612
|
+
violations.push(violation({
|
|
2613
|
+
code: "prior_validation_invalid",
|
|
2614
|
+
message: "convergent source evidence claim requires a valid answer-support judgment",
|
|
2615
|
+
subjectId: claim.answer_claim_id,
|
|
2616
|
+
}));
|
|
2617
|
+
}
|
|
2618
|
+
else {
|
|
2619
|
+
// Count the claim's OWN supporting_evidence_refs that are judge-confirmed
|
|
2620
|
+
// in one of its cited clusters, not every ref in those clusters. Downstream
|
|
2621
|
+
// (claim projection, ontology expansion) consumes supporting_evidence_refs,
|
|
2622
|
+
// so sufficiency must be carried by the evidence the claim actually cites.
|
|
2623
|
+
const independentConfirmed = new Set();
|
|
2624
|
+
for (const ref of claim.supporting_evidence_refs) {
|
|
2625
|
+
const judgeConfirmed = claim.evidence_cluster_refs.some((clusterRef) => judgeSupported.has(`${clusterRef}#${evidenceRefKey(ref)}`));
|
|
2626
|
+
if (judgeConfirmed) {
|
|
2627
|
+
independentConfirmed.add(`${normalizedPathRef(ref.source_ref)}:${normalizedPathRef(ref.location)}`);
|
|
2628
|
+
}
|
|
2629
|
+
}
|
|
2630
|
+
if (independentConfirmed.size < 2) {
|
|
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
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
}
|
|
2667
|
+
}
|
|
1680
2668
|
if (claim.evidence_cluster_refs.length === 0) {
|
|
1681
2669
|
violations.push(violation({
|
|
1682
2670
|
code: "support_mode_missing_authority",
|
|
@@ -1698,13 +2686,16 @@ export function validateMaturationAnswerClaims(args) {
|
|
|
1698
2686
|
created_at: isoNow(),
|
|
1699
2687
|
maturation_answer_claims_ref: args.maturationAnswerClaimsRef ?? null,
|
|
1700
2688
|
answer_support_ledger_validation_ref: args.answerSupportLedgerValidationRef ?? null,
|
|
2689
|
+
answer_support_judgment_validation_ref: args.answerSupportJudgmentValidationRef ?? null,
|
|
1701
2690
|
maturation_question_frontier_validation_ref: args.maturationQuestionFrontierValidationRef ?? null,
|
|
1702
2691
|
validation_status: violations.length === 0 ? "valid" : "invalid",
|
|
1703
2692
|
answer_claim_count: artifact.answer_claims.length,
|
|
1704
2693
|
answered_question_count: answeredQuestions.size,
|
|
2694
|
+
judge_support_shortfall_claim_ids: [...judgeSupportShortfallClaimIds].sort(),
|
|
1705
2695
|
validation_results: violations.length === 0
|
|
1706
2696
|
? ["maturation_answer_claims_valid"]
|
|
1707
2697
|
: ["maturation_answer_claims_invalid"],
|
|
2698
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1708
2699
|
violations,
|
|
1709
2700
|
};
|
|
1710
2701
|
}
|
|
@@ -1722,6 +2713,25 @@ export function validateOntologyExpansion(args) {
|
|
|
1722
2713
|
defer: 0,
|
|
1723
2714
|
reject: 0,
|
|
1724
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");
|
|
1725
2735
|
if (artifact.session_id !== args.maturationAnswerClaims.session_id) {
|
|
1726
2736
|
violations.push(violation({
|
|
1727
2737
|
code: "session_id_mismatch",
|
|
@@ -1826,6 +2836,7 @@ export function validateOntologyExpansion(args) {
|
|
|
1826
2836
|
validation_results: violations.length === 0
|
|
1827
2837
|
? ["ontology_expansion_valid"]
|
|
1828
2838
|
: ["ontology_expansion_invalid"],
|
|
2839
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
1829
2840
|
violations,
|
|
1830
2841
|
};
|
|
1831
2842
|
}
|
|
@@ -2035,6 +3046,14 @@ export function validateMaturationConvergenceLedger(args) {
|
|
|
2035
3046
|
const answerClaimIds = new Set(args.maturationAnswerClaims.answer_claims.map((claim) => claim.answer_claim_id));
|
|
2036
3047
|
const answerSupportIds = new Set(args.answerSupportLedger.evidence_clusters.map((cluster) => cluster.evidence_cluster_id));
|
|
2037
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");
|
|
2038
3057
|
if (ledger.session_id !== args.maturationQuestionFrontier.session_id) {
|
|
2039
3058
|
violations.push(violation({
|
|
2040
3059
|
code: "session_id_mismatch",
|
|
@@ -2376,18 +3395,15 @@ export function validateMaturationConvergenceLedger(args) {
|
|
|
2376
3395
|
validation_results: violations.length === 0
|
|
2377
3396
|
? ["maturation_convergence_ledger_valid"]
|
|
2378
3397
|
: ["maturation_convergence_ledger_invalid"],
|
|
3398
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
2379
3399
|
violations,
|
|
2380
3400
|
};
|
|
2381
3401
|
}
|
|
2382
3402
|
export function buildMaturationSourceDeltaArtifact(args) {
|
|
2383
3403
|
const matrixRows = args.actionabilityMatrix.rows;
|
|
3404
|
+
const actionabilityIndex = indexActionabilityRowsByDelta(matrixRows);
|
|
2384
3405
|
const impactRows = (args.sourceObservationDelta?.delta_rows ?? []).map((deltaRow) => {
|
|
2385
|
-
const affectedMatrixRowRefs = matrixRows
|
|
2386
|
-
.filter((row) => row.member_source_refs.some((ref) => sameResolvedRef(ref, deltaRow.source_ref)) ||
|
|
2387
|
-
row.cross_material_ref_refs.some((ref) => sameResolvedRef(ref, deltaRow.source_ref)) ||
|
|
2388
|
-
row.supporting_refs.includes(deltaRow.observation_id))
|
|
2389
|
-
.map((row) => row.matrix_row_id)
|
|
2390
|
-
.sort();
|
|
3406
|
+
const affectedMatrixRowRefs = affectedMatrixRowRefsForDelta(matrixRows, actionabilityIndex, deltaRow);
|
|
2391
3407
|
return {
|
|
2392
3408
|
impact_row_id: `maturation-source-delta:${slug(deltaRow.delta_row_id)}`,
|
|
2393
3409
|
delta_row_id: deltaRow.delta_row_id,
|
|
@@ -2426,7 +3442,9 @@ export function buildMaturationSourceDeltaArtifact(args) {
|
|
|
2426
3442
|
export function validateMaturationSourceDelta(args) {
|
|
2427
3443
|
const artifact = args.maturationSourceDelta;
|
|
2428
3444
|
const violations = [];
|
|
2429
|
-
const
|
|
3445
|
+
const matrixRows = args.actionabilityMatrix.rows;
|
|
3446
|
+
const matrixRowIds = new Set(matrixRows.map((row) => row.matrix_row_id));
|
|
3447
|
+
const actionabilityIndex = indexActionabilityRowsByDelta(matrixRows);
|
|
2430
3448
|
const deltaRowsById = new Map((args.sourceObservationDelta?.delta_rows ?? []).map((row) => [
|
|
2431
3449
|
row.delta_row_id,
|
|
2432
3450
|
row,
|
|
@@ -2504,12 +3522,7 @@ export function validateMaturationSourceDelta(args) {
|
|
|
2504
3522
|
}));
|
|
2505
3523
|
}
|
|
2506
3524
|
}
|
|
2507
|
-
const expectedAffectedMatrixRowRefs =
|
|
2508
|
-
.filter((matrixRow) => matrixRow.member_source_refs.some((ref) => sameResolvedRef(ref, deltaRow.source_ref)) ||
|
|
2509
|
-
matrixRow.cross_material_ref_refs.some((ref) => sameResolvedRef(ref, deltaRow.source_ref)) ||
|
|
2510
|
-
matrixRow.supporting_refs.includes(deltaRow.observation_id))
|
|
2511
|
-
.map((matrixRow) => matrixRow.matrix_row_id)
|
|
2512
|
-
.sort();
|
|
3525
|
+
const expectedAffectedMatrixRowRefs = affectedMatrixRowRefsForDelta(matrixRows, actionabilityIndex, deltaRow);
|
|
2513
3526
|
if (row.affected_matrix_row_refs.join("\0") !==
|
|
2514
3527
|
expectedAffectedMatrixRowRefs.join("\0")) {
|
|
2515
3528
|
violations.push(violation({
|
|
@@ -2608,15 +3621,50 @@ export async function writeMaturationSourceDeltaValidationArtifact(args) {
|
|
|
2608
3621
|
await writeYamlDocument(args.outputPath, validation);
|
|
2609
3622
|
return validation;
|
|
2610
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
|
+
}
|
|
2611
3635
|
export function buildMaturationContinuationDecisionArtifact(args) {
|
|
2612
3636
|
const materialRows = args.actionabilityMatrix.rows.filter((row) => row.materiality === "blocker" || row.materiality === "high");
|
|
2613
3637
|
const frontierRows = materialRows.filter((row) => row.member_readiness === "frontier_required");
|
|
2614
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");
|
|
2615
3647
|
const blockingRowRefs = frontierRows.map((row) => row.matrix_row_id);
|
|
2616
3648
|
const authorityRequestRefs = args.maturationClosureFrontier.authority_requests.map((request) => request.authority_request_id);
|
|
2617
3649
|
const unresolvedAuthorityResponses = args.maturationAuthorityResponse.responses.filter((response) => response.response_status !== "provided");
|
|
2618
3650
|
const finalRequestionStatus = args.maturationConvergenceLedgerValidation.final_requestion_pass_status;
|
|
2619
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);
|
|
2620
3668
|
let decisionState;
|
|
2621
3669
|
let rationale;
|
|
2622
3670
|
const convergenceLimitationRefs = [];
|
|
@@ -2628,23 +3676,52 @@ export function buildMaturationContinuationDecisionArtifact(args) {
|
|
|
2628
3676
|
decisionState = "blocked";
|
|
2629
3677
|
rationale = "Material rows remain frontier-required, but no validated next source or authority response can advance them.";
|
|
2630
3678
|
}
|
|
2631
|
-
else if (limitationRows.length > 0 &&
|
|
3679
|
+
else if (limitationRows.length > 0 &&
|
|
3680
|
+
closedRows.length === 0 &&
|
|
3681
|
+
valueResolvedRows.length === 0) {
|
|
2632
3682
|
decisionState = "blocked";
|
|
2633
|
-
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.";
|
|
2634
3684
|
}
|
|
2635
3685
|
else if (limitationRows.length > 0) {
|
|
2636
3686
|
decisionState = "actionable_limited";
|
|
2637
3687
|
rationale = "No material frontier remains, but named limitations constrain the actionability claim.";
|
|
2638
3688
|
}
|
|
2639
|
-
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) {
|
|
2640
3707
|
decisionState = "actionable_limited";
|
|
2641
|
-
convergenceLimitationRefs.push(`maturation-final-requestion:${finalRequestionStatus}`);
|
|
2642
3708
|
rationale = "No material frontier remains, but final re-question convergence has not proven actionable readiness.";
|
|
2643
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
|
+
}
|
|
2644
3718
|
else {
|
|
2645
3719
|
decisionState = "actionable_ready";
|
|
2646
3720
|
rationale = "All material rows are closed for the declared purpose.";
|
|
2647
3721
|
}
|
|
3722
|
+
if (convergenceUnproven) {
|
|
3723
|
+
convergenceLimitationRefs.push(`maturation-final-requestion:${finalRequestionStatus}`);
|
|
3724
|
+
}
|
|
2648
3725
|
const nextFrontierRefs = [
|
|
2649
3726
|
...args.maturationQuestionFrontier.questions
|
|
2650
3727
|
.filter((question) => question.current_answer_status !== "answerable" &&
|
|
@@ -2666,19 +3743,38 @@ export function buildMaturationContinuationDecisionArtifact(args) {
|
|
|
2666
3743
|
authority_request_refs: authorityRequestRefs,
|
|
2667
3744
|
authority_response_refs: args.maturationAuthorityResponse.responses.map((response) => response.authority_response_id),
|
|
2668
3745
|
claim_scope: {
|
|
2669
|
-
|
|
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),
|
|
2670
3754
|
excluded_row_refs: args.actionabilityMatrix.rows
|
|
2671
|
-
.filter((row) => row.member_readiness !== "closed"
|
|
3755
|
+
.filter((row) => row.member_readiness !== "closed" &&
|
|
3756
|
+
row.member_readiness !== "value_resolved")
|
|
2672
3757
|
.map((row) => row.matrix_row_id),
|
|
2673
3758
|
exclusion_rationale: limitationRows.length > 0 || frontierRows.length > 0
|
|
2674
3759
|
? "Rows outside the trusted claim remain limitation-backed or frontier-required."
|
|
2675
3760
|
: null,
|
|
2676
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,
|
|
2677
3766
|
limitation_refs: [
|
|
2678
3767
|
...new Set([
|
|
2679
3768
|
...args.actionabilityMatrix.rows.flatMap((row) => row.limitation_refs),
|
|
3769
|
+
...candidateLimitationRefs,
|
|
2680
3770
|
...args.ontologyExpansionValidation.violations.map((item) => item.subject_id ?? "ontology_expansion_validation"),
|
|
2681
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}`),
|
|
2682
3778
|
]),
|
|
2683
3779
|
],
|
|
2684
3780
|
};
|
|
@@ -2686,6 +3782,24 @@ export function buildMaturationContinuationDecisionArtifact(args) {
|
|
|
2686
3782
|
export function validateMaturationContinuationDecision(args) {
|
|
2687
3783
|
const decision = args.maturationContinuationDecision;
|
|
2688
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");
|
|
2689
3803
|
const matrixRows = new Map(args.actionabilityMatrix.rows.map((row) => [
|
|
2690
3804
|
row.matrix_row_id,
|
|
2691
3805
|
row,
|
|
@@ -2716,6 +3830,7 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2716
3830
|
maturation_authority_response: args.maturationAuthorityResponseValidation.validation_status,
|
|
2717
3831
|
ontology_expansion: args.ontologyExpansionValidation.validation_status,
|
|
2718
3832
|
maturation_convergence_ledger: args.maturationConvergenceLedgerValidation.validation_status,
|
|
3833
|
+
revision_proposal: args.revisionProposalValidation.validation_status,
|
|
2719
3834
|
})) {
|
|
2720
3835
|
if (status !== "valid") {
|
|
2721
3836
|
violations.push(violation({
|
|
@@ -2745,6 +3860,33 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2745
3860
|
}));
|
|
2746
3861
|
}
|
|
2747
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
|
+
}
|
|
2748
3890
|
const hasAuthorityNeed = decision.authority_request_refs.length > 0;
|
|
2749
3891
|
if (decision.decision_state === "actionable_ready" && materialOpenRows.length > 0) {
|
|
2750
3892
|
violations.push(violation({
|
|
@@ -2753,6 +3895,85 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2753
3895
|
subjectId: "actionable_ready",
|
|
2754
3896
|
}));
|
|
2755
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
|
+
}
|
|
2756
3977
|
if (decision.decision_state === "actionable_ready" &&
|
|
2757
3978
|
args.maturationConvergenceLedgerValidation.final_requestion_pass_status !==
|
|
2758
3979
|
"no_new_material_question") {
|
|
@@ -2813,6 +4034,7 @@ export function validateMaturationContinuationDecision(args) {
|
|
|
2813
4034
|
validation_results: violations.length === 0
|
|
2814
4035
|
? ["maturation_continuation_decision_valid"]
|
|
2815
4036
|
: ["maturation_continuation_decision_invalid"],
|
|
4037
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
2816
4038
|
violations,
|
|
2817
4039
|
};
|
|
2818
4040
|
}
|
|
@@ -2911,6 +4133,17 @@ export function validateActionableOntology(args) {
|
|
|
2911
4133
|
const expansionIds = new Set(args.ontologyExpansion.expansions.map((expansion) => expansion.expansion_id));
|
|
2912
4134
|
const projectedMatrixRefs = new Set();
|
|
2913
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");
|
|
2914
4147
|
if (artifact.session_id !== args.actionabilityMatrix.session_id) {
|
|
2915
4148
|
violations.push(violation({
|
|
2916
4149
|
code: "session_id_mismatch",
|
|
@@ -3016,10 +4249,12 @@ export function validateActionableOntology(args) {
|
|
|
3016
4249
|
subjectId: row.projection_row_id,
|
|
3017
4250
|
}));
|
|
3018
4251
|
}
|
|
3019
|
-
if (row.claim_scope === "included" &&
|
|
4252
|
+
if (row.claim_scope === "included" &&
|
|
4253
|
+
matrixRow.member_readiness !== "closed" &&
|
|
4254
|
+
matrixRow.member_readiness !== "value_resolved") {
|
|
3020
4255
|
violations.push(violation({
|
|
3021
4256
|
code: "conflicting_state",
|
|
3022
|
-
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",
|
|
3023
4258
|
subjectId: row.matrix_row_ref,
|
|
3024
4259
|
}));
|
|
3025
4260
|
}
|
|
@@ -3069,6 +4304,7 @@ export function validateActionableOntology(args) {
|
|
|
3069
4304
|
validation_results: violations.length === 0
|
|
3070
4305
|
? ["actionable_ontology_valid"]
|
|
3071
4306
|
: ["actionable_ontology_invalid"],
|
|
4307
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
3072
4308
|
violations,
|
|
3073
4309
|
};
|
|
3074
4310
|
}
|
|
@@ -3196,7 +4432,7 @@ export async function writeMaturationBaselineValidationArtifact(args) {
|
|
|
3196
4432
|
}
|
|
3197
4433
|
export async function writeActionabilityMatrixArtifact(args) {
|
|
3198
4434
|
const maturationBaseline = await readYamlDocument(args.maturationBaselinePath);
|
|
3199
|
-
const [maturationAnswerClaims, maturationAnswerClaimsValidation, ontologyExpansion, ontologyExpansionValidation,] = await Promise.all([
|
|
4435
|
+
const [maturationAnswerClaims, maturationAnswerClaimsValidation, ontologyExpansion, ontologyExpansionValidation, maturationQuestionFrontier, maturationQuestionFrontierValidation, maturationValueDischarge, maturationValueDischargeValidation,] = await Promise.all([
|
|
3200
4436
|
args.maturationAnswerClaimsPath
|
|
3201
4437
|
? readYamlDocument(args.maturationAnswerClaimsPath)
|
|
3202
4438
|
: Promise.resolve(null),
|
|
@@ -3209,6 +4445,18 @@ export async function writeActionabilityMatrixArtifact(args) {
|
|
|
3209
4445
|
args.ontologyExpansionValidationPath
|
|
3210
4446
|
? readYamlDocument(args.ontologyExpansionValidationPath)
|
|
3211
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),
|
|
3212
4460
|
]);
|
|
3213
4461
|
const artifact = buildActionabilityMatrixArtifact({
|
|
3214
4462
|
sessionId: args.sessionId,
|
|
@@ -3218,15 +4466,19 @@ export async function writeActionabilityMatrixArtifact(args) {
|
|
|
3218
4466
|
maturationAnswerClaims,
|
|
3219
4467
|
maturationAnswerClaimsValidation,
|
|
3220
4468
|
maturationAnswerClaimsValidationRef: args.maturationAnswerClaimsValidationPath ?? null,
|
|
4469
|
+
maturationValueDischarge,
|
|
4470
|
+
maturationValueDischargeValidation,
|
|
3221
4471
|
ontologyExpansion,
|
|
3222
4472
|
ontologyExpansionValidation,
|
|
3223
4473
|
ontologyExpansionValidationRef: args.ontologyExpansionValidationPath ?? null,
|
|
4474
|
+
maturationQuestionFrontier,
|
|
4475
|
+
maturationQuestionFrontierValidation,
|
|
3224
4476
|
});
|
|
3225
4477
|
await writeYamlDocument(args.outputPath, artifact);
|
|
3226
4478
|
return artifact;
|
|
3227
4479
|
}
|
|
3228
4480
|
export async function writeActionabilityMatrixValidationArtifact(args) {
|
|
3229
|
-
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([
|
|
3230
4482
|
readYamlDocument(args.actionabilityMatrixPath),
|
|
3231
4483
|
readYamlDocument(args.maturationBaselinePath),
|
|
3232
4484
|
readYamlDocument(args.maturationBaselineValidationPath),
|
|
@@ -3242,6 +4494,18 @@ export async function writeActionabilityMatrixValidationArtifact(args) {
|
|
|
3242
4494
|
args.ontologyExpansionValidationPath
|
|
3243
4495
|
? readYamlDocument(args.ontologyExpansionValidationPath)
|
|
3244
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),
|
|
3245
4509
|
]);
|
|
3246
4510
|
const validation = validateActionabilityMatrix({
|
|
3247
4511
|
actionabilityMatrix,
|
|
@@ -3252,9 +4516,15 @@ export async function writeActionabilityMatrixValidationArtifact(args) {
|
|
|
3252
4516
|
maturationAnswerClaims,
|
|
3253
4517
|
maturationAnswerClaimsValidation,
|
|
3254
4518
|
maturationAnswerClaimsValidationRef: args.maturationAnswerClaimsValidationPath ?? null,
|
|
4519
|
+
maturationValueDischarge,
|
|
4520
|
+
maturationValueDischargeValidation,
|
|
3255
4521
|
ontologyExpansion,
|
|
3256
4522
|
ontologyExpansionValidation,
|
|
3257
4523
|
ontologyExpansionValidationRef: args.ontologyExpansionValidationPath ?? null,
|
|
4524
|
+
maturationQuestionFrontier,
|
|
4525
|
+
maturationQuestionFrontierValidation,
|
|
4526
|
+
maturationQuestionFrontierValidationRef: args.maturationQuestionFrontierValidationPath ?? null,
|
|
4527
|
+
maturationQuestionFrontierRef: args.maturationQuestionFrontierPath ?? null,
|
|
3258
4528
|
});
|
|
3259
4529
|
await writeYamlDocument(args.outputPath, validation);
|
|
3260
4530
|
return validation;
|
|
@@ -3404,6 +4674,170 @@ export async function writeAnswerSupportLedgerValidationArtifact(args) {
|
|
|
3404
4674
|
await writeYamlDocument(args.outputPath, validation);
|
|
3405
4675
|
return validation;
|
|
3406
4676
|
}
|
|
4677
|
+
// B-5 answer-support judge validation. Structural author!=judge attribution is
|
|
4678
|
+
// enforced upstream (separate authored artifact + 1:1 telemetry mapping); this
|
|
4679
|
+
// validator therefore NEVER compares directive_author.author_id (spoofable).
|
|
4680
|
+
// Obligations: refs resolve (unknown_id) / supports enum (invalid_enum) /
|
|
4681
|
+
// rationale present (missing_required_ref) / one verdict per (cluster, evidence)
|
|
4682
|
+
// pair (duplicate_id) / convergent coverage — every cited evidence_ref of a
|
|
4683
|
+
// convergent_source_evidence cluster must be judged (reuses missing_required_coverage).
|
|
4684
|
+
export function validateAnswerSupportJudgment(args) {
|
|
4685
|
+
const artifact = args.answerSupportJudgment;
|
|
4686
|
+
const ledger = args.answerSupportLedger;
|
|
4687
|
+
const violations = [];
|
|
4688
|
+
const SUPPORTS_VALUES = ["supported", "not_supported"];
|
|
4689
|
+
if (artifact.session_id !== ledger.session_id) {
|
|
4690
|
+
violations.push(violation({
|
|
4691
|
+
code: "session_id_mismatch",
|
|
4692
|
+
message: "answer support judgment session_id must match the support ledger",
|
|
4693
|
+
subjectId: artifact.session_id,
|
|
4694
|
+
}));
|
|
4695
|
+
}
|
|
4696
|
+
if (args.answerSupportLedgerValidation.validation_status !== "valid") {
|
|
4697
|
+
violations.push(violation({
|
|
4698
|
+
code: "prior_validation_invalid",
|
|
4699
|
+
message: "answer support judgment requires a valid support-ledger validation",
|
|
4700
|
+
subjectId: args.answerSupportLedgerValidationRef ?? "answer-support-ledger-validation",
|
|
4701
|
+
}));
|
|
4702
|
+
}
|
|
4703
|
+
const clusters = new Map(ledger.evidence_clusters.map((cluster) => [cluster.evidence_cluster_id, cluster]));
|
|
4704
|
+
const evidenceKeysByCluster = new Map(ledger.evidence_clusters.map((cluster) => [
|
|
4705
|
+
cluster.evidence_cluster_id,
|
|
4706
|
+
new Set(cluster.evidence_refs.map(evidenceRefKey)),
|
|
4707
|
+
]));
|
|
4708
|
+
// IDENTITY keys a judgment actually covered, per cluster (drives coverage D).
|
|
4709
|
+
const judgedKeysByCluster = new Map();
|
|
4710
|
+
const seen = new Set();
|
|
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");
|
|
4721
|
+
for (const judgment of artifact.judgments) {
|
|
4722
|
+
if (seen.has(judgment.judgment_id)) {
|
|
4723
|
+
violations.push(violation({
|
|
4724
|
+
code: "duplicate_id",
|
|
4725
|
+
message: "answer support judgment ids must be unique",
|
|
4726
|
+
subjectId: judgment.judgment_id,
|
|
4727
|
+
}));
|
|
4728
|
+
}
|
|
4729
|
+
else {
|
|
4730
|
+
seen.add(judgment.judgment_id);
|
|
4731
|
+
}
|
|
4732
|
+
// A: cluster ref + evidence ref resolve to a cited evidence in the ledger.
|
|
4733
|
+
const evidenceKey = evidenceRefKey(judgment.evidence_ref);
|
|
4734
|
+
const clusterKeys = evidenceKeysByCluster.get(judgment.evidence_cluster_ref);
|
|
4735
|
+
if (!clusters.has(judgment.evidence_cluster_ref) || !clusterKeys) {
|
|
4736
|
+
violations.push(violation({
|
|
4737
|
+
code: "unknown_id",
|
|
4738
|
+
message: "judgment evidence_cluster_ref must resolve to a support-ledger cluster",
|
|
4739
|
+
subjectId: judgment.evidence_cluster_ref,
|
|
4740
|
+
}));
|
|
4741
|
+
}
|
|
4742
|
+
else if (!clusterKeys.has(evidenceKey)) {
|
|
4743
|
+
violations.push(violation({
|
|
4744
|
+
code: "unknown_id",
|
|
4745
|
+
message: "judgment evidence_ref must resolve to a cited evidence ref in its cluster",
|
|
4746
|
+
subjectId: judgment.judgment_id,
|
|
4747
|
+
}));
|
|
4748
|
+
}
|
|
4749
|
+
else {
|
|
4750
|
+
const covered = judgedKeysByCluster.get(judgment.evidence_cluster_ref) ??
|
|
4751
|
+
new Set();
|
|
4752
|
+
// One verdict per (cluster, evidence) IDENTITY pair: a second judgment for
|
|
4753
|
+
// the same pair (e.g. a contradictory supported + not_supported) must be
|
|
4754
|
+
// invalid. Otherwise B-6 would launder it — judgeSupported only ADDS
|
|
4755
|
+
// 'supported' keys, so a conflict silently resolves to "supported wins" and
|
|
4756
|
+
// an ambiguous ref counts toward convergent sufficiency. Reuses duplicate_id.
|
|
4757
|
+
if (covered.has(evidenceKey)) {
|
|
4758
|
+
violations.push(violation({
|
|
4759
|
+
code: "duplicate_id",
|
|
4760
|
+
message: "answer support judgment must record at most one verdict per (cluster, evidence) pair",
|
|
4761
|
+
subjectId: judgment.judgment_id,
|
|
4762
|
+
}));
|
|
4763
|
+
}
|
|
4764
|
+
else {
|
|
4765
|
+
covered.add(evidenceKey);
|
|
4766
|
+
judgedKeysByCluster.set(judgment.evidence_cluster_ref, covered);
|
|
4767
|
+
}
|
|
4768
|
+
}
|
|
4769
|
+
// B: supports enum (raw projection of supported count, NOT sufficiency).
|
|
4770
|
+
if (!SUPPORTS_VALUES.includes(judgment.supports)) {
|
|
4771
|
+
violations.push(violation({
|
|
4772
|
+
code: "invalid_enum",
|
|
4773
|
+
message: `invalid supports verdict ${judgment.supports}`,
|
|
4774
|
+
subjectId: judgment.judgment_id,
|
|
4775
|
+
}));
|
|
4776
|
+
}
|
|
4777
|
+
else if (judgment.supports === "supported") {
|
|
4778
|
+
supportedJudgmentCount += 1;
|
|
4779
|
+
}
|
|
4780
|
+
// C: rationale ref present (existence only, content not read).
|
|
4781
|
+
if (!judgment.rationale_ref || judgment.rationale_ref.trim().length === 0) {
|
|
4782
|
+
violations.push(violation({
|
|
4783
|
+
code: "missing_required_ref",
|
|
4784
|
+
message: "answer support judgment must cite a rationale_ref",
|
|
4785
|
+
subjectId: judgment.judgment_id,
|
|
4786
|
+
}));
|
|
4787
|
+
}
|
|
4788
|
+
}
|
|
4789
|
+
// D: convergent coverage (Codex #3) — every cited evidence_ref of a
|
|
4790
|
+
// convergent_source_evidence cluster must have a judgment row, so an
|
|
4791
|
+
// unfavorable/ambiguous ref cannot be silently omitted. Reuses the existing
|
|
4792
|
+
// missing_required_coverage failure kind (same "required coverage missing"
|
|
4793
|
+
// family). Non-convergent clusters do not trigger coverage.
|
|
4794
|
+
for (const cluster of ledger.evidence_clusters) {
|
|
4795
|
+
if (cluster.support_mode !== "convergent_source_evidence")
|
|
4796
|
+
continue;
|
|
4797
|
+
const covered = judgedKeysByCluster.get(cluster.evidence_cluster_id) ??
|
|
4798
|
+
new Set();
|
|
4799
|
+
for (const ref of cluster.evidence_refs) {
|
|
4800
|
+
if (!covered.has(evidenceRefKey(ref))) {
|
|
4801
|
+
violations.push(violation({
|
|
4802
|
+
code: "missing_required_coverage",
|
|
4803
|
+
message: "convergent cluster requires a judgment for every cited evidence ref",
|
|
4804
|
+
subjectId: cluster.evidence_cluster_id,
|
|
4805
|
+
}));
|
|
4806
|
+
}
|
|
4807
|
+
}
|
|
4808
|
+
}
|
|
4809
|
+
return {
|
|
4810
|
+
schema_version: "1",
|
|
4811
|
+
session_id: artifact.session_id,
|
|
4812
|
+
created_at: isoNow(),
|
|
4813
|
+
answer_support_judgment_ref: args.answerSupportJudgmentRef ?? null,
|
|
4814
|
+
answer_support_ledger_validation_ref: args.answerSupportLedgerValidationRef ?? null,
|
|
4815
|
+
validation_status: violations.length === 0 ? "valid" : "invalid",
|
|
4816
|
+
judgment_count: artifact.judgments.length,
|
|
4817
|
+
supported_judgment_count: supportedJudgmentCount,
|
|
4818
|
+
validation_results: violations.length === 0
|
|
4819
|
+
? ["answer_support_judgment_valid"]
|
|
4820
|
+
: ["answer_support_judgment_invalid"],
|
|
4821
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
4822
|
+
violations,
|
|
4823
|
+
};
|
|
4824
|
+
}
|
|
4825
|
+
export async function writeAnswerSupportJudgmentValidationArtifact(args) {
|
|
4826
|
+
const [answerSupportJudgment, answerSupportLedger, answerSupportLedgerValidation,] = await Promise.all([
|
|
4827
|
+
readYamlDocument(args.answerSupportJudgmentPath),
|
|
4828
|
+
readYamlDocument(args.answerSupportLedgerPath),
|
|
4829
|
+
readYamlDocument(args.answerSupportLedgerValidationPath),
|
|
4830
|
+
]);
|
|
4831
|
+
const validation = validateAnswerSupportJudgment({
|
|
4832
|
+
answerSupportJudgment,
|
|
4833
|
+
answerSupportJudgmentRef: args.answerSupportJudgmentPath,
|
|
4834
|
+
answerSupportLedger,
|
|
4835
|
+
answerSupportLedgerValidation,
|
|
4836
|
+
answerSupportLedgerValidationRef: args.answerSupportLedgerValidationPath,
|
|
4837
|
+
});
|
|
4838
|
+
await writeYamlDocument(args.outputPath, validation);
|
|
4839
|
+
return validation;
|
|
4840
|
+
}
|
|
3407
4841
|
export async function writeMaturationAnswerClaimsValidationArtifact(args) {
|
|
3408
4842
|
const [maturationAnswerClaims, answerSupportLedger, answerSupportLedgerValidation, maturationQuestionFrontier, maturationQuestionFrontierValidation,] = await Promise.all([
|
|
3409
4843
|
readYamlDocument(args.maturationAnswerClaimsPath),
|
|
@@ -3412,6 +4846,14 @@ export async function writeMaturationAnswerClaimsValidationArtifact(args) {
|
|
|
3412
4846
|
readYamlDocument(args.maturationQuestionFrontierPath),
|
|
3413
4847
|
readYamlDocument(args.maturationQuestionFrontierValidationPath),
|
|
3414
4848
|
]);
|
|
4849
|
+
const [answerSupportJudgment, answerSupportJudgmentValidation] = await Promise.all([
|
|
4850
|
+
args.answerSupportJudgmentPath
|
|
4851
|
+
? readYamlDocument(args.answerSupportJudgmentPath)
|
|
4852
|
+
: Promise.resolve(null),
|
|
4853
|
+
args.answerSupportJudgmentValidationPath
|
|
4854
|
+
? readYamlDocument(args.answerSupportJudgmentValidationPath)
|
|
4855
|
+
: Promise.resolve(null),
|
|
4856
|
+
]);
|
|
3415
4857
|
const validation = validateMaturationAnswerClaims({
|
|
3416
4858
|
maturationAnswerClaims,
|
|
3417
4859
|
maturationAnswerClaimsRef: args.maturationAnswerClaimsPath,
|
|
@@ -3421,6 +4863,10 @@ export async function writeMaturationAnswerClaimsValidationArtifact(args) {
|
|
|
3421
4863
|
maturationQuestionFrontier,
|
|
3422
4864
|
maturationQuestionFrontierValidation,
|
|
3423
4865
|
maturationQuestionFrontierValidationRef: args.maturationQuestionFrontierValidationPath,
|
|
4866
|
+
answerSupportJudgment,
|
|
4867
|
+
answerSupportJudgmentRef: args.answerSupportJudgmentPath ?? null,
|
|
4868
|
+
answerSupportJudgmentValidation,
|
|
4869
|
+
answerSupportJudgmentValidationRef: args.answerSupportJudgmentValidationPath ?? null,
|
|
3424
4870
|
});
|
|
3425
4871
|
await writeYamlDocument(args.outputPath, validation);
|
|
3426
4872
|
return validation;
|
|
@@ -3515,7 +4961,7 @@ export async function writeMaturationConvergenceLedgerValidationArtifact(args) {
|
|
|
3515
4961
|
return validation;
|
|
3516
4962
|
}
|
|
3517
4963
|
export async function writeMaturationContinuationDecisionArtifact(args) {
|
|
3518
|
-
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([
|
|
3519
4965
|
readYamlDocument(args.actionabilityMatrixPath),
|
|
3520
4966
|
readYamlDocument(args.maturationQuestionFrontierPath),
|
|
3521
4967
|
readYamlDocument(args.maturationClosureFrontierPath),
|
|
@@ -3523,6 +4969,8 @@ export async function writeMaturationContinuationDecisionArtifact(args) {
|
|
|
3523
4969
|
readYamlDocument(args.maturationAuthorityResponsePath),
|
|
3524
4970
|
readYamlDocument(args.ontologyExpansionValidationPath),
|
|
3525
4971
|
readYamlDocument(args.maturationConvergenceLedgerValidationPath),
|
|
4972
|
+
readYamlDocument(args.revisionProposalPath),
|
|
4973
|
+
readYamlDocument(args.revisionProposalValidationPath),
|
|
3526
4974
|
]);
|
|
3527
4975
|
const artifact = buildMaturationContinuationDecisionArtifact({
|
|
3528
4976
|
sessionId: args.sessionId,
|
|
@@ -3535,12 +4983,14 @@ export async function writeMaturationContinuationDecisionArtifact(args) {
|
|
|
3535
4983
|
maturationClosureFrontierValidation,
|
|
3536
4984
|
maturationAuthorityResponse,
|
|
3537
4985
|
ontologyExpansionValidation,
|
|
4986
|
+
revisionProposal,
|
|
4987
|
+
revisionProposalValidation,
|
|
3538
4988
|
});
|
|
3539
4989
|
await writeYamlDocument(args.outputPath, artifact);
|
|
3540
4990
|
return artifact;
|
|
3541
4991
|
}
|
|
3542
4992
|
export async function writeMaturationContinuationDecisionValidationArtifact(args) {
|
|
3543
|
-
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([
|
|
3544
4994
|
readYamlDocument(args.maturationContinuationDecisionPath),
|
|
3545
4995
|
readYamlDocument(args.actionabilityMatrixPath),
|
|
3546
4996
|
readYamlDocument(args.actionabilityMatrixValidationPath),
|
|
@@ -3550,6 +5000,8 @@ export async function writeMaturationContinuationDecisionValidationArtifact(args
|
|
|
3550
5000
|
readYamlDocument(args.maturationAuthorityResponseValidationPath),
|
|
3551
5001
|
readYamlDocument(args.ontologyExpansionValidationPath),
|
|
3552
5002
|
readYamlDocument(args.maturationConvergenceLedgerValidationPath),
|
|
5003
|
+
readYamlDocument(args.revisionProposalPath),
|
|
5004
|
+
readYamlDocument(args.revisionProposalValidationPath),
|
|
3553
5005
|
]);
|
|
3554
5006
|
const validation = validateMaturationContinuationDecision({
|
|
3555
5007
|
maturationContinuationDecision,
|
|
@@ -3569,6 +5021,9 @@ export async function writeMaturationContinuationDecisionValidationArtifact(args
|
|
|
3569
5021
|
ontologyExpansionValidationRef: args.ontologyExpansionValidationPath,
|
|
3570
5022
|
maturationConvergenceLedgerValidation,
|
|
3571
5023
|
maturationConvergenceLedgerValidationRef: args.maturationConvergenceLedgerValidationPath,
|
|
5024
|
+
revisionProposal,
|
|
5025
|
+
revisionProposalValidation,
|
|
5026
|
+
revisionProposalRef: args.revisionProposalPath,
|
|
3572
5027
|
});
|
|
3573
5028
|
await writeYamlDocument(args.outputPath, validation);
|
|
3574
5029
|
return validation;
|