@openagentsinc/conformance-kit 0.2.0-rc.7 → 0.2.1-rc.3
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/README.md +18 -8
- package/package.json +20 -6
- package/src/dse-extraction-laws.ts +273 -0
- package/src/graph-archive-laws.ts +267 -0
- package/src/graph-capability-delete-laws.ts +289 -0
- package/src/graph-fixtures.ts +40 -0
- package/src/graph-identity-laws.ts +267 -0
- package/src/graph-ranking-laws.ts +362 -0
- package/src/graph-rlm-laws.ts +243 -0
- package/src/index.ts +16 -0
- package/src/adapter-laws.test.ts +0 -36
- package/src/corpus-composition-laws.test.ts +0 -9
- package/src/event-log-laws.test.ts +0 -10
- package/src/recall-laws.test.ts +0 -10
- package/src/reducer-laws.test.ts +0 -17
- package/src/rlm-cap-laws.test.ts +0 -11
package/README.md
CHANGED
|
@@ -23,14 +23,24 @@ is proven to work before anyone points it at a third-party implementation.
|
|
|
23
23
|
|
|
24
24
|
## The suites
|
|
25
25
|
|
|
26
|
-
| Import
|
|
27
|
-
|
|
|
28
|
-
| `@openagentsinc/conformance-kit/adapter`
|
|
29
|
-
| `@openagentsinc/conformance-kit/event-log`
|
|
30
|
-
| `@openagentsinc/conformance-kit/reducer`
|
|
31
|
-
| `@openagentsinc/conformance-kit/recall`
|
|
32
|
-
| `@openagentsinc/conformance-kit/rlm`
|
|
33
|
-
| `@openagentsinc/conformance-kit/corpus`
|
|
26
|
+
| Import | Runner | Implementation under test | Laws |
|
|
27
|
+
| ----------------------------------------------- | ------------------------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
28
|
+
| `@openagentsinc/conformance-kit/adapter` | `runAdapterLaws` | `AgentHarness` | turn framing, capability refusal is fail-closed and named, suspend/continue cursor exactness (with honest lossy degradation) |
|
|
29
|
+
| `@openagentsinc/conformance-kit/event-log` | `runEventLogLaws` | `HarnessEventLogStore` | append monotonicity / dup-free, gap-free replay from a cursor, durable replay after process death, rerun boundaries, live attach, single-flight attach |
|
|
30
|
+
| `@openagentsinc/conformance-kit/reducer` | `runReducerLaws` | a progressive UI-message reducer | progressive fold, tool state machine, transient bypass, fail-loud-never-corrupt |
|
|
31
|
+
| `@openagentsinc/conformance-kit/recall` | `runRecallLaws` | a `HistoryRecall` Tier D source | correctness anchor, caps truncate + report, `cost.modelCalls === 0`, coverage-note carry-through, typed invalid input |
|
|
32
|
+
| `@openagentsinc/conformance-kit/rlm` | `runRlmCapLaws` | the RLM engine + corpus source | every cap → honest `Partial`, generous → `Completed`, no laundering, deterministic never touches a model |
|
|
33
|
+
| `@openagentsinc/conformance-kit/corpus` | `runCorpusCompositionLaws` | a constructor and projection | identity, order, policy, scope, freshness, citations, duplicates, planes, coverage, exclusions, bounded access |
|
|
34
|
+
| `@openagentsinc/conformance-kit/graph-identity` | `runGraphIdentityLaws` | graph builders | identity separation, deterministic rebuild, merge evidence, provenance |
|
|
35
|
+
| `@openagentsinc/conformance-kit/graph-delete` | `runGraphCapabilityDeleteLaws` | graph adapter and delete planner | typed capabilities, shared retention, all artifact planes, execution receipts |
|
|
36
|
+
| `@openagentsinc/conformance-kit/dse-extraction` | `runDseExtractionLaws` | DSE graph extraction | bounded extraction, receipts, inert prompt-injection text |
|
|
37
|
+
| `@openagentsinc/conformance-kit/graph-rlm` | `runGraphRlmLaws` | graph RLM projection | exact citations, policy, freshness, bounded operators |
|
|
38
|
+
| `@openagentsinc/conformance-kit/graph-ranking` | `runGraphRankingLaws` | graph ranker | isolation, deterministic order, degradation, evidence validation |
|
|
39
|
+
| `@openagentsinc/conformance-kit/graph-archive` | `runGraphArchiveLaws` | archive codec | stable round trip, optional state, semantic corruption refusal |
|
|
40
|
+
|
|
41
|
+
The `@openagentsinc/conformance-kit/graph-fixtures` subpath exports public-safe
|
|
42
|
+
fixtures. The fixtures contain no credentials, private corpus text, or host
|
|
43
|
+
authority.
|
|
34
44
|
|
|
35
45
|
Everything is re-exported from the package root too
|
|
36
46
|
(`@openagentsinc/conformance-kit`).
|
package/package.json
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openagentsinc/conformance-kit",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1-rc.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"files": [
|
|
7
|
-
"src",
|
|
7
|
+
"src/**/*.ts",
|
|
8
|
+
"!src/**/*.test.ts",
|
|
8
9
|
"README.md"
|
|
9
10
|
],
|
|
10
11
|
"type": "module",
|
|
@@ -16,6 +17,13 @@
|
|
|
16
17
|
"./recall": "./src/recall-laws.ts",
|
|
17
18
|
"./rlm": "./src/rlm-cap-laws.ts",
|
|
18
19
|
"./corpus": "./src/corpus-composition-laws.ts",
|
|
20
|
+
"./graph-identity": "./src/graph-identity-laws.ts",
|
|
21
|
+
"./graph-delete": "./src/graph-capability-delete-laws.ts",
|
|
22
|
+
"./dse-extraction": "./src/dse-extraction-laws.ts",
|
|
23
|
+
"./graph-rlm": "./src/graph-rlm-laws.ts",
|
|
24
|
+
"./graph-ranking": "./src/graph-ranking-laws.ts",
|
|
25
|
+
"./graph-archive": "./src/graph-archive-laws.ts",
|
|
26
|
+
"./graph-fixtures": "./src/graph-fixtures.ts",
|
|
19
27
|
"./fixtures": "./src/fixtures.ts"
|
|
20
28
|
},
|
|
21
29
|
"publishConfig": {
|
|
@@ -23,16 +31,22 @@
|
|
|
23
31
|
},
|
|
24
32
|
"dependencies": {
|
|
25
33
|
"effect": "4.0.0-beta.94",
|
|
26
|
-
"@openagentsinc/agent-
|
|
27
|
-
"@openagentsinc/agent-
|
|
28
|
-
"@openagentsinc/
|
|
29
|
-
"@openagentsinc/
|
|
34
|
+
"@openagentsinc/agent-harness-contract": "0.2.1-rc.3",
|
|
35
|
+
"@openagentsinc/agent-runtime-schema": "0.2.1-rc.3",
|
|
36
|
+
"@openagentsinc/history-corpus": "0.2.1-rc.3",
|
|
37
|
+
"@openagentsinc/dse": "0.2.1-rc.3",
|
|
38
|
+
"@openagentsinc/graph-corpus": "0.2.1-rc.3",
|
|
39
|
+
"@openagentsinc/rlm": "0.2.1-rc.3"
|
|
30
40
|
},
|
|
31
41
|
"devDependencies": {
|
|
32
42
|
"@types/node": "24.13.1",
|
|
33
43
|
"typescript": "^6.0.3",
|
|
44
|
+
"vite-plus": "0.2.4",
|
|
34
45
|
"@types/vite-plus-matchers": "0.0.0"
|
|
35
46
|
},
|
|
47
|
+
"peerDependencies": {
|
|
48
|
+
"vite-plus": ">=0.2.4 <1"
|
|
49
|
+
},
|
|
36
50
|
"scripts": {
|
|
37
51
|
"test": "vp test --run packages/conformance-kit/src",
|
|
38
52
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import { Effect, Schema as S } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
COMPILED_PROGRAM_SCHEMA_LITERAL,
|
|
4
|
+
CompiledProgram,
|
|
5
|
+
GRAPH_EXTRACTION_CORPUS_SCHEMA_ID,
|
|
6
|
+
GraphExtractionCorpus,
|
|
7
|
+
GraphExtractionLimits,
|
|
8
|
+
GraphExtractionError,
|
|
9
|
+
graphExtractionSignature,
|
|
10
|
+
type GraphExtractionCandidates,
|
|
11
|
+
type applyGraphExtractionCandidates,
|
|
12
|
+
type planGraphExtractionBatches,
|
|
13
|
+
type runDeterministicGraphExtraction,
|
|
14
|
+
type runGraphExtraction,
|
|
15
|
+
type validateGraphExtractionRunReceipt,
|
|
16
|
+
type GraphExtractionRuntimeDeps,
|
|
17
|
+
} from "@openagentsinc/dse";
|
|
18
|
+
import type { RlmSourceLocator } from "@openagentsinc/rlm/schemas";
|
|
19
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
20
|
+
|
|
21
|
+
import { graphPromptInjectionFixture } from "./graph-fixtures.ts";
|
|
22
|
+
|
|
23
|
+
export interface DseExtractionLawsConfig {
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly planGraphExtractionBatches: typeof planGraphExtractionBatches;
|
|
26
|
+
readonly runGraphExtraction: typeof runGraphExtraction;
|
|
27
|
+
readonly runDeterministicGraphExtraction: typeof runDeterministicGraphExtraction;
|
|
28
|
+
readonly validateGraphExtractionRunReceipt: typeof validateGraphExtractionRunReceipt;
|
|
29
|
+
readonly applyGraphExtractionCandidates: typeof applyGraphExtractionCandidates;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const source = (entryKey: string): RlmSourceLocator => ({
|
|
33
|
+
sourcePlane: "repository",
|
|
34
|
+
sourceKind: "conformance_fixture",
|
|
35
|
+
sourceAddress: { addressSchemaId: "conformance.path.v1", encodedAddress: `src/${entryKey}.ts` },
|
|
36
|
+
corpusRef: "corpus.extraction.conformance",
|
|
37
|
+
contentDigest: "a".repeat(64) as RlmSourceLocator["contentDigest"],
|
|
38
|
+
entryRef: entryKey as RlmSourceLocator["entryRef"],
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const corpus = S.decodeUnknownSync(GraphExtractionCorpus)({
|
|
42
|
+
schemaId: GRAPH_EXTRACTION_CORPUS_SCHEMA_ID,
|
|
43
|
+
corpusRef: "corpus.extraction.conformance",
|
|
44
|
+
contentDigest: "a".repeat(64),
|
|
45
|
+
manifestDigest: "b".repeat(64),
|
|
46
|
+
entries: [
|
|
47
|
+
{ entryKey: "a", source: source("a"), text: graphPromptInjectionFixture },
|
|
48
|
+
{ entryKey: "b", source: source("b"), text: "Alex builds portable agents." },
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const limits = S.decodeUnknownSync(GraphExtractionLimits)({
|
|
53
|
+
maxEntries: 10,
|
|
54
|
+
maxCharacters: 10_000,
|
|
55
|
+
maxInputTokens: 10_000,
|
|
56
|
+
maxOutputTokens: 10_000,
|
|
57
|
+
maxOutputCharacters: 20_000,
|
|
58
|
+
maxModelCalls: 10,
|
|
59
|
+
maxWallClockMs: 10_000,
|
|
60
|
+
maxConcurrency: 1,
|
|
61
|
+
maxEntriesPerBatch: 10,
|
|
62
|
+
maxCharactersPerBatch: 10_000,
|
|
63
|
+
maxInputTokensPerBatch: 10_000,
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
const candidates: GraphExtractionCandidates = {
|
|
67
|
+
mentions: [
|
|
68
|
+
{
|
|
69
|
+
candidateKey: "m1",
|
|
70
|
+
identityNamespace: "people",
|
|
71
|
+
canonicalKey: "alex:a",
|
|
72
|
+
supportEntryKey: "a",
|
|
73
|
+
confidence: 0.9,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
candidateKey: "m2",
|
|
77
|
+
identityNamespace: "people",
|
|
78
|
+
canonicalKey: "alex:b",
|
|
79
|
+
supportEntryKey: "b",
|
|
80
|
+
confidence: 0.8,
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
entities: [
|
|
84
|
+
{
|
|
85
|
+
candidateKey: "e1",
|
|
86
|
+
identityNamespace: "people",
|
|
87
|
+
canonicalKey: "alex",
|
|
88
|
+
mentionCandidateKeys: ["m1", "m2"],
|
|
89
|
+
confidence: 0.95,
|
|
90
|
+
},
|
|
91
|
+
],
|
|
92
|
+
relations: [],
|
|
93
|
+
merges: [
|
|
94
|
+
{
|
|
95
|
+
candidateKey: "merge1",
|
|
96
|
+
entityCandidateKey: "e1",
|
|
97
|
+
mentionCandidateKeys: ["m1", "m2"],
|
|
98
|
+
confidence: 0.9,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const deps = (): GraphExtractionRuntimeDeps => ({
|
|
104
|
+
countTokens: (text) => text.length,
|
|
105
|
+
monotonicMs: () => 0,
|
|
106
|
+
now: () => "2026-07-22T00:00:00Z",
|
|
107
|
+
assertCorpusUnchanged: () => Effect.succeed("freshness.conformance"),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
/** Laws for bounded DSE graph extraction and application. */
|
|
111
|
+
export const runDseExtractionLaws = (implementation: DseExtractionLawsConfig): void => {
|
|
112
|
+
describe(`[${implementation.label}] DSE graph extraction`, () => {
|
|
113
|
+
test("deterministic extraction is stable, receipted, and applicable", async () => {
|
|
114
|
+
const extractor = {
|
|
115
|
+
parserRef: "parser.conformance.v1",
|
|
116
|
+
parserVersion: "1.0.0",
|
|
117
|
+
extract: () => Effect.succeed(candidates),
|
|
118
|
+
};
|
|
119
|
+
const args = { corpus, extractor, limits, deps: deps() };
|
|
120
|
+
const left = await Effect.runPromise(implementation.runDeterministicGraphExtraction(args));
|
|
121
|
+
const right = await Effect.runPromise(implementation.runDeterministicGraphExtraction(args));
|
|
122
|
+
expect(right).toEqual(left);
|
|
123
|
+
expect(left.receipt).toMatchObject({ modelCalls: 0, concurrencyHighWaterMark: 0 });
|
|
124
|
+
await expect(
|
|
125
|
+
Effect.runPromise(
|
|
126
|
+
implementation.validateGraphExtractionRunReceipt(left.receipt, {
|
|
127
|
+
corpus,
|
|
128
|
+
limits,
|
|
129
|
+
countTokens: deps().countTokens,
|
|
130
|
+
assertCorpusUnchanged: deps().assertCorpusUnchanged,
|
|
131
|
+
result: left,
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
).resolves.toBeUndefined();
|
|
135
|
+
const built = await Effect.runPromise(
|
|
136
|
+
implementation.applyGraphExtractionCandidates({
|
|
137
|
+
run: left,
|
|
138
|
+
execution: {
|
|
139
|
+
corpus,
|
|
140
|
+
limits,
|
|
141
|
+
countTokens: deps().countTokens,
|
|
142
|
+
assertCorpusUnchanged: deps().assertCorpusUnchanged,
|
|
143
|
+
},
|
|
144
|
+
graphRef: "graph.extraction.conformance",
|
|
145
|
+
scopeRef: "tenant.a",
|
|
146
|
+
policy: { includeVisibilities: ["private"], includeRedactionClasses: ["none"] },
|
|
147
|
+
}),
|
|
148
|
+
);
|
|
149
|
+
expect(built.snapshot).toMatchObject({
|
|
150
|
+
mentions: { length: 2 },
|
|
151
|
+
entities: { length: 1 },
|
|
152
|
+
merges: { length: 1 },
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("caps are named and partial output cannot be applied", async () => {
|
|
157
|
+
const capped = S.decodeUnknownSync(GraphExtractionLimits)({ ...limits, maxEntries: 1 });
|
|
158
|
+
const plan = implementation.planGraphExtractionBatches({
|
|
159
|
+
corpus,
|
|
160
|
+
limits: capped,
|
|
161
|
+
countTokens: deps().countTokens,
|
|
162
|
+
});
|
|
163
|
+
expect(plan.reasons).toContain("entry_cap");
|
|
164
|
+
const run = await Effect.runPromise(
|
|
165
|
+
implementation.runDeterministicGraphExtraction({
|
|
166
|
+
corpus,
|
|
167
|
+
extractor: {
|
|
168
|
+
parserRef: "parser.conformance.v1",
|
|
169
|
+
parserVersion: "1.0.0",
|
|
170
|
+
extract: () =>
|
|
171
|
+
Effect.succeed({ mentions: [], entities: [], relations: [], merges: [] }),
|
|
172
|
+
},
|
|
173
|
+
limits: capped,
|
|
174
|
+
deps: deps(),
|
|
175
|
+
}),
|
|
176
|
+
);
|
|
177
|
+
expect(run.status).toBe("Partial");
|
|
178
|
+
const error = await Effect.runPromise(
|
|
179
|
+
implementation
|
|
180
|
+
.applyGraphExtractionCandidates({
|
|
181
|
+
run,
|
|
182
|
+
execution: {
|
|
183
|
+
corpus,
|
|
184
|
+
limits: capped,
|
|
185
|
+
countTokens: deps().countTokens,
|
|
186
|
+
assertCorpusUnchanged: deps().assertCorpusUnchanged,
|
|
187
|
+
},
|
|
188
|
+
graphRef: "graph.extraction.partial",
|
|
189
|
+
scopeRef: "tenant.a",
|
|
190
|
+
policy: { includeVisibilities: ["private"], includeRedactionClasses: ["none"] },
|
|
191
|
+
})
|
|
192
|
+
.pipe(Effect.flip),
|
|
193
|
+
);
|
|
194
|
+
expect(error.reason).toBe("invalid_candidate");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("prompt-injection text stays in the untrusted envelope", async () => {
|
|
198
|
+
const program = S.decodeUnknownSync(CompiledProgram)({
|
|
199
|
+
schema: COMPILED_PROGRAM_SCHEMA_LITERAL,
|
|
200
|
+
signatureId: graphExtractionSignature.signatureId,
|
|
201
|
+
promptIr: graphExtractionSignature.defaultPromptIr,
|
|
202
|
+
decodePolicy: { maxRepairs: 0, maxOutputChars: 20_000 },
|
|
203
|
+
modelRole: "graph_extractor",
|
|
204
|
+
});
|
|
205
|
+
const observed: string[] = [];
|
|
206
|
+
await Effect.runPromise(
|
|
207
|
+
implementation.runGraphExtraction({
|
|
208
|
+
corpus,
|
|
209
|
+
program,
|
|
210
|
+
model: {
|
|
211
|
+
complete: ({ message }) => {
|
|
212
|
+
observed.push(message.canonicalBytes);
|
|
213
|
+
return Effect.succeed({
|
|
214
|
+
text: JSON.stringify(candidates),
|
|
215
|
+
modelIdentity: "model.conformance.v1",
|
|
216
|
+
usage: { _tag: "Exact", inputTokens: 1, outputTokens: 1 },
|
|
217
|
+
});
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
limits,
|
|
221
|
+
deps: deps(),
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
const envelope = JSON.parse(observed[0]!) as { trusted: unknown; untrustedContext: string };
|
|
225
|
+
expect(envelope.untrustedContext).toContain(graphPromptInjectionFixture);
|
|
226
|
+
expect(JSON.stringify(envelope.trusted)).not.toContain(graphPromptInjectionFixture);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("a corpus that changes at an external-call boundary is refused", async () => {
|
|
230
|
+
const program = S.decodeUnknownSync(CompiledProgram)({
|
|
231
|
+
schema: COMPILED_PROGRAM_SCHEMA_LITERAL,
|
|
232
|
+
signatureId: graphExtractionSignature.signatureId,
|
|
233
|
+
promptIr: graphExtractionSignature.defaultPromptIr,
|
|
234
|
+
decodePolicy: { maxRepairs: 0, maxOutputChars: 20_000 },
|
|
235
|
+
modelRole: "graph_extractor",
|
|
236
|
+
});
|
|
237
|
+
let assertions = 0;
|
|
238
|
+
const changingDeps: GraphExtractionRuntimeDeps = {
|
|
239
|
+
...deps(),
|
|
240
|
+
assertCorpusUnchanged: () => {
|
|
241
|
+
assertions += 1;
|
|
242
|
+
return assertions < 3
|
|
243
|
+
? Effect.succeed("freshness.conformance")
|
|
244
|
+
: Effect.fail(
|
|
245
|
+
new GraphExtractionError({
|
|
246
|
+
reason: "invalid_corpus",
|
|
247
|
+
detailSafe: "The conformance corpus changed.",
|
|
248
|
+
}),
|
|
249
|
+
);
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
const result = await Effect.runPromise(
|
|
253
|
+
implementation.runGraphExtraction({
|
|
254
|
+
corpus,
|
|
255
|
+
program,
|
|
256
|
+
model: {
|
|
257
|
+
complete: () =>
|
|
258
|
+
Effect.succeed({
|
|
259
|
+
text: JSON.stringify(candidates),
|
|
260
|
+
modelIdentity: "model.conformance.v1",
|
|
261
|
+
usage: { _tag: "Exact", inputTokens: 1, outputTokens: 1 },
|
|
262
|
+
}),
|
|
263
|
+
},
|
|
264
|
+
limits,
|
|
265
|
+
deps: changingDeps,
|
|
266
|
+
}),
|
|
267
|
+
);
|
|
268
|
+
expect(result.status).toBe("Refused");
|
|
269
|
+
expect(result.batches).toEqual([]);
|
|
270
|
+
expect(result.receipt.reasons).toContain("invalid_corpus");
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
};
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { Effect, Schema as S } from "effect";
|
|
3
|
+
import {
|
|
4
|
+
GraphArchiveRef,
|
|
5
|
+
type GraphCorpusArchive,
|
|
6
|
+
type encodeGraphCorpusArchive,
|
|
7
|
+
type exportGraphCorpusArchive,
|
|
8
|
+
type importGraphCorpusArchive,
|
|
9
|
+
} from "@openagentsinc/graph-corpus/archive";
|
|
10
|
+
import {
|
|
11
|
+
canonicalJson,
|
|
12
|
+
GraphSummaryArtifact,
|
|
13
|
+
graphDigest,
|
|
14
|
+
sha256Hex,
|
|
15
|
+
type GraphDigest,
|
|
16
|
+
buildGraphCorpus,
|
|
17
|
+
makeGraphAdapterCapabilities,
|
|
18
|
+
makeGraphArtifactInventory,
|
|
19
|
+
makeGraphMention,
|
|
20
|
+
} from "@openagentsinc/graph-corpus";
|
|
21
|
+
import {
|
|
22
|
+
GRAPH_RANKING_ALGORITHM_VERSION,
|
|
23
|
+
GRAPH_RANKING_SNAPSHOT_SCHEMA_ID,
|
|
24
|
+
GraphRankingSnapshot,
|
|
25
|
+
} from "@openagentsinc/graph-corpus/ranking";
|
|
26
|
+
import { describe, expect, test } from "vite-plus/test";
|
|
27
|
+
|
|
28
|
+
import {
|
|
29
|
+
graphArchiveCorruptionFixtures,
|
|
30
|
+
graphConformanceDerivation,
|
|
31
|
+
graphConformancePolicy,
|
|
32
|
+
graphConformanceSource,
|
|
33
|
+
} from "./graph-fixtures.ts";
|
|
34
|
+
|
|
35
|
+
export interface GraphArchiveLawsConfig {
|
|
36
|
+
readonly label: string;
|
|
37
|
+
readonly buildGraphCorpus: typeof buildGraphCorpus;
|
|
38
|
+
readonly makeGraphMention: typeof makeGraphMention;
|
|
39
|
+
readonly makeGraphAdapterCapabilities: typeof makeGraphAdapterCapabilities;
|
|
40
|
+
readonly makeGraphArtifactInventory: typeof makeGraphArtifactInventory;
|
|
41
|
+
readonly exportGraphCorpusArchive: typeof exportGraphCorpusArchive;
|
|
42
|
+
readonly encodeGraphCorpusArchive: typeof encodeGraphCorpusArchive;
|
|
43
|
+
readonly importGraphCorpusArchive: typeof importGraphCorpusArchive;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
type Mutable<T> = T extends string | number | boolean | null | undefined
|
|
47
|
+
? T
|
|
48
|
+
: T extends ReadonlyArray<infer U>
|
|
49
|
+
? Mutable<U>[]
|
|
50
|
+
: T extends object
|
|
51
|
+
? { -readonly [K in keyof T]: Mutable<T[K]> }
|
|
52
|
+
: T;
|
|
53
|
+
const digest = (value: unknown): GraphDigest => graphDigest(sha256Hex(canonicalJson(value)));
|
|
54
|
+
const bytesOf = (value: unknown): Uint8Array => new TextEncoder().encode(canonicalJson(value));
|
|
55
|
+
const payloadDigest = (bytes: Uint8Array): GraphDigest =>
|
|
56
|
+
graphDigest(createHash("sha256").update(bytes).digest("hex"));
|
|
57
|
+
const base64 = (bytes: Uint8Array): string =>
|
|
58
|
+
btoa(Array.from(bytes, (value) => String.fromCharCode(value)).join(""));
|
|
59
|
+
const rehash = (value: GraphCorpusArchive | Mutable<GraphCorpusArchive>): GraphCorpusArchive => {
|
|
60
|
+
const archive = structuredClone(value) as unknown as Mutable<GraphCorpusArchive>;
|
|
61
|
+
const update = (section: { sectionDigest: GraphDigest; [key: string]: unknown }) => {
|
|
62
|
+
const { sectionDigest: _old, ...content } = section;
|
|
63
|
+
section.sectionDigest = digest(content);
|
|
64
|
+
};
|
|
65
|
+
update(archive.sections.graph);
|
|
66
|
+
update(archive.sections.nodes);
|
|
67
|
+
update(archive.sections.edges);
|
|
68
|
+
update(archive.sections.sourceMemberships);
|
|
69
|
+
update(archive.sections.mergeEvidence);
|
|
70
|
+
update(archive.sections.provenance);
|
|
71
|
+
update(archive.sections.descriptors);
|
|
72
|
+
if (archive.sections.vectors !== undefined) update(archive.sections.vectors);
|
|
73
|
+
if (archive.sections.summaries !== undefined) update(archive.sections.summaries);
|
|
74
|
+
if (archive.sections.ranking !== undefined) update(archive.sections.ranking);
|
|
75
|
+
if (archive.sections.contentExtension !== undefined) update(archive.sections.contentExtension);
|
|
76
|
+
archive.manifest.sectionDigests = {
|
|
77
|
+
graph: archive.sections.graph.sectionDigest,
|
|
78
|
+
nodes: archive.sections.nodes.sectionDigest,
|
|
79
|
+
edges: archive.sections.edges.sectionDigest,
|
|
80
|
+
sourceMemberships: archive.sections.sourceMemberships.sectionDigest,
|
|
81
|
+
mergeEvidence: archive.sections.mergeEvidence.sectionDigest,
|
|
82
|
+
provenance: archive.sections.provenance.sectionDigest,
|
|
83
|
+
descriptors: archive.sections.descriptors.sectionDigest,
|
|
84
|
+
...(archive.sections.vectors === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { vectors: archive.sections.vectors.sectionDigest }),
|
|
87
|
+
...(archive.sections.summaries === undefined
|
|
88
|
+
? {}
|
|
89
|
+
: { summaries: archive.sections.summaries.sectionDigest }),
|
|
90
|
+
...(archive.sections.ranking === undefined
|
|
91
|
+
? {}
|
|
92
|
+
: { ranking: archive.sections.ranking.sectionDigest }),
|
|
93
|
+
...(archive.sections.contentExtension === undefined
|
|
94
|
+
? {}
|
|
95
|
+
: { contentExtension: archive.sections.contentExtension.sectionDigest }),
|
|
96
|
+
};
|
|
97
|
+
archive.manifest.contentDigest = digest(archive.sections);
|
|
98
|
+
const {
|
|
99
|
+
manifestDigest: _manifestDigest,
|
|
100
|
+
archiveRef: _archiveRef,
|
|
101
|
+
...manifestContent
|
|
102
|
+
} = archive.manifest;
|
|
103
|
+
archive.manifest.manifestDigest = digest(manifestContent);
|
|
104
|
+
archive.manifest.archiveRef = S.decodeUnknownSync(GraphArchiveRef)(
|
|
105
|
+
`archive.${sha256Hex(canonicalJson({ contentDigest: archive.manifest.contentDigest, manifestDigest: archive.manifest.manifestDigest }))}`,
|
|
106
|
+
);
|
|
107
|
+
return archive as unknown as GraphCorpusArchive;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Laws for canonical graph archive round trips and semantic corruption refusal. */
|
|
111
|
+
export const runGraphArchiveLaws = (implementation: GraphArchiveLawsConfig): void => {
|
|
112
|
+
const fixture = async () => {
|
|
113
|
+
const mention = implementation.makeGraphMention({
|
|
114
|
+
identityNamespace: "archive",
|
|
115
|
+
canonicalKey: "alex",
|
|
116
|
+
identityScopeRef: "tenant.a",
|
|
117
|
+
source: graphConformanceSource("archive"),
|
|
118
|
+
derivation: graphConformanceDerivation,
|
|
119
|
+
});
|
|
120
|
+
const built = await Effect.runPromise(
|
|
121
|
+
implementation.buildGraphCorpus({
|
|
122
|
+
graphRef: "graph.archive.conformance",
|
|
123
|
+
scopeRef: "tenant.a",
|
|
124
|
+
policy: graphConformancePolicy,
|
|
125
|
+
mentions: [mention],
|
|
126
|
+
entities: [],
|
|
127
|
+
relations: [],
|
|
128
|
+
}),
|
|
129
|
+
);
|
|
130
|
+
const summaryBytes = new TextEncoder().encode("public conformance summary");
|
|
131
|
+
const summary = S.decodeUnknownSync(GraphSummaryArtifact)({
|
|
132
|
+
artifactKind: "summary",
|
|
133
|
+
artifactRef: "summary.conformance",
|
|
134
|
+
artifactDigest: payloadDigest(summaryBytes),
|
|
135
|
+
ownerElementRef: mention.elementRef,
|
|
136
|
+
});
|
|
137
|
+
const inventory = implementation.makeGraphArtifactInventory({
|
|
138
|
+
built,
|
|
139
|
+
vectors: [],
|
|
140
|
+
summaries: [summary],
|
|
141
|
+
rankingRefs: [],
|
|
142
|
+
coverage: {
|
|
143
|
+
vectors: { _tag: "Complete" },
|
|
144
|
+
summaries: { _tag: "Complete" },
|
|
145
|
+
rankingRefs: { _tag: "Complete" },
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
const rankingContent = {
|
|
149
|
+
schemaId: GRAPH_RANKING_SNAPSHOT_SCHEMA_ID,
|
|
150
|
+
graphRef: built.snapshot.graphRef,
|
|
151
|
+
scopeRef: built.snapshot.scopeRef,
|
|
152
|
+
graphDigest: built.snapshot.graphDigest,
|
|
153
|
+
manifestDigest: built.manifest.manifestDigest,
|
|
154
|
+
corpusRef: "corpus.archive.conformance",
|
|
155
|
+
contentDigest: "c".repeat(64),
|
|
156
|
+
corpusManifestDigest: "d".repeat(64),
|
|
157
|
+
classificationDigest: "e".repeat(64),
|
|
158
|
+
algorithmVersion: GRAPH_RANKING_ALGORITHM_VERSION,
|
|
159
|
+
feedbackObservations: [],
|
|
160
|
+
confidences: [],
|
|
161
|
+
features: [],
|
|
162
|
+
} as const;
|
|
163
|
+
const ranking = S.decodeUnknownSync(GraphRankingSnapshot)({
|
|
164
|
+
...rankingContent,
|
|
165
|
+
snapshotRef: `ranking-snapshot.${sha256Hex(canonicalJson(rankingContent))}`,
|
|
166
|
+
snapshotDigest: digest(rankingContent),
|
|
167
|
+
});
|
|
168
|
+
return { built, inventory, ranking, summary, summaryBytes };
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
describe(`[${implementation.label}] graph archive`, () => {
|
|
172
|
+
test("graph, provenance, inventory, and ranking round-trip in stable inert bytes", async () => {
|
|
173
|
+
const value = await fixture();
|
|
174
|
+
const capabilities = implementation.makeGraphAdapterCapabilities(["snapshot_export"]);
|
|
175
|
+
const input = {
|
|
176
|
+
built: value.built,
|
|
177
|
+
capabilities,
|
|
178
|
+
artifactInventory: value.inventory,
|
|
179
|
+
summaryRecords: [
|
|
180
|
+
{
|
|
181
|
+
artifact: value.summary,
|
|
182
|
+
summarySchemaId: "summary.conformance.v1",
|
|
183
|
+
encoding: "utf8-base64" as const,
|
|
184
|
+
payloadBase64: base64(value.summaryBytes),
|
|
185
|
+
payloadDigest: payloadDigest(value.summaryBytes),
|
|
186
|
+
visibility: "private" as const,
|
|
187
|
+
redactionClass: "none" as const,
|
|
188
|
+
},
|
|
189
|
+
],
|
|
190
|
+
rankingSnapshots: [value.ranking],
|
|
191
|
+
};
|
|
192
|
+
const left = await Effect.runPromise(implementation.encodeGraphCorpusArchive(input));
|
|
193
|
+
const right = await Effect.runPromise(implementation.encodeGraphCorpusArchive(input));
|
|
194
|
+
expect(right).toEqual(left);
|
|
195
|
+
const imported = await Effect.runPromise(implementation.importGraphCorpusArchive(left));
|
|
196
|
+
expect(imported.built).toEqual(value.built);
|
|
197
|
+
expect(imported.artifactInventory).toEqual(value.inventory);
|
|
198
|
+
expect(imported.rankingSnapshots).toEqual([value.ranking]);
|
|
199
|
+
expect(Object.isFrozen(imported)).toBe(true);
|
|
200
|
+
expect(Object.isFrozen(imported.archive)).toBe(true);
|
|
201
|
+
expect(Object.isFrozen(imported.archive.sections)).toBe(true);
|
|
202
|
+
expect(Object.isFrozen(imported.built.snapshot)).toBe(true);
|
|
203
|
+
expect(Object.isFrozen(imported.built.snapshot.mentions)).toBe(true);
|
|
204
|
+
expect(Object.isFrozen(imported.artifactInventory)).toBe(true);
|
|
205
|
+
expect(Object.isFrozen(imported.rankingSnapshots)).toBe(true);
|
|
206
|
+
expect(Object.isFrozen(imported.rankingSnapshots[0])).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test("semantic corruption reaches exact fail-closed reasons", async () => {
|
|
210
|
+
expect(graphArchiveCorruptionFixtures).toEqual(
|
|
211
|
+
expect.arrayContaining([
|
|
212
|
+
"changed_graph_identity",
|
|
213
|
+
"missing_provenance",
|
|
214
|
+
"unsupported_version",
|
|
215
|
+
"non_canonical_bytes",
|
|
216
|
+
]),
|
|
217
|
+
);
|
|
218
|
+
const value = await fixture();
|
|
219
|
+
const capabilities = implementation.makeGraphAdapterCapabilities(["snapshot_export"]);
|
|
220
|
+
const archive = await Effect.runPromise(
|
|
221
|
+
implementation.exportGraphCorpusArchive({
|
|
222
|
+
built: value.built,
|
|
223
|
+
capabilities,
|
|
224
|
+
artifactInventory: value.inventory,
|
|
225
|
+
summaryRecords: [
|
|
226
|
+
{
|
|
227
|
+
artifact: value.summary,
|
|
228
|
+
summarySchemaId: "summary.conformance.v1",
|
|
229
|
+
encoding: "utf8-base64",
|
|
230
|
+
payloadBase64: base64(value.summaryBytes),
|
|
231
|
+
payloadDigest: payloadDigest(value.summaryBytes),
|
|
232
|
+
visibility: "private",
|
|
233
|
+
redactionClass: "none",
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
rankingSnapshots: [value.ranking],
|
|
237
|
+
}),
|
|
238
|
+
);
|
|
239
|
+
const missing = structuredClone(archive) as unknown as Mutable<GraphCorpusArchive>;
|
|
240
|
+
missing.sections.provenance.entries.pop();
|
|
241
|
+
await expect(
|
|
242
|
+
Effect.runPromise(implementation.importGraphCorpusArchive(bytesOf(rehash(missing)))),
|
|
243
|
+
).rejects.toMatchObject({
|
|
244
|
+
_tag: "GraphCorpus.ArchiveError",
|
|
245
|
+
reason: "stale_binding",
|
|
246
|
+
});
|
|
247
|
+
await expect(
|
|
248
|
+
Effect.runPromise(
|
|
249
|
+
implementation.importGraphCorpusArchive(
|
|
250
|
+
new TextEncoder().encode('{"manifest":{"formatVersion":2}}'),
|
|
251
|
+
),
|
|
252
|
+
),
|
|
253
|
+
).rejects.toMatchObject({ reason: "unsupported_version" });
|
|
254
|
+
const canonical = bytesOf(archive);
|
|
255
|
+
await expect(
|
|
256
|
+
Effect.runPromise(
|
|
257
|
+
implementation.importGraphCorpusArchive(new Uint8Array([...canonical, 32])),
|
|
258
|
+
),
|
|
259
|
+
).rejects.toMatchObject({ reason: "non_canonical" });
|
|
260
|
+
const changed = structuredClone(archive) as unknown as Mutable<GraphCorpusArchive>;
|
|
261
|
+
changed.sections.graph.graphDigest = "f".repeat(64) as GraphDigest;
|
|
262
|
+
await expect(
|
|
263
|
+
Effect.runPromise(implementation.importGraphCorpusArchive(bytesOf(rehash(changed)))),
|
|
264
|
+
).rejects.toMatchObject({ reason: "stale_binding" });
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
};
|