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,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { parse as parseYaml
|
|
3
|
+
import { parse as parseYaml } from "yaml";
|
|
4
|
+
import { atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
|
|
5
|
+
import { assertObligation } from "./obligation-assertion.js";
|
|
4
6
|
const ONTOLOGY_DOMAIN_REQUIRED_CATEGORIES = [
|
|
5
7
|
{
|
|
6
8
|
category_id: "terminology_glossary",
|
|
@@ -70,10 +72,6 @@ async function readYamlDocumentIfPresent(filePath) {
|
|
|
70
72
|
throw error;
|
|
71
73
|
}
|
|
72
74
|
}
|
|
73
|
-
async function writeYamlDocument(filePath, value) {
|
|
74
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
75
|
-
await fs.writeFile(filePath, stringifyYaml(value), "utf8");
|
|
76
|
-
}
|
|
77
75
|
function selectedPurposeCandidate(args) {
|
|
78
76
|
const selectedId = args.sourcePurposeCandidatesValidation.selected_purpose_candidate_id;
|
|
79
77
|
return args.sourcePurposeCandidates.purpose_candidates.find((candidate) => candidate.purpose_candidate_id === selectedId) ?? null;
|
|
@@ -139,8 +137,19 @@ function limitationRefsForElement(args) {
|
|
|
139
137
|
return [];
|
|
140
138
|
if (args.row.limitation_refs.length > 0)
|
|
141
139
|
return args.row.limitation_refs;
|
|
142
|
-
if (purposeElementProjectsHandoffLimitation(args.element)
|
|
143
|
-
|
|
140
|
+
if (elementHasSourceEvidence(args) && (purposeElementProjectsHandoffLimitation(args.element) ||
|
|
141
|
+
// Defect-2: a frontier_required element that has source evidence but no
|
|
142
|
+
// available frontier (no accepted frontier refs) is structurally a handoff
|
|
143
|
+
// limitation — frontier deepening cannot happen, so its closure converts to a
|
|
144
|
+
// recorded limitation rather than collapsing to `missing` (which would force
|
|
145
|
+
// frontier_required + no_concrete_frontier = a permanent gate-throw deadlock on
|
|
146
|
+
// single-source input). Routing through the existing handoff-limitation path
|
|
147
|
+
// makes it limitation_backed -> limited_seed_possible, exactly like an element
|
|
148
|
+
// the author explicitly declared as projecting handoff_limitations. Evidence
|
|
149
|
+
// presence is the safety boundary: a frontier_required element WITHOUT evidence
|
|
150
|
+
// stays `missing` (a genuine hole the gate still refuses).
|
|
151
|
+
(args.element.closure_expectation === "frontier_required" &&
|
|
152
|
+
args.frontierRefs.length === 0))) {
|
|
144
153
|
return [`purpose_handoff_limitation:${slug(args.element.element_id)}`];
|
|
145
154
|
}
|
|
146
155
|
return [];
|
|
@@ -364,7 +373,7 @@ export function buildSeedAuthoringReadinessFromArtifacts(args) {
|
|
|
364
373
|
const frontierRefs = acceptedFrontierRefIds(args.sourceFrontierValidations ?? []);
|
|
365
374
|
const closureRows = purposeElements.map((element) => {
|
|
366
375
|
const row = materialRowsByElement.get(element.element_id) ?? null;
|
|
367
|
-
const limitationRefs = limitationRefsForElement({ element, row });
|
|
376
|
+
const limitationRefs = limitationRefsForElement({ element, row, frontierRefs });
|
|
368
377
|
return {
|
|
369
378
|
closure_row_id: `seed-authoring-closure:${slug(element.element_id)}`,
|
|
370
379
|
required_element_ref: element.element_id,
|
|
@@ -502,6 +511,44 @@ function normalizedRef(ref) {
|
|
|
502
511
|
export function validateSeedAuthoringReadiness(args) {
|
|
503
512
|
const violations = [];
|
|
504
513
|
const artifact = args.seedAuthoringReadiness;
|
|
514
|
+
// G(a) deferred-7 slice 4: record only the obligation this validator FULLY + UNCONDITIONALLY enforces.
|
|
515
|
+
// Stamped at the top so it fires on any input. This validator receives already-parsed objects and runs a
|
|
516
|
+
// recompute-and-compare against buildSeedAuthoringReadinessFromArtifacts(args), so the slice-3 null-read
|
|
517
|
+
// false-pass does NOT apply here. asserted_obligation_ids is in-memory-only telemetry (Stage 0 #145) on a
|
|
518
|
+
// reuse-hashed artifact (run.ts seed_authoring_readiness_validation_sha256), so stamping does not rotate reuse.
|
|
519
|
+
//
|
|
520
|
+
// RECORD 1:
|
|
521
|
+
// - validate_blocked_validation_gap_is_projection_not_semantic_decision → readiness_classification_mismatch
|
|
522
|
+
// (+ closure_row_invalid_state): blocked_validation_gap is recomputed deterministically from input
|
|
523
|
+
// validation status and compared UNCONDITIONALLY (single scalar), so it cannot be overridden by a
|
|
524
|
+
// hand-authored semantic decision (tests: "rejects tampered readiness classification" + "projects
|
|
525
|
+
// blocked_validation_gap before semantic readiness states"). Immune to both codex #150 edges below.
|
|
526
|
+
//
|
|
527
|
+
// PARKED 7 (ledger notes carry the codex-referenced detail):
|
|
528
|
+
// - validate_readiness_consumes_pre_seed_source_scout_validation_snapshot: codex #150 P2 — the three
|
|
529
|
+
// source_scout_pre_seed_identity_mismatch checks are GATED on sourceScoutPackValidationRef/Validation being
|
|
530
|
+
// present; an absent/null snapshot is handled by the validation-gap mechanism, not the identity check, so an
|
|
531
|
+
// unconditional top stamp would over-claim pre-seed consumption for gap-only executions.
|
|
532
|
+
// - validate_selected_purpose_required_elements_have_closure_rows: codex #150 P2 — expected and actual
|
|
533
|
+
// closure rows are keyed by required_element_ref in a Map, so duplicate element_ids (not rejected upstream)
|
|
534
|
+
// collapse and one closure row can satisfy multiple required elements → a missing duplicate is not caught.
|
|
535
|
+
// - validate_readiness_projection_uses_validated_input_refs_only: the closure-row validated_upstream_refs /
|
|
536
|
+
// llm_authority_refs projection fields are never compared; the only input-validation gate is the gap
|
|
537
|
+
// mechanism, whose binding is readiness_classification_mismatch (recorded above) — no distinct binding.
|
|
538
|
+
// - validate_actor_action_state_scout_rows_do_not_replace_purpose_required_elements: the boundary note is
|
|
539
|
+
// presence-only, and closure_row_dangling_required_element rejects foreign-ref rows but never verifies a
|
|
540
|
+
// row's material is purpose-derived rather than scout-derived (a scout row under a valid ref passes).
|
|
541
|
+
// - validate_frontier_required_preserves_exploration_budget_state_without_declaring_source_insufficiency:
|
|
542
|
+
// compound — source_sufficiency_state is recompute-compared, but exploration_budget_state is COPIED from
|
|
543
|
+
// the artifact (not compared), so the "preserves exploration_budget_state" half is unenforced.
|
|
544
|
+
// - validate_ontology_domain_required_category_rows_only_when_selected_purpose_closure_rows_resolve_to_
|
|
545
|
+
// domain_categories: asymmetric — only expected→actual presence (ontology_domain_category_missing) is
|
|
546
|
+
// checked; spurious/orphan category rows are never rejected, so the "only when" direction is unenforced.
|
|
547
|
+
// - validate_only_seed_ready_or_limited_seed_possible_allows_seed_authoring: DELEGATED to the sibling
|
|
548
|
+
// assertSeedAuthoringReadinessAllowsSeed gate; validateSeedAuthoringReadiness returns valid for
|
|
549
|
+
// frontier_required, so the allow-gate is not enforced by this function.
|
|
550
|
+
const assertedObligationIds = [];
|
|
551
|
+
assertObligation(assertedObligationIds, "validate_blocked_validation_gap_is_projection_not_semantic_decision");
|
|
505
552
|
if (artifact.session_id !== args.sourcePurposeCandidates.session_id) {
|
|
506
553
|
violations.push(violation({
|
|
507
554
|
code: "session_id_mismatch",
|
|
@@ -655,6 +702,7 @@ export function validateSeedAuthoringReadiness(args) {
|
|
|
655
702
|
validation_results: violations.length === 0
|
|
656
703
|
? ["seed_authoring_readiness_valid"]
|
|
657
704
|
: ["seed_authoring_readiness_invalid"],
|
|
705
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
658
706
|
violations,
|
|
659
707
|
};
|
|
660
708
|
}
|
|
@@ -47,8 +47,14 @@ const GOLDEN_FIXTURES = {
|
|
|
47
47
|
name_alternates: ["explainfixture", "fixtureexplanation"],
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
|
+
// Q1 presence is credited by a source binding (with a source_ref) whose
|
|
51
|
+
// seed_ref resolves to the fixture-service object — not by binding-id
|
|
52
|
+
// spelling (the old name_alternates were fit to the mock's
|
|
53
|
+
// `binding-fixture-source` id and missed descriptive live ids).
|
|
54
|
+
// name_alternates remain for the Q2 CQ linkage.
|
|
50
55
|
concept_key: "fixture-source-binding",
|
|
51
56
|
name_alternates: ["fixturesource", "bindingfixturesource", "sourcebinding"],
|
|
57
|
+
binding_target_alternates: ["fixtureservice"],
|
|
52
58
|
},
|
|
53
59
|
],
|
|
54
60
|
expected_cq: [
|
|
@@ -132,8 +138,14 @@ const GOLDEN_FIXTURES = {
|
|
|
132
138
|
name_alternates: ["checkoutbook", "checkout", "borrowbook", "loanbook"],
|
|
133
139
|
},
|
|
134
140
|
{
|
|
141
|
+
// Q1 presence is credited by a source binding (with a source_ref) whose
|
|
142
|
+
// seed_ref resolves to the loan-record object. The `loanrecord`
|
|
143
|
+
// name_alternate also matches the LoanRecord object NAME, so using it for
|
|
144
|
+
// Q1 would wrongly credit a seed that has the object but no binding; Q1
|
|
145
|
+
// here is binding-targeted only, and name_alternates remain for Q2.
|
|
135
146
|
concept_key: "loan-record-binding",
|
|
136
147
|
name_alternates: ["loanrecord", "loanbinding"],
|
|
148
|
+
binding_target_alternates: ["loanrecord"],
|
|
137
149
|
},
|
|
138
150
|
],
|
|
139
151
|
expected_cq: [
|
|
@@ -219,7 +231,66 @@ function seedNameRows(seed) {
|
|
|
219
231
|
}
|
|
220
232
|
return rows;
|
|
221
233
|
}
|
|
222
|
-
|
|
234
|
+
/**
|
|
235
|
+
* The object_type ids whose object NAME matches a binding-target alternate. The
|
|
236
|
+
* target object is identified by its NAME (model-agnostic on the id), and the
|
|
237
|
+
* binding is then tied to it by EXACT seed_ref == object_type_id (below), so no
|
|
238
|
+
* substring matching ever runs over model-chosen ids. Only `semantic_layer.
|
|
239
|
+
* object_types` are scanned — a source binding must back an object type, not a
|
|
240
|
+
* concept/action/actor (which would let the fixture pass without the backed
|
|
241
|
+
* object it measures).
|
|
242
|
+
*/
|
|
243
|
+
function targetObjectTypeIds(objectTypeRows, targetAlternates) {
|
|
244
|
+
const ids = new Set();
|
|
245
|
+
for (const row of objectTypeRows) {
|
|
246
|
+
const name = normalizeName(stringField(row, "name"));
|
|
247
|
+
if (name.length === 0)
|
|
248
|
+
continue;
|
|
249
|
+
if (targetAlternates.some((alternate) => name.includes(normalizeName(alternate)))) {
|
|
250
|
+
const id = stringField(row, "object_type_id").trim();
|
|
251
|
+
if (id.length > 0)
|
|
252
|
+
ids.add(id);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return ids;
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Whether a source binding satisfies a binding-target concept: it must name a
|
|
259
|
+
* backing source (`source_ref` present) and target the right object — its
|
|
260
|
+
* (trimmed) `seed_ref` is EXACTLY one of the target object type ids. Id equality
|
|
261
|
+
* (not substring) means a descriptive binding id, an opaque object id, incidental
|
|
262
|
+
* whitespace, or a mis-named object cannot leak a false match.
|
|
263
|
+
*/
|
|
264
|
+
function bindingTargetsConcept(binding, targetObjectIds) {
|
|
265
|
+
if (stringField(binding, "source_ref").trim().length === 0)
|
|
266
|
+
return false;
|
|
267
|
+
return targetObjectIds.has(stringField(binding, "seed_ref").trim());
|
|
268
|
+
}
|
|
269
|
+
function conceptMatch(expected, rows, sourceBindings, objectTypeRows) {
|
|
270
|
+
// A binding-target concept's PRESENCE (Q1) is credited only by a source
|
|
271
|
+
// binding (with a source_ref) whose seed_ref is exactly the id of an object
|
|
272
|
+
// type the fixture names. `name_alternates` are NOT used for Q1 presence here —
|
|
273
|
+
// they would also match the target object's own NAME and credit the binding
|
|
274
|
+
// concept for a seed that has the object but no binding; they remain for the Q2
|
|
275
|
+
// CQ↔concept linkage.
|
|
276
|
+
if (expected.binding_target_alternates) {
|
|
277
|
+
const targetIds = targetObjectTypeIds(objectTypeRows, expected.binding_target_alternates);
|
|
278
|
+
for (const binding of sourceBindings) {
|
|
279
|
+
if (bindingTargetsConcept(binding, targetIds)) {
|
|
280
|
+
return {
|
|
281
|
+
concept_key: expected.concept_key,
|
|
282
|
+
matched_name: [
|
|
283
|
+
stringField(binding, "binding_id"),
|
|
284
|
+
stringField(binding, "seed_ref"),
|
|
285
|
+
]
|
|
286
|
+
.filter((value) => value.length > 0)
|
|
287
|
+
.join(" "),
|
|
288
|
+
seed_family: "data_binding_layer.source_bindings",
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
223
294
|
for (const row of rows) {
|
|
224
295
|
const normalized = normalizeName(row.name);
|
|
225
296
|
for (const alternate of expected.name_alternates) {
|
|
@@ -236,18 +307,29 @@ function conceptMatch(expected, rows) {
|
|
|
236
307
|
}
|
|
237
308
|
/**
|
|
238
309
|
* Explicit no-call exemptions: the only LLM-owned units with a runtime path
|
|
239
|
-
* that completes without any LLM call (a not-required purpose confirmation,
|
|
240
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
310
|
+
* that completes without any LLM call (a not-required purpose confirmation, or
|
|
311
|
+
* maturation units the runtime authors as empty projections — when the question
|
|
312
|
+
* frontier is empty, or, for the answer-support judge, when the ledger has no
|
|
313
|
+
* convergent_source_evidence cluster). Every other completed non-runtime unit
|
|
314
|
+
* must carry telemetry — reject-by-default, so a newly added authored unit
|
|
315
|
+
* cannot silently skip measurement provenance.
|
|
244
316
|
*/
|
|
245
317
|
const NO_CALL_EXEMPT_UNIT_IDS = new Set([
|
|
246
318
|
"purpose_confirmation",
|
|
247
319
|
"maturation_question_frontier",
|
|
248
320
|
"maturation_closure_frontier",
|
|
321
|
+
"answer_support_judgment",
|
|
249
322
|
"maturation_answer_claims",
|
|
250
323
|
"ontology_expansion",
|
|
324
|
+
// leaf-read is a conditional LLM-touch: when no spreadsheet region triggers a read (no spreadsheet
|
|
325
|
+
// source, or structure already complete), the stage completes with no LLM call but still writes its
|
|
326
|
+
// census artifact (the R9 honest-signal). A leaf-read that DID call records leaf_read telemetry; the
|
|
327
|
+
// census itself records attempts/failures, so an exempt no-call completion still carries provenance.
|
|
328
|
+
"leaf_read",
|
|
329
|
+
// Layer-2 semantic_map (W3 review onto issue-003): same conditional-LLM-touch shape as leaf_read —
|
|
330
|
+
// a capability-present run with no evaluatable observation (or a fully preflight-capped one)
|
|
331
|
+
// completes with ZERO calls but still writes its census/sidecar (the provenance surface).
|
|
332
|
+
"semantic_map",
|
|
251
333
|
]);
|
|
252
334
|
/**
|
|
253
335
|
* Source-field rejection: completed LLM units must carry the telemetry
|
|
@@ -327,10 +409,12 @@ export function evaluateReconstructGoldenQualityGate(args) {
|
|
|
327
409
|
}
|
|
328
410
|
// Q1 — expected-concept recall over the authored seed.
|
|
329
411
|
const nameRows = seedNameRows(args.ontologySeed);
|
|
412
|
+
const sourceBindings = recordRows(args.ontologySeed.data_binding_layer, "source_bindings");
|
|
413
|
+
const objectTypeRows = recordRows(args.ontologySeed.semantic_layer, "object_types");
|
|
330
414
|
const matches = [];
|
|
331
415
|
const missing = [];
|
|
332
416
|
for (const expected of spec.expected_concepts) {
|
|
333
|
-
const match = conceptMatch(expected, nameRows);
|
|
417
|
+
const match = conceptMatch(expected, nameRows, sourceBindings, objectTypeRows);
|
|
334
418
|
if (match)
|
|
335
419
|
matches.push(match);
|
|
336
420
|
else
|
|
@@ -357,9 +441,30 @@ export function evaluateReconstructGoldenQualityGate(args) {
|
|
|
357
441
|
if (!concept) {
|
|
358
442
|
throw new Error(`Fixture ${spec.fixture_id} expected_cq ${expected.cq_key} links unknown concept ${expected.linked_concept_key}`);
|
|
359
443
|
}
|
|
444
|
+
// A binding-target concept's CQ is supported only when a question explicitly
|
|
445
|
+
// references one of the run's TARGETING bindings BY ID (exact equality on a
|
|
446
|
+
// claim/seed ref). The object name or seed_ref must NOT credit it — a question
|
|
447
|
+
// merely about the object is not a question about its backing source — and an
|
|
448
|
+
// id SUBSTRING must not (a short binding_id like `b1` would collide with an
|
|
449
|
+
// unrelated ref `object-b10`). With no targeting binding the set is empty, so
|
|
450
|
+
// the binding CQ cannot be supported — the missing-binding case Q1 catches.
|
|
451
|
+
// Non-binding concepts link via name_alternates over the question text/refs.
|
|
360
452
|
const question = args.competencyQuestions.questions.find((candidate) => {
|
|
361
453
|
if (usedQuestionIds.has(candidate.question_id))
|
|
362
454
|
return false;
|
|
455
|
+
if (concept.binding_target_alternates) {
|
|
456
|
+
const targetIds = targetObjectTypeIds(objectTypeRows, concept.binding_target_alternates);
|
|
457
|
+
const bindingIds = new Set(sourceBindings
|
|
458
|
+
.filter((binding) => bindingTargetsConcept(binding, targetIds))
|
|
459
|
+
.map((binding) => stringField(binding, "binding_id").trim())
|
|
460
|
+
.filter((id) => id.length > 0));
|
|
461
|
+
if (bindingIds.size === 0)
|
|
462
|
+
return false;
|
|
463
|
+
return [
|
|
464
|
+
...(candidate.linked_claim_ids ?? []),
|
|
465
|
+
...(candidate.seed_ref_refs ?? []),
|
|
466
|
+
].some((ref) => bindingIds.has(ref.trim()));
|
|
467
|
+
}
|
|
363
468
|
const haystack = normalizeName([
|
|
364
469
|
candidate.question_id,
|
|
365
470
|
candidate.question,
|
|
@@ -1,18 +1,16 @@
|
|
|
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 { assertArrayField, atomicWriteYamlDocument as writeYamlDocument } from "../artifact-io.js";
|
|
5
6
|
import { sourceSafetyRowIdForObservation } from "./source-safety-validation.js";
|
|
7
|
+
import { assertObligation } from "./obligation-assertion.js";
|
|
6
8
|
function isoNow() {
|
|
7
9
|
return new Date().toISOString();
|
|
8
10
|
}
|
|
9
11
|
async function readYamlDocument(filePath) {
|
|
10
12
|
return parseYaml(await fs.readFile(filePath, "utf8"));
|
|
11
13
|
}
|
|
12
|
-
async function writeYamlDocument(filePath, value) {
|
|
13
|
-
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
14
|
-
await fs.writeFile(filePath, stringifyYaml(value), "utf8");
|
|
15
|
-
}
|
|
16
14
|
function stableJson(value) {
|
|
17
15
|
if (Array.isArray(value)) {
|
|
18
16
|
return `[${value.map(stableJson).join(",")}]`;
|
|
@@ -70,6 +68,7 @@ function samePathRef(left, right) {
|
|
|
70
68
|
return path.resolve(left) === path.resolve(right);
|
|
71
69
|
}
|
|
72
70
|
function observationsBySourceRef(observations) {
|
|
71
|
+
assertArrayField(observations.observations, "source-observations", "observations");
|
|
73
72
|
return new Map(observations.observations.map((observation) => [
|
|
74
73
|
path.resolve(observation.source_ref),
|
|
75
74
|
observation,
|
|
@@ -191,7 +190,20 @@ export function buildSourceObservationDeltaArtifact(args) {
|
|
|
191
190
|
};
|
|
192
191
|
}
|
|
193
192
|
export function validateSourceObservationDelta(args) {
|
|
193
|
+
assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
|
|
194
|
+
assertArrayField(args.delta.delta_rows, "source-observation-delta", "delta_rows");
|
|
195
|
+
assertArrayField(args.delta.accepted_frontier_ref_ids, "source-observation-delta", "accepted_frontier_ref_ids");
|
|
196
|
+
assertArrayField(args.delta.added_observation_ids, "source-observation-delta", "added_observation_ids");
|
|
194
197
|
const violations = [];
|
|
198
|
+
// G(a) slice 5: record the four delta obligations with a distinct enforcement region, before any
|
|
199
|
+
// per-row loop so they are proven wired on a zero-row delta. validate_delta_frontier_kind_is_supported
|
|
200
|
+
// stays parked (ledger audit note): normalizeFrontierForDelta checks kind-vs-artifact CONSISTENCY,
|
|
201
|
+
// not that the kind value itself is in a supported set, so an unsupported kind can fall through.
|
|
202
|
+
const assertedObligationIds = [];
|
|
203
|
+
assertObligation(assertedObligationIds, "validate_delta_observation_refs_exist_in_source_observations");
|
|
204
|
+
assertObligation(assertedObligationIds, "validate_delta_rows_match_accepted_frontier_refs");
|
|
205
|
+
assertObligation(assertedObligationIds, "validate_delta_rows_preserve_observation_batch_id_and_triggering_frontier_validation_ref");
|
|
206
|
+
assertObligation(assertedObligationIds, "validate_delta_source_ref_material_kind_and_observation_hash_match_observed_content");
|
|
195
207
|
if (args.delta.schema_version !== "1") {
|
|
196
208
|
violations.push(violation({
|
|
197
209
|
code: "schema_shape_invalid",
|
|
@@ -378,11 +390,23 @@ export function validateSourceObservationDelta(args) {
|
|
|
378
390
|
validation_results: violations.length === 0
|
|
379
391
|
? ["source_observation_delta_valid"]
|
|
380
392
|
: ["source_observation_delta_invalid"],
|
|
393
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
381
394
|
violations,
|
|
382
395
|
};
|
|
383
396
|
}
|
|
384
397
|
export function validateSourceObservationReentry(args) {
|
|
398
|
+
assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
|
|
399
|
+
assertArrayField(args.sourceSafetyLedger.safety_rows, "source-safety-ledger", "safety_rows");
|
|
400
|
+
assertArrayField(args.delta.added_observation_ids, "source-observation-delta", "added_observation_ids");
|
|
385
401
|
const violations = [];
|
|
402
|
+
// G(a) slice 5: record the four re-entry obligations. R1/R4 are top-level gate checks; R2/R3 are
|
|
403
|
+
// per-observation, so all four are stamped before the loop to be proven wired on a zero-observation
|
|
404
|
+
// delta. Each was audited to a distinct enforcement region (no laundering).
|
|
405
|
+
const assertedObligationIds = [];
|
|
406
|
+
assertObligation(assertedObligationIds, "validate_delta_validation_passed_before_prompt_reentry");
|
|
407
|
+
assertObligation(assertedObligationIds, "validate_each_delta_observation_exists_in_source_observations");
|
|
408
|
+
assertObligation(assertedObligationIds, "validate_each_delta_observation_has_exact_prompt_context_source_safety_row");
|
|
409
|
+
assertObligation(assertedObligationIds, "validate_source_safety_validation_passed_before_prompt_reentry");
|
|
386
410
|
if (args.delta.schema_version !== "1") {
|
|
387
411
|
violations.push(reentryViolation({
|
|
388
412
|
code: "schema_shape_invalid",
|
|
@@ -458,11 +482,34 @@ export function validateSourceObservationReentry(args) {
|
|
|
458
482
|
validation_results: violations.length === 0
|
|
459
483
|
? ["source_observation_reentry_valid"]
|
|
460
484
|
: ["source_observation_reentry_invalid"],
|
|
485
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
461
486
|
violations,
|
|
462
487
|
};
|
|
463
488
|
}
|
|
464
489
|
export async function validateSourceObservationLineageIndex(args) {
|
|
490
|
+
assertArrayField(args.sourceObservations.observations, "source-observations", "observations");
|
|
491
|
+
assertArrayField(args.lineageIndex.lineage_rows, "source-observation-lineage-index", "lineage_rows");
|
|
465
492
|
const violations = [];
|
|
493
|
+
// G(a) deferred-7 slice 3: record only the uniqueness obligation this validator fully enforces. Stamped
|
|
494
|
+
// before the per-row loop so it fires on zero rows. asserted_obligation_ids is in-memory-only telemetry
|
|
495
|
+
// (Stage 0 #145) on a reuse-hashed + scout-captured artifact, so stamping does not rotate reuse.
|
|
496
|
+
// validate_unique_session_level_lineage_row_ids → duplicate_id (the `seen` set below rejects any repeated
|
|
497
|
+
// lineage_row_id, including a repeated blank/absent id).
|
|
498
|
+
//
|
|
499
|
+
// PARKED (codex #149 R1, all false-pass edges of this gap-prone lineage validator):
|
|
500
|
+
// - require_each_lineage_row_{delta,reentry}_validation_to_be_valid, validate_each_lineage_row_delta_ref_
|
|
501
|
+
// is_readable_and_session_matching, validate_lineage_added_observation_ids_match_delta_added_observation_
|
|
502
|
+
// ids: a ref pointing at an EXISTING EMPTY/null YAML makes readYamlDocument return null WITHOUT throwing,
|
|
503
|
+
// so the catch (..._missing) never fires AND the `if (delta)` / `if (deltaValidation)` / `if (reentry
|
|
504
|
+
// Validation)` guards skip every check — the row passes valid. Not fully enforced until non-object reads
|
|
505
|
+
// are rejected.
|
|
506
|
+
// - validate_each_lineage_added_observation_exists_in_source_observations: checks membership in the PASSED
|
|
507
|
+
// sourceObservations object only; it is never bound to the delta/re-entry chain's source_observations_ref
|
|
508
|
+
// or session, so a wrong/stale source-observations file with the same ids passes (caller-supplied authority).
|
|
509
|
+
// - validate_each_lineage_added_observation_was_reentered_by_its_validation: shares the
|
|
510
|
+
// lineage_added_observation_mismatch code with the added-ids-match obligation (no distinct binding; slice-21).
|
|
511
|
+
const assertedObligationIds = [];
|
|
512
|
+
assertObligation(assertedObligationIds, "validate_unique_session_level_lineage_row_ids");
|
|
466
513
|
const seen = new Set();
|
|
467
514
|
const observationsById = new Map(args.sourceObservations.observations.map((observation) => [observation.observation_id, observation]));
|
|
468
515
|
const addedObservationIds = new Set();
|
|
@@ -670,6 +717,7 @@ export async function validateSourceObservationLineageIndex(args) {
|
|
|
670
717
|
validation_results: violations.length === 0
|
|
671
718
|
? ["source_observation_lineage_index_valid"]
|
|
672
719
|
: ["source_observation_lineage_index_invalid"],
|
|
720
|
+
asserted_obligation_ids: assertedObligationIds,
|
|
673
721
|
violations,
|
|
674
722
|
};
|
|
675
723
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { inventoryHasInspectedStructure, SPREADSHEET_CAPTURE_TRUNCATED_PHRASE, SPREADSHEET_MACRO_PRESENT_PHRASE, SPREADSHEET_OBSERVER_ADAPTER_ID, VALIDATION_MEMBER_CHAR_CAP, VALIDATION_MEMBER_COUNT_CAP, } from "../spreadsheet-structure-observer.js";
|
|
2
|
+
import { validateComprehensionArtifact, } from "./comprehension-artifact.js";
|
|
1
3
|
const PROHIBITED_STRUCTURAL_KEYS = new Set([
|
|
2
4
|
"aggregate_root",
|
|
3
5
|
"business_entity",
|
|
@@ -31,6 +33,142 @@ function collectObjectKeys(value, keys = []) {
|
|
|
31
33
|
}
|
|
32
34
|
return keys;
|
|
33
35
|
}
|
|
36
|
+
/** Lowercase 64-char hex (a raw-byte sha256). */
|
|
37
|
+
const CONTENT_SHA256_PATTERN = /^[0-9a-f]{64}$/;
|
|
38
|
+
/**
|
|
39
|
+
* P6 spreadsheet honesty/provenance assertions — the positive complement of the
|
|
40
|
+
* prohibition checks above: what a spreadsheet observation MUST honestly disclose.
|
|
41
|
+
* Runs only for spreadsheet observations; CRUCIALLY it exempts the legitimate
|
|
42
|
+
* UNSUPPORTED states the observer deliberately emits (empty-csv placeholder,
|
|
43
|
+
* oversized/unreadable with an empty hash) so the gate never converts an honest
|
|
44
|
+
* "nothing to inspect / could not read" inventory into a hard crash (the validator
|
|
45
|
+
* throws inside the builder, before the materialize loop's graceful skip-demotion).
|
|
46
|
+
*/
|
|
47
|
+
function validateSpreadsheetObservationHonesty(observation, violations) {
|
|
48
|
+
const inventory = observation.structural_data.workbook_inventory;
|
|
49
|
+
// An array is `typeof "object"` too; reject it like the prompt-projection recompute
|
|
50
|
+
// does — this validator is also the boundary for persisted/replayed observations,
|
|
51
|
+
// so a malformed `workbook_inventory: []` must not pass as a valid inventory.
|
|
52
|
+
if (!inventory || typeof inventory !== "object" || Array.isArray(inventory)) {
|
|
53
|
+
violations.push("spreadsheet observation must carry a workbook_inventory in structural_data");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// `unsupported_reason` is three-valued: null (supported), a non-empty string
|
|
57
|
+
// (genuinely unsupported), or a present-but-BLANK string (incoherent). A blank
|
|
58
|
+
// reason would skip the supported hash check yet not demote downstream
|
|
59
|
+
// (spreadsheetUnsupportedReason ignores blanks), admitting a no-evidence workbook —
|
|
60
|
+
// reject it explicitly so the honest disclosure is never empty.
|
|
61
|
+
const reason = inventory.unsupported_reason;
|
|
62
|
+
if (typeof reason === "string" && reason.trim() === "") {
|
|
63
|
+
violations.push("unsupported_reason must not be blank");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const supported = reason == null;
|
|
67
|
+
if (supported) {
|
|
68
|
+
// B (provenance anchor): a SUPPORTED workbook (bytes actually read) must carry a
|
|
69
|
+
// well-formed raw-byte content hash at the TOP level — the field the source-scout-
|
|
70
|
+
// pack provenance consumer binds to (materialize-preparation surfaces it there).
|
|
71
|
+
// It is a presence/format check on the raw-byte content hash. An UNSUPPORTED
|
|
72
|
+
// inventory may legitimately carry an empty hash (oversized/unreadable: bytes
|
|
73
|
+
// never read) — its unsupported_reason is the honest disclosure.
|
|
74
|
+
const sha = observation.structural_data.content_sha256;
|
|
75
|
+
if (typeof sha !== "string" || !CONTENT_SHA256_PATTERN.test(sha)) {
|
|
76
|
+
violations.push("content_sha256_missing");
|
|
77
|
+
}
|
|
78
|
+
else if (sha !== inventory.content_sha256) {
|
|
79
|
+
// The top-level hash anchors source-scout provenance; the nested inventory hash
|
|
80
|
+
// is projected into prompts. A corrupted/replayed envelope where they disagree
|
|
81
|
+
// would let the two name different raw bytes — assert they match.
|
|
82
|
+
violations.push("content_sha256 disagrees with workbook_inventory hash");
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
else if (inventoryHasInspectedStructure(inventory)) {
|
|
86
|
+
// C (unsupported<->empty coherence): an unsupported inventory must not claim any
|
|
87
|
+
// inspected structure across the full inventory surface, not just `sheets`.
|
|
88
|
+
violations.push("unsupported spreadsheet inventory must not claim inspected structure");
|
|
89
|
+
}
|
|
90
|
+
// D (truncation/macro honesty): when the inventory flags partial capture or macro
|
|
91
|
+
// presence, the prompt-visible summary MUST disclose it with the fixed phrase the
|
|
92
|
+
// producer emits (assert + emit bound to the same literal so they cannot drift).
|
|
93
|
+
if (inventory.capture_truncated &&
|
|
94
|
+
!observation.summary.includes(SPREADSHEET_CAPTURE_TRUNCATED_PHRASE)) {
|
|
95
|
+
violations.push("capture_truncated not disclosed in observation summary");
|
|
96
|
+
}
|
|
97
|
+
if (inventory.macro_present &&
|
|
98
|
+
!observation.summary.includes(SPREADSHEET_MACRO_PRESENT_PHRASE)) {
|
|
99
|
+
violations.push("macro_present not disclosed in observation summary");
|
|
100
|
+
}
|
|
101
|
+
// E (design-C value-aware bound): the ONLY value-bearing field is
|
|
102
|
+
// data_validations[].members — the declared type=list enum labels. This enforces BOUNDS +
|
|
103
|
+
// INTERNAL CONSISTENCY only: any entry carrying members must be validation_type === "list"
|
|
104
|
+
// with count <= VALIDATION_MEMBER_COUNT_CAP and each member <= VALIDATION_MEMBER_CHAR_CAP.
|
|
105
|
+
// formula1 PROVENANCE (that members came from an inline literal, not observed cells) is an
|
|
106
|
+
// emission-time guarantee in the observer, NOT replay-verifiable here — a forged
|
|
107
|
+
// members+type=list pair is outside this honesty model (documented limitation).
|
|
108
|
+
// Codex round3 #5: a replayed / host-supplied artifact may carry a non-array (or missing)
|
|
109
|
+
// data_validations; guard before iterating so a malformed observation degrades to valid:false
|
|
110
|
+
// rather than throwing "not iterable" out of the boundary check.
|
|
111
|
+
if (!Array.isArray(inventory.data_validations)) {
|
|
112
|
+
violations.push("data_validations is missing or not an array");
|
|
113
|
+
}
|
|
114
|
+
for (const dv of Array.isArray(inventory.data_validations)
|
|
115
|
+
? inventory.data_validations
|
|
116
|
+
: []) {
|
|
117
|
+
if (dv.members === undefined)
|
|
118
|
+
continue;
|
|
119
|
+
// Replayed / host-supplied artifacts are untyped despite the cast: a non-string member would
|
|
120
|
+
// make the length bounds silently pass (members:[123] → m.length undefined) or throw
|
|
121
|
+
// (members:"abc" → no .some). Reject non-string-array members before the bounds (Codex #3).
|
|
122
|
+
if (!Array.isArray(dv.members) ||
|
|
123
|
+
dv.members.some((m) => typeof m !== "string")) {
|
|
124
|
+
violations.push("data_validation members must be an array of strings");
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (dv.validation_type !== "list") {
|
|
128
|
+
violations.push("data_validation members present but validation_type is not 'list'");
|
|
129
|
+
}
|
|
130
|
+
if (dv.members.length > VALIDATION_MEMBER_COUNT_CAP) {
|
|
131
|
+
violations.push("data_validation members exceed VALIDATION_MEMBER_COUNT_CAP");
|
|
132
|
+
}
|
|
133
|
+
if (dv.members.some((m) => m.length > VALIDATION_MEMBER_CHAR_CAP)) {
|
|
134
|
+
violations.push("data_validation member exceeds VALIDATION_MEMBER_CHAR_CAP");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// E2 (P1-C1 value-tile safety): segmented_value_tiles is aggregate-only — type/shape/format
|
|
138
|
+
// counts, capped distinct COUNTS (never values), lower-bound flags, row anchors. Its only
|
|
139
|
+
// string-bearing fields are format-identities (the SANITIZED display-format grammar). A quoted
|
|
140
|
+
// literal there would mean a domain literal (e.g. "USD", "Customer:") leaked past
|
|
141
|
+
// normalizeFormatCode — reject it so a forged/replayed artifact cannot smuggle source text into
|
|
142
|
+
// a prompt-visible tile. (Bounds are structural: caps live in value_tile_config.)
|
|
143
|
+
const valueTiles = inventory.segmented_value_tiles;
|
|
144
|
+
if (valueTiles !== undefined) {
|
|
145
|
+
if (!Array.isArray(valueTiles)) {
|
|
146
|
+
violations.push("segmented_value_tiles must be an array when present");
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
let unsanitized = false;
|
|
150
|
+
for (const sheet of valueTiles) {
|
|
151
|
+
for (const col of sheet.columns ?? []) {
|
|
152
|
+
for (const seg of col.segments ?? []) {
|
|
153
|
+
if (Object.keys(seg.format_counts ?? {}).some((fid) => fid.includes('"'))) {
|
|
154
|
+
unsanitized = true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const note of col.intra_tile_notes ?? []) {
|
|
158
|
+
if (note.boundary_kind === "display_format" &&
|
|
159
|
+
((typeof note.prev_shape === "string" && note.prev_shape.includes('"')) ||
|
|
160
|
+
(typeof note.new_shape === "string" && note.new_shape.includes('"')))) {
|
|
161
|
+
unsanitized = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
if (unsanitized) {
|
|
167
|
+
violations.push("value-tile format-identity must be sanitized (no quoted literal)");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
34
172
|
export function validateSourceObservationBoundary(observation) {
|
|
35
173
|
const violations = [];
|
|
36
174
|
if (!observation.observation_id.trim()) {
|
|
@@ -46,6 +184,24 @@ export function validateSourceObservationBoundary(observation) {
|
|
|
46
184
|
!observation.observation_batch_id.trim()) {
|
|
47
185
|
violations.push("observation_batch_id must not be blank when present");
|
|
48
186
|
}
|
|
187
|
+
// Defect-3 basis A is forgery-resistant at the artifact boundary (covers
|
|
188
|
+
// persisted/replayed/host-supplied observations, not just freshly produced ones):
|
|
189
|
+
if ("is_runtime_target_source" in observation) {
|
|
190
|
+
// (a) fail-closed type: downstream source-safety treats ONLY literal boolean
|
|
191
|
+
// true as Basis A, so a non-boolean must not pass the boundary silently.
|
|
192
|
+
if (observation.is_runtime_target_source !== undefined &&
|
|
193
|
+
typeof observation.is_runtime_target_source !== "boolean") {
|
|
194
|
+
violations.push("is_runtime_target_source must be a boolean when present");
|
|
195
|
+
}
|
|
196
|
+
// (b) mutual exclusion: a runtime-target source is the user-provided initial
|
|
197
|
+
// target and is never a frontier re-entry. Rejecting target+trigger blocks a
|
|
198
|
+
// replayed/forged frontier observation from claiming runtime-target provenance.
|
|
199
|
+
if (observation.is_runtime_target_source === true &&
|
|
200
|
+
typeof observation.triggering_frontier_validation_ref === "string" &&
|
|
201
|
+
observation.triggering_frontier_validation_ref.trim()) {
|
|
202
|
+
violations.push("is_runtime_target_source must not be true on a frontier re-entry observation (triggering_frontier_validation_ref present)");
|
|
203
|
+
}
|
|
204
|
+
}
|
|
49
205
|
if (!observation.adapter_id.trim()) {
|
|
50
206
|
violations.push("adapter_id is required");
|
|
51
207
|
}
|
|
@@ -65,6 +221,16 @@ export function validateSourceObservationBoundary(observation) {
|
|
|
65
221
|
violations.push(`summary contains prohibited ontology interpretation: ${label}`);
|
|
66
222
|
}
|
|
67
223
|
}
|
|
224
|
+
if (observation.adapter_id === SPREADSHEET_OBSERVER_ADAPTER_ID) {
|
|
225
|
+
validateSpreadsheetObservationHonesty(observation, violations);
|
|
226
|
+
}
|
|
227
|
+
// P1-C1 §5.7: validate the companion ComprehensionArtifact when present (construction + replay).
|
|
228
|
+
// Completeness is fail-closed (a silently-missing baseline field is a violation), so an invalid
|
|
229
|
+
// contract throws at construction (the materialize loop) and is rejected on replay.
|
|
230
|
+
const comprehensionArtifact = observation.structural_data.comprehension_artifact;
|
|
231
|
+
if (comprehensionArtifact !== undefined) {
|
|
232
|
+
validateComprehensionArtifact(comprehensionArtifact, violations);
|
|
233
|
+
}
|
|
68
234
|
return {
|
|
69
235
|
valid: violations.length === 0,
|
|
70
236
|
violations,
|