Skip to content

Commit 4f83def

Browse files
committed
feat(evidence): ADR-0005 WP-2 Set 0 provider contract and conformance suite.
Adds P/D/F qualification types, provider API, golden exemplars E01–E10, provider-json reference provider, presentation primitives, and pnpm test-provider gate.
1 parent 9bbfec4 commit 4f83def

47 files changed

Lines changed: 1817 additions & 1 deletion

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"version-packages": "changeset version",
2525
"validate:publish": "node scripts/check-no-workspace-refs.mjs",
2626
"typecheck:examples": "tsc -p tsconfig.examples.json",
27-
"release": "pnpm validate:publish && changeset publish"
27+
"release": "pnpm validate:publish && changeset publish",
28+
"test-provider": "pnpm --filter @loop-engine/provider-conformance run test-provider"
2829
},
2930
"devDependencies": {
3031
"@changesets/changelog-github": "^0.5.2",
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
import { describe, expect, it } from "vitest";
3+
import {
4+
definitionMeetsMinimum,
5+
provenanceMeetsMinimum,
6+
} from "../qualification.js";
7+
import { validateEvidenceSnapshotProposed } from "../validate-provider-snapshot.js";
8+
import { LAUNCH_DECISION_RECORD_VERSIONS } from "../types.js";
9+
10+
const baseSnapshot = {
11+
snapshotKey: "metric-total",
12+
sourceSystem: "warehouse",
13+
sourceRef: "semantic://revenue_total",
14+
capturedAt: "2026-07-07T12:00:00.000Z",
15+
capturedBy: "provider-json",
16+
values: { amount: 12500, currency: "USD" },
17+
versions: LAUNCH_DECISION_RECORD_VERSIONS,
18+
qualification: {
19+
provenance: "system-attested",
20+
definition: "governed",
21+
freshness: {
22+
applicable: "applicable",
23+
snapshotFrozenAt: "2026-07-07T12:00:00.000Z",
24+
valueAsOf: "2026-07-07T11:58:00.000Z",
25+
bindingClock: "valueAsOf",
26+
freshnessVerdict: "fresh",
27+
},
28+
},
29+
};
30+
31+
describe("qualification vector ordering", () => {
32+
it("compares provenance rungs with ≥ semantics", () => {
33+
expect(provenanceMeetsMinimum("origin-attested", "system-attested")).toBe(true);
34+
expect(provenanceMeetsMinimum("unknown", "system-attested")).toBe(false);
35+
});
36+
37+
it("skips definition check when axis is not-applicable", () => {
38+
expect(definitionMeetsMinimum("not-applicable", "governed")).toBe(true);
39+
expect(definitionMeetsMinimum("governed", "not-applicable")).toBe(true);
40+
});
41+
});
42+
43+
describe("validateEvidenceSnapshotProposed (Set 0)", () => {
44+
it("accepts a governed semantic metric exemplar shape", () => {
45+
expect(validateEvidenceSnapshotProposed(baseSnapshot).valid).toBe(true);
46+
});
47+
48+
it("rejects missing qualification vector", () => {
49+
const result = validateEvidenceSnapshotProposed({
50+
...baseSnapshot,
51+
qualification: undefined,
52+
});
53+
expect(result.valid).toBe(false);
54+
});
55+
56+
it("rejects applicable freshness without clocks", () => {
57+
const result = validateEvidenceSnapshotProposed({
58+
...baseSnapshot,
59+
qualification: {
60+
...baseSnapshot.qualification,
61+
freshness: { applicable: "applicable" },
62+
},
63+
});
64+
expect(result.valid).toBe(false);
65+
expect(result.errors.some((e) => e.path?.includes("snapshotFrozenAt"))).toBe(true);
66+
});
67+
});

packages/evidence/src/index.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,29 @@ export {
4747
validateEvidenceSnapshotPersistedBatch,
4848
validateGovernedEvidencePresent,
4949
} from "./validate-evidence-snapshot.js";
50+
export {
51+
validateEvidenceSnapshotProposed,
52+
assertEvidenceSnapshotProposed,
53+
assertQualificationVector,
54+
} from "./validate-provider-snapshot.js";
55+
export { isFreshnessApplicable } from "./qualification.js";
56+
export type {
57+
ProvenanceRung,
58+
DefinitionRung,
59+
FreshnessQualification,
60+
FreshnessVerdict,
61+
FreshnessBindingClock,
62+
QualificationVector,
63+
VersionIdentityConstraint,
64+
} from "./qualification.js";
65+
export {
66+
PROVENANCE_ORDER,
67+
DEFINITION_ORDER,
68+
provenanceMeetsMinimum,
69+
definitionMeetsMinimum,
70+
} from "./qualification.js";
71+
export type {
72+
EvidenceSnapshotProposed,
73+
EvidenceSnapshotContract,
74+
EvidenceRenderedArtifact,
75+
} from "./provider-snapshot-contract.js";
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright (c) Better Data, Inc.
3+
4+
/**
5+
* ADR-0005 Set 0 — EvidenceSnapshot provider contract DTO.
6+
*
7+
* Extends WP-1 commit input with qualification vector, version identity,
8+
* measurements (opaque), and native metadata isolation boundaries.
9+
*/
10+
11+
import type { EvidenceLineageRef, DecisionRecordVersionMetadata } from "./types.js";
12+
import type {
13+
FreshnessQualification,
14+
QualificationVector,
15+
VersionIdentityConstraint,
16+
} from "./qualification.js";
17+
18+
/** Provider-proposed snapshot before engine freeze (ADR-0005). */
19+
export type EvidenceSnapshotProposed = {
20+
snapshotKey: string;
21+
sourceSystem: string;
22+
sourceRef?: string;
23+
capturedAt: string;
24+
capturedBy: string;
25+
values: Record<string, unknown>;
26+
versions: DecisionRecordVersionMetadata;
27+
qualification: QualificationVector;
28+
lineage?: EvidenceLineageRef[];
29+
version?: VersionIdentityConstraint;
30+
/** Quantitative properties — opaque until Canon promotes a key (ADR-0005). */
31+
measurements?: Record<string, unknown>;
32+
/** Provider-native terms — must not leak into Canon vocabulary. */
33+
nativeMetadata?: Record<string, unknown>;
34+
timelineEvent?: string;
35+
timelineAt?: string;
36+
sequence?: number;
37+
providerId?: string;
38+
conformanceSuiteVersion?: string;
39+
/** Optional client hash — engine recomputes from `values` at commit. */
40+
contentSha256?: string;
41+
renderedArtifact?: EvidenceRenderedArtifact;
42+
};
43+
44+
/** Frozen authoritative snapshot after engine commit (ADR-0004 + ADR-0005). */
45+
export type EvidenceSnapshotContract = EvidenceSnapshotProposed & {
46+
id?: string;
47+
contentSha256: string;
48+
};
49+
50+
export type EvidenceRenderedArtifact = {
51+
kind: "image" | "pdf" | "html" | "json";
52+
mimeType: string;
53+
uri?: string;
54+
/** Inline preview payload when uri absent (reference viewer only). */
55+
inlineBase64?: string;
56+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
// Copyright (c) Better Data, Inc.
3+
4+
/**
5+
* ADR-0005 Qualification Vector — P/D/F axes (Set 0 Provider Contract).
6+
*
7+
* Qualification is inherited from provider mapping, never caller-asserted (ADR-0004 C1).
8+
*/
9+
10+
/** Axis P — Provenance (universal). */
11+
export type ProvenanceRung = "unknown" | "system-attested" | "origin-attested";
12+
13+
/** Axis D — Definition (derived values; `not-applicable` for raw facts). */
14+
export type DefinitionRung =
15+
| "not-applicable"
16+
| "unreviewed"
17+
| "certified"
18+
| "governed";
19+
20+
export type FreshnessVerdict = "fresh" | "stale" | "unknown";
21+
22+
export type FreshnessBindingClock =
23+
| "snapshotFrozenAt"
24+
| "valueAsOf"
25+
| "sourceDataFreshness";
26+
27+
/** Axis F — Freshness (where temporal; identity constraints use `version` instead). */
28+
export type FreshnessQualification =
29+
| { applicable: "not-applicable" }
30+
| {
31+
applicable: "applicable";
32+
snapshotFrozenAt: string;
33+
valueAsOf: string;
34+
sourceDataFreshness?: string;
35+
freshnessSla?: string;
36+
freshnessVerdict?: FreshnessVerdict;
37+
/** Two-clock sources: binding (stalest) clock for role evaluation. */
38+
bindingClock?: FreshnessBindingClock;
39+
};
40+
41+
/** Open extensible vector — only P/D/F defined at launch (ADR-0005). */
42+
export type QualificationVector = {
43+
provenance: ProvenanceRung;
44+
definition: DefinitionRung;
45+
freshness: FreshnessQualification;
46+
};
47+
48+
/** Version identity constraint — not an ordered axis (ADR-0005). */
49+
export type VersionIdentityConstraint = {
50+
kind: "must_equal" | "must_be_in";
51+
value: string | string[];
52+
};
53+
54+
/** Compare ordered rungs on a single axis (≥ semantics for P/D; F uses age separately). */
55+
export const PROVENANCE_ORDER: readonly ProvenanceRung[] = [
56+
"unknown",
57+
"system-attested",
58+
"origin-attested",
59+
];
60+
61+
export const DEFINITION_ORDER: readonly DefinitionRung[] = [
62+
"not-applicable",
63+
"unreviewed",
64+
"certified",
65+
"governed",
66+
];
67+
68+
export function provenanceMeetsMinimum(
69+
actual: ProvenanceRung,
70+
required: ProvenanceRung,
71+
): boolean {
72+
return (
73+
PROVENANCE_ORDER.indexOf(actual) >= PROVENANCE_ORDER.indexOf(required)
74+
);
75+
}
76+
77+
export function definitionMeetsMinimum(
78+
actual: DefinitionRung,
79+
required: DefinitionRung,
80+
): boolean {
81+
if (actual === "not-applicable" || required === "not-applicable") {
82+
return true;
83+
}
84+
return (
85+
DEFINITION_ORDER.indexOf(actual) >= DEFINITION_ORDER.indexOf(required)
86+
);
87+
}
88+
89+
export function isFreshnessApplicable(
90+
freshness: FreshnessQualification,
91+
): freshness is Extract<FreshnessQualification, { applicable: "applicable" }> {
92+
return freshness.applicable === "applicable";
93+
}

0 commit comments

Comments
 (0)