@nanobpm/urban 0.56.0 → 0.58.0
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/dist/context/adversarial/index.d.ts +1 -0
- package/dist/context/adversarial/index.js +1 -0
- package/dist/context/binding/index.d.ts +1 -0
- package/dist/context/binding/index.js +1 -0
- package/dist/context/conformance/index.d.ts +66 -0
- package/dist/context/conformance/index.js +215 -0
- package/dist/context/conformance/records.conformance.d.ts +1 -0
- package/dist/context/conformance/records.conformance.js +52 -0
- package/dist/context/git/index.d.ts +1 -0
- package/dist/context/git/index.js +1 -0
- package/dist/context/index.d.ts +6 -0
- package/dist/context/index.js +18 -0
- package/dist/context/pii/index.d.ts +1 -0
- package/dist/context/pii/index.js +1 -0
- package/dist/context/retrieval/index.d.ts +1 -0
- package/dist/context/retrieval/index.js +1 -0
- package/dist/context/schema/index.d.ts +135 -0
- package/dist/context/schema/index.js +330 -0
- package/package.json +32 -1
- package/src/context/README.md +60 -0
- package/src/context/adversarial/index.ts +2 -0
- package/src/context/binding/index.ts +2 -0
- package/src/context/conformance/index.ts +285 -0
- package/src/context/conformance/records.conformance.ts +68 -0
- package/src/context/git/index.ts +2 -0
- package/src/context/index.ts +19 -0
- package/src/context/pii/index.ts +2 -0
- package/src/context/retrieval/index.ts +2 -0
- package/src/context/schema/index.ts +443 -0
- package/src/context/schema/schema.test.ts +251 -0
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
// Unit tests for the memory-record schema (slice S2). Run via
|
|
2
|
+
// `npm run test --workspace @nanobpm/urban` (node --test, src/**/*.test.ts).
|
|
3
|
+
import { test } from "node:test";
|
|
4
|
+
import assert from "node:assert/strict";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
validateMemoryRecord,
|
|
8
|
+
isMemoryRecord,
|
|
9
|
+
assertMemoryRecord,
|
|
10
|
+
MemoryRecordValidationError,
|
|
11
|
+
checkInvariants,
|
|
12
|
+
isAuthoritative,
|
|
13
|
+
SCOPE_LADDER,
|
|
14
|
+
MEMORY_MODES,
|
|
15
|
+
PROVENANCES,
|
|
16
|
+
AUTHORITIES,
|
|
17
|
+
MEMORY_RECORD_SCHEMA_VERSION,
|
|
18
|
+
type MemoryRecord,
|
|
19
|
+
type ValidationResult,
|
|
20
|
+
} from "./index.ts";
|
|
21
|
+
|
|
22
|
+
function base(overrides: Record<string, unknown> = {}): Record<string, unknown> {
|
|
23
|
+
return {
|
|
24
|
+
schemaVersion: MEMORY_RECORD_SCHEMA_VERSION,
|
|
25
|
+
id: "rec-1",
|
|
26
|
+
scope: "repo",
|
|
27
|
+
mode: "empirical",
|
|
28
|
+
provenance: "human",
|
|
29
|
+
authority: "authoritative",
|
|
30
|
+
statement: "the tests pass",
|
|
31
|
+
createdAt: "2026-01-01T00:00:00.000Z",
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test("accepts a well-formed record and narrows it", () => {
|
|
37
|
+
const result = validateMemoryRecord(base());
|
|
38
|
+
assert.equal(result.ok, true);
|
|
39
|
+
if (result.ok) {
|
|
40
|
+
const rec: MemoryRecord = result.record;
|
|
41
|
+
assert.equal(rec.id, "rec-1");
|
|
42
|
+
assert.equal(rec.schemaVersion, MEMORY_RECORD_SCHEMA_VERSION);
|
|
43
|
+
assert.equal(isAuthoritative(rec), true);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("accepts optional fields when well-typed and drops absent ones", () => {
|
|
48
|
+
const result = validateMemoryRecord(base({ provenance: "measured", scopeRef: "elem-1", subject: "latency", evidence: ["run-1"], supersedes: "rec-0" }));
|
|
49
|
+
assert.equal(result.ok, true);
|
|
50
|
+
if (result.ok) {
|
|
51
|
+
assert.equal(result.record.scopeRef, "elem-1");
|
|
52
|
+
assert.deepEqual(result.record.evidence, ["run-1"]);
|
|
53
|
+
assert.equal("subject" in result.record, true);
|
|
54
|
+
}
|
|
55
|
+
const bare = validateMemoryRecord(base());
|
|
56
|
+
if (bare.ok) {
|
|
57
|
+
assert.equal("scopeRef" in bare.record, false);
|
|
58
|
+
assert.equal("evidence" in bare.record, false);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("rejects a non-object with a clear error", () => {
|
|
63
|
+
const result = validateMemoryRecord(42);
|
|
64
|
+
assert.equal(result.ok, false);
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
assert.equal(result.errors[0]?.code, "not-an-object");
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("never throws on hostile inputs: throwing getters and exotic prototypes are rejected, not read", () => {
|
|
71
|
+
// A throwing getter must not be invoked — it is rejected as not-an-object.
|
|
72
|
+
const withThrowingGetter = {
|
|
73
|
+
get id(): string {
|
|
74
|
+
throw new Error("hostile getter must never be read");
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
let hostile: ValidationResult | undefined;
|
|
78
|
+
assert.doesNotThrow(() => {
|
|
79
|
+
hostile = validateMemoryRecord(withThrowingGetter);
|
|
80
|
+
});
|
|
81
|
+
assert.equal(hostile?.ok, false);
|
|
82
|
+
if (hostile && !hostile.ok) {
|
|
83
|
+
assert.equal(hostile.errors[0]?.code, "not-an-object");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// A non-plain prototype (class instance) is likewise rejected up front.
|
|
87
|
+
class Exotic {
|
|
88
|
+
id = "";
|
|
89
|
+
}
|
|
90
|
+
const proto = validateMemoryRecord(new Exotic());
|
|
91
|
+
assert.equal(proto.ok, false);
|
|
92
|
+
if (!proto.ok) {
|
|
93
|
+
assert.equal(proto.errors[0]?.code, "not-an-object");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// A null-prototype object with only data properties is still a plain object.
|
|
97
|
+
const nullProto: Record<string, unknown> = Object.create(null);
|
|
98
|
+
nullProto.schemaVersion = MEMORY_RECORD_SCHEMA_VERSION;
|
|
99
|
+
assert.doesNotThrow(() => validateMemoryRecord(nullProto));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("reports every problem, not just the first", () => {
|
|
103
|
+
const result = validateMemoryRecord({ schemaVersion: 9, scope: "galaxy", mode: "x" });
|
|
104
|
+
assert.equal(result.ok, false);
|
|
105
|
+
if (!result.ok) {
|
|
106
|
+
assert.ok(result.errors.length >= 3);
|
|
107
|
+
const codes = new Set(result.errors.map((e) => e.code));
|
|
108
|
+
assert.ok(codes.has("unsupported-schema-version"));
|
|
109
|
+
assert.ok(codes.has("invalid-vocabulary"));
|
|
110
|
+
assert.ok(codes.has("missing-field"));
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("distinguishes missing, wrong-typed, and unsupported schemaVersion", () => {
|
|
115
|
+
const missing = validateMemoryRecord(base({ schemaVersion: undefined }));
|
|
116
|
+
assert.equal(missing.ok, false);
|
|
117
|
+
if (!missing.ok) {
|
|
118
|
+
assert.equal(missing.errors.find((e) => e.path === "schemaVersion")?.code, "missing-field");
|
|
119
|
+
}
|
|
120
|
+
const wrongType = validateMemoryRecord(base({ schemaVersion: "1" }));
|
|
121
|
+
assert.equal(wrongType.ok, false);
|
|
122
|
+
if (!wrongType.ok) {
|
|
123
|
+
assert.equal(wrongType.errors.find((e) => e.path === "schemaVersion")?.code, "wrong-type");
|
|
124
|
+
}
|
|
125
|
+
const unsupported = validateMemoryRecord(base({ schemaVersion: 9 }));
|
|
126
|
+
assert.equal(unsupported.ok, false);
|
|
127
|
+
if (!unsupported.ok) {
|
|
128
|
+
assert.equal(unsupported.errors.find((e) => e.path === "schemaVersion")?.code, "unsupported-schema-version");
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("rejects drifted vocabulary on each controlled field", () => {
|
|
133
|
+
assert.equal(validateMemoryRecord(base({ scope: "galaxy" })).ok, false);
|
|
134
|
+
assert.equal(validateMemoryRecord(base({ mode: "prescriptive" })).ok, false);
|
|
135
|
+
assert.equal(validateMemoryRecord(base({ provenance: "robot" })).ok, false);
|
|
136
|
+
assert.equal(validateMemoryRecord(base({ authority: "supreme" })).ok, false);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("distinguishes wrong-type from drifted vocabulary on controlled fields", () => {
|
|
140
|
+
for (const field of ["scope", "mode", "provenance", "authority"]) {
|
|
141
|
+
const wrongType = validateMemoryRecord(base({ [field]: 42 }));
|
|
142
|
+
assert.equal(wrongType.ok, false);
|
|
143
|
+
if (!wrongType.ok) {
|
|
144
|
+
const err = wrongType.errors.find((e) => e.path === field);
|
|
145
|
+
assert.equal(err?.code, "wrong-type", `${field}: a non-string must report wrong-type, not invalid-vocabulary`);
|
|
146
|
+
}
|
|
147
|
+
const drifted = validateMemoryRecord(base({ [field]: "not-in-ladder" }));
|
|
148
|
+
assert.equal(drifted.ok, false);
|
|
149
|
+
if (!drifted.ok) {
|
|
150
|
+
const err = drifted.errors.find((e) => e.path === field);
|
|
151
|
+
assert.equal(err?.code, "invalid-vocabulary", `${field}: a drifted string must report invalid-vocabulary`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("distinguishes empty-string from wrong-type on optional non-empty-string fields", () => {
|
|
157
|
+
for (const field of ["scopeRef", "supersedes"]) {
|
|
158
|
+
const empty = validateMemoryRecord(base({ [field]: "" }));
|
|
159
|
+
assert.equal(empty.ok, false);
|
|
160
|
+
if (!empty.ok) {
|
|
161
|
+
const err = empty.errors.find((e) => e.path === field);
|
|
162
|
+
assert.equal(err?.code, "empty-string", `${field}: an empty string is a valid type and must report empty-string, not wrong-type`);
|
|
163
|
+
}
|
|
164
|
+
const wrongType = validateMemoryRecord(base({ [field]: 42 }));
|
|
165
|
+
assert.equal(wrongType.ok, false);
|
|
166
|
+
if (!wrongType.ok) {
|
|
167
|
+
const err = wrongType.errors.find((e) => e.path === field);
|
|
168
|
+
assert.equal(err?.code, "wrong-type", `${field}: a non-string must report wrong-type`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("rejects non-ISO timestamps but accepts offset zones", () => {
|
|
174
|
+
assert.equal(validateMemoryRecord(base({ createdAt: "yesterday" })).ok, false);
|
|
175
|
+
assert.equal(validateMemoryRecord(base({ createdAt: "2026-01-01" })).ok, false);
|
|
176
|
+
assert.equal(validateMemoryRecord(base({ createdAt: "2026-01-01T12:00:00+13:00" })).ok, true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- The hypothesis-never-fix invariant, at every scope -------------------
|
|
180
|
+
|
|
181
|
+
test("INVARIANT: agent-retro can never be authoritative, at any scope", () => {
|
|
182
|
+
for (const scope of SCOPE_LADDER) {
|
|
183
|
+
for (const mode of MEMORY_MODES) {
|
|
184
|
+
const result = validateMemoryRecord(base({ scope, mode, provenance: "agent-retro", authority: "authoritative" }));
|
|
185
|
+
assert.equal(result.ok, false, `agent-retro/authoritative must be rejected at ${scope}/${mode}`);
|
|
186
|
+
if (!result.ok) {
|
|
187
|
+
assert.ok(result.errors.some((e) => e.code === "invariant-violation"));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test("agent-retro as a hypothesis is accepted at every scope and mode", () => {
|
|
194
|
+
for (const scope of SCOPE_LADDER) {
|
|
195
|
+
for (const mode of MEMORY_MODES) {
|
|
196
|
+
assert.equal(validateMemoryRecord(base({ scope, mode, provenance: "agent-retro", authority: "hypothesis" })).ok, true);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("measured/instance must be empirical + authoritative", () => {
|
|
202
|
+
assert.equal(validateMemoryRecord(base({ provenance: "measured", authority: "hypothesis" })).ok, false);
|
|
203
|
+
assert.equal(validateMemoryRecord(base({ provenance: "measured", mode: "normative", authority: "authoritative" })).ok, false);
|
|
204
|
+
assert.equal(validateMemoryRecord(base({ provenance: "instance", authority: "hypothesis" })).ok, false);
|
|
205
|
+
assert.equal(validateMemoryRecord(base({ provenance: "instance", mode: "normative", authority: "authoritative" })).ok, false);
|
|
206
|
+
assert.equal(validateMemoryRecord(base({ provenance: "measured", mode: "empirical", authority: "authoritative" })).ok, true);
|
|
207
|
+
assert.equal(validateMemoryRecord(base({ provenance: "instance", mode: "empirical", authority: "authoritative" })).ok, true);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("only human/agent-retro may author a normative record", () => {
|
|
211
|
+
assert.equal(validateMemoryRecord(base({ mode: "normative", provenance: "human", authority: "hypothesis" })).ok, true);
|
|
212
|
+
assert.equal(validateMemoryRecord(base({ mode: "normative", provenance: "agent-retro", authority: "hypothesis" })).ok, true);
|
|
213
|
+
assert.equal(validateMemoryRecord(base({ mode: "normative", provenance: "measured", authority: "authoritative" })).ok, false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("checkInvariants: the only route to authoritative excludes agent-retro", () => {
|
|
217
|
+
for (const provenance of PROVENANCES) {
|
|
218
|
+
for (const mode of MEMORY_MODES) {
|
|
219
|
+
const errs = checkInvariants({ provenance, mode, authority: "authoritative" });
|
|
220
|
+
if (provenance === "agent-retro") {
|
|
221
|
+
assert.ok(errs.length > 0, "agent-retro must never validate as authoritative");
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("assertMemoryRecord throws MemoryRecordValidationError carrying structured errors", () => {
|
|
228
|
+
assert.throws(
|
|
229
|
+
() => assertMemoryRecord(base({ provenance: "agent-retro", authority: "authoritative" })),
|
|
230
|
+
(err: unknown) => {
|
|
231
|
+
assert.ok(err instanceof MemoryRecordValidationError);
|
|
232
|
+
assert.ok(err.errors.length > 0);
|
|
233
|
+
return true;
|
|
234
|
+
},
|
|
235
|
+
);
|
|
236
|
+
const ok = assertMemoryRecord(base());
|
|
237
|
+
assert.equal(ok.id, "rec-1");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("isMemoryRecord is a usable type guard", () => {
|
|
241
|
+
const v: unknown = base();
|
|
242
|
+
assert.equal(isMemoryRecord(v), true);
|
|
243
|
+
assert.equal(isMemoryRecord(42), false);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("vocabulary arrays are the expected controlled sets", () => {
|
|
247
|
+
assert.deepEqual([...SCOPE_LADDER], ["element", "instance", "epic", "repo", "corpus"]);
|
|
248
|
+
assert.deepEqual([...MEMORY_MODES], ["normative", "empirical"]);
|
|
249
|
+
assert.deepEqual([...PROVENANCES], ["human", "agent-retro", "measured", "instance"]);
|
|
250
|
+
assert.deepEqual([...AUTHORITIES], ["hypothesis", "authoritative"]);
|
|
251
|
+
});
|