@nanobpm/urban 0.57.0 → 0.59.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nanobpm/urban",
3
- "version": "0.57.0",
3
+ "version": "0.59.0",
4
4
  "description": "Urban: build and run code-first apps on Nano — runtime, derivation toolkit, and CLI in one. Author durable processes with @nanobpm/workflow (defineFlow), a typed datasource, forms, triggers and surfaces from a nano.app.json manifest, on Node or Deno.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,2 +1,285 @@
1
- // Slice S2 (conformance corpus + fixtures) — owner replaces this placeholder barrel.
2
- export {};
1
+ // Slice S2 (conformance) — the shared conformance corpus + a reusable runner.
2
+ //
3
+ // Published as `@nanobpm/urban/context/conformance`. This subpath IS the
4
+ // contract: BOTH the producer (this repo) and the consumer
5
+ // (`nanobpm/nano-workforce#291`) are held to the SAME corpus, and the
6
+ // adversarial slice (S7) imports the corpus + runner from here directly. It is
7
+ // intentionally self-contained — it does NOT re-export the schema module, so a
8
+ // consumer can import it without pulling the whole schema surface, and the top
9
+ // barrel never sees a duplicate symbol.
10
+ //
11
+ // The runner is a PURE function over a validator callback (no test framework),
12
+ // so any party can point it at their own validator and get a structured report.
13
+ // The `.conformance.ts` sibling wires it into `node:test` for `test:conformance`.
14
+
15
+ import { MEMORY_RECORD_SCHEMA_VERSION, SCOPE_LADDER } from "../schema/index.ts";
16
+
17
+ // ---------------------------------------------------------------------------
18
+ // Public shapes
19
+ // ---------------------------------------------------------------------------
20
+
21
+ /** Whether the corpus expects a validator to accept or reject an input. */
22
+ export type ConformanceExpectation = "accept" | "reject";
23
+
24
+ /** A single corpus entry: an input and the outcome every conformant validator must produce. */
25
+ export interface ConformanceCase {
26
+ /** Unique, stable, human-readable case name. */
27
+ readonly name: string;
28
+ /** The required outcome. */
29
+ readonly expect: ConformanceExpectation;
30
+ /** The (untrusted) value fed to the validator. */
31
+ readonly input: unknown;
32
+ /** Why this case exists / what rule it exercises. */
33
+ readonly note: string;
34
+ }
35
+
36
+ /**
37
+ * The minimal validator contract the corpus tests. A conformant validator maps
38
+ * an untrusted value to an accept/reject decision via `{ ok }`.
39
+ * `validateMemoryRecord` (result form) adapts to this shape via
40
+ * {@link fromResultValidator}; the boolean `isMemoryRecord` conforms by wrapping
41
+ * its result — `(input) => ({ ok: isMemoryRecord(input) })`.
42
+ */
43
+ export type ConformanceValidator = (input: unknown) => { readonly ok: boolean };
44
+
45
+ /** The outcome of running one case through a validator. */
46
+ export interface ConformanceCaseResult {
47
+ readonly case: ConformanceCase;
48
+ readonly expected: ConformanceExpectation;
49
+ readonly actual: ConformanceExpectation;
50
+ readonly passed: boolean;
51
+ }
52
+
53
+ /** The aggregate report from {@link runConformance}. */
54
+ export interface ConformanceReport {
55
+ readonly total: number;
56
+ readonly passed: number;
57
+ readonly failed: number;
58
+ readonly results: readonly ConformanceCaseResult[];
59
+ readonly failures: readonly ConformanceCaseResult[];
60
+ }
61
+
62
+ // ---------------------------------------------------------------------------
63
+ // Corpus construction
64
+ // ---------------------------------------------------------------------------
65
+
66
+ interface Triple {
67
+ readonly mode: string;
68
+ readonly provenance: string;
69
+ readonly authority: string;
70
+ }
71
+
72
+ /**
73
+ * Every (provenance, mode, authority) triple that is VALID by the schema
74
+ * invariants. Instantiated across all scopes below, this covers every
75
+ * scope × provenance × mode combination that a well-formed record can take.
76
+ */
77
+ const VALID_TRIPLES: readonly Triple[] = [
78
+ { provenance: "human", mode: "normative", authority: "hypothesis" },
79
+ { provenance: "human", mode: "normative", authority: "authoritative" },
80
+ { provenance: "human", mode: "empirical", authority: "hypothesis" },
81
+ { provenance: "human", mode: "empirical", authority: "authoritative" },
82
+ { provenance: "agent-retro", mode: "normative", authority: "hypothesis" },
83
+ { provenance: "agent-retro", mode: "empirical", authority: "hypothesis" },
84
+ { provenance: "measured", mode: "empirical", authority: "authoritative" },
85
+ { provenance: "instance", mode: "empirical", authority: "authoritative" },
86
+ ];
87
+
88
+ /**
89
+ * Every (provenance, mode, authority) triple that MUST be rejected by the
90
+ * invariants, with the rule it violates. The first two are the headline
91
+ * hypothesis-never-fix invariant (agent-retro can never be authoritative).
92
+ */
93
+ const INVALID_TRIPLES: readonly (Triple & { readonly why: string })[] = [
94
+ { provenance: "agent-retro", mode: "normative", authority: "authoritative", why: "hypothesis-never-fix: agent-retro can never be authoritative" },
95
+ { provenance: "agent-retro", mode: "empirical", authority: "authoritative", why: "hypothesis-never-fix: agent-retro can never be authoritative" },
96
+ { provenance: "measured", mode: "empirical", authority: "hypothesis", why: "measured must be authoritative" },
97
+ { provenance: "measured", mode: "normative", authority: "authoritative", why: "measured must be empirical, not normative" },
98
+ { provenance: "instance", mode: "empirical", authority: "hypothesis", why: "instance must be authoritative" },
99
+ { provenance: "instance", mode: "normative", authority: "authoritative", why: "instance must be empirical, not normative" },
100
+ ];
101
+
102
+ function baseRecord(scope: string, triple: Triple): Record<string, unknown> {
103
+ return {
104
+ schemaVersion: MEMORY_RECORD_SCHEMA_VERSION,
105
+ id: `rec-${scope}-${triple.provenance}-${triple.mode}-${triple.authority}`,
106
+ scope,
107
+ mode: triple.mode,
108
+ provenance: triple.provenance,
109
+ authority: triple.authority,
110
+ statement: `example ${triple.mode} ${triple.provenance} record at ${scope} scope`,
111
+ createdAt: "2026-01-01T00:00:00.000Z",
112
+ };
113
+ }
114
+
115
+ function buildCombinationalCases(): readonly ConformanceCase[] {
116
+ const cases: ConformanceCase[] = [];
117
+ for (const scope of SCOPE_LADDER) {
118
+ for (const triple of VALID_TRIPLES) {
119
+ cases.push({
120
+ name: `accept ${scope}/${triple.mode}/${triple.provenance}/${triple.authority}`,
121
+ expect: "accept",
122
+ input: baseRecord(scope, triple),
123
+ note: "well-formed record; valid vocabulary + consistent invariants",
124
+ });
125
+ }
126
+ for (const triple of INVALID_TRIPLES) {
127
+ cases.push({
128
+ name: `reject ${scope}/${triple.mode}/${triple.provenance}/${triple.authority}`,
129
+ expect: "reject",
130
+ input: baseRecord(scope, triple),
131
+ note: triple.why,
132
+ });
133
+ }
134
+ }
135
+ return cases;
136
+ }
137
+
138
+ /** Structural / vocabulary-drift rejects that are independent of scope. */
139
+ const STRUCTURAL_CASES: readonly ConformanceCase[] = [
140
+ { name: "reject non-object (number)", expect: "reject", input: 42, note: "record must be an object" },
141
+ { name: "reject non-object (null)", expect: "reject", input: null, note: "record must be an object" },
142
+ { name: "reject non-object (array)", expect: "reject", input: [], note: "record must be a plain object, not an array" },
143
+ {
144
+ name: "reject unsupported schemaVersion",
145
+ expect: "reject",
146
+ input: { schemaVersion: 2, id: "x", scope: "repo", mode: "empirical", provenance: "human", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
147
+ note: "schemaVersion must be exactly 1",
148
+ },
149
+ {
150
+ name: "reject missing schemaVersion",
151
+ expect: "reject",
152
+ input: { id: "x", scope: "repo", mode: "empirical", provenance: "human", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
153
+ note: "schemaVersion is required",
154
+ },
155
+ {
156
+ name: "reject missing id",
157
+ expect: "reject",
158
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, scope: "repo", mode: "empirical", provenance: "human", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
159
+ note: "id is required",
160
+ },
161
+ {
162
+ name: "reject empty statement",
163
+ expect: "reject",
164
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "repo", mode: "empirical", provenance: "human", authority: "authoritative", statement: "", createdAt: "2026-01-01T00:00:00.000Z" },
165
+ note: "statement must be non-empty",
166
+ },
167
+ {
168
+ name: "reject drifted scope vocabulary",
169
+ expect: "reject",
170
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "galaxy", mode: "empirical", provenance: "human", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
171
+ note: "scope must be in the ladder vocabulary",
172
+ },
173
+ {
174
+ name: "reject drifted mode vocabulary",
175
+ expect: "reject",
176
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "repo", mode: "prescriptive", provenance: "human", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
177
+ note: "mode must be normative|empirical",
178
+ },
179
+ {
180
+ name: "reject drifted provenance vocabulary",
181
+ expect: "reject",
182
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "repo", mode: "empirical", provenance: "robot", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
183
+ note: "provenance must be in the vocabulary",
184
+ },
185
+ {
186
+ name: "reject drifted authority vocabulary",
187
+ expect: "reject",
188
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "repo", mode: "empirical", provenance: "human", authority: "supreme", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
189
+ note: "authority must be hypothesis|authoritative",
190
+ },
191
+ {
192
+ name: "reject non-ISO timestamp",
193
+ expect: "reject",
194
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "repo", mode: "empirical", provenance: "human", authority: "authoritative", statement: "s", createdAt: "yesterday" },
195
+ note: "createdAt must be ISO-8601",
196
+ },
197
+ {
198
+ name: "reject wrong-typed id",
199
+ expect: "reject",
200
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: 123, scope: "repo", mode: "empirical", provenance: "human", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z" },
201
+ note: "id must be a string",
202
+ },
203
+ {
204
+ name: "reject wrong-typed evidence",
205
+ expect: "reject",
206
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "repo", mode: "empirical", provenance: "measured", authority: "authoritative", statement: "s", createdAt: "2026-01-01T00:00:00.000Z", evidence: "not-an-array" },
207
+ note: "evidence, if present, must be a string array",
208
+ },
209
+ {
210
+ name: "accept well-formed measured record with evidence + optionals",
211
+ expect: "accept",
212
+ input: { schemaVersion: MEMORY_RECORD_SCHEMA_VERSION, id: "x", scope: "instance", scopeRef: "elem-7", subject: "latency", mode: "empirical", provenance: "measured", authority: "authoritative", statement: "p99 improved", createdAt: "2026-01-01T00:00:00.000Z", evidence: ["run-1", "run-2"], supersedes: "rec-old" },
213
+ note: "optional fields accepted when well-typed",
214
+ },
215
+ ];
216
+
217
+ /**
218
+ * The full conformance corpus: exhaustive scope × valid/invalid triples, plus
219
+ * structural and vocabulary-drift cases. Deterministic and stable — treat it as
220
+ * the versioned contract fixture both producer and consumer are held to.
221
+ */
222
+ export const corpus: readonly ConformanceCase[] = [...buildCombinationalCases(), ...STRUCTURAL_CASES];
223
+
224
+ /** The subset of the corpus that must be accepted. */
225
+ export const validFixtures: readonly ConformanceCase[] = corpus.filter((c) => c.expect === "accept");
226
+
227
+ /** The subset of the corpus that must be rejected. */
228
+ export const invalidFixtures: readonly ConformanceCase[] = corpus.filter((c) => c.expect === "reject");
229
+
230
+ // ---------------------------------------------------------------------------
231
+ // The runner
232
+ // ---------------------------------------------------------------------------
233
+
234
+ /**
235
+ * Adapt a `{ ok } | { ... }`-returning validator (like `validateMemoryRecord`)
236
+ * to the {@link ConformanceValidator} shape. Any function returning an object
237
+ * with a boolean `ok` already conforms; this is a typed identity helper.
238
+ */
239
+ export function fromResultValidator(validate: (input: unknown) => { readonly ok: boolean }): ConformanceValidator {
240
+ return (input: unknown) => ({ ok: validate(input).ok });
241
+ }
242
+
243
+ /**
244
+ * Run `validate` against the corpus (or a supplied subset) and return a
245
+ * structured report. Pure: does not throw and does not depend on a test runner.
246
+ */
247
+ export function runConformance(
248
+ validate: ConformanceValidator,
249
+ cases: readonly ConformanceCase[] = corpus,
250
+ ): ConformanceReport {
251
+ const results: ConformanceCaseResult[] = cases.map((c) => {
252
+ // A validator that throws (even accidentally) must not crash the run: treat
253
+ // a throw as a rejection so the runner stays pure and the doc above holds.
254
+ let ok: boolean;
255
+ try {
256
+ ok = validate(c.input).ok;
257
+ } catch {
258
+ ok = false;
259
+ }
260
+ const actual: ConformanceExpectation = ok ? "accept" : "reject";
261
+ return { case: c, expected: c.expect, actual, passed: actual === c.expect };
262
+ });
263
+ const failures = results.filter((r) => !r.passed);
264
+ return {
265
+ total: results.length,
266
+ passed: results.length - failures.length,
267
+ failed: failures.length,
268
+ results,
269
+ failures,
270
+ };
271
+ }
272
+
273
+ /**
274
+ * Assert full conformance: throws with a readable summary if any case fails.
275
+ * Consumers can call this in their own test suite to be held to the contract.
276
+ */
277
+ export function assertConformance(validate: ConformanceValidator, cases: readonly ConformanceCase[] = corpus): void {
278
+ const report = runConformance(validate, cases);
279
+ if (report.failed > 0) {
280
+ const detail = report.failures
281
+ .map((f) => ` - ${f.case.name}: expected ${f.expected}, got ${f.actual} (${f.case.note})`)
282
+ .join("\n");
283
+ throw new Error(`conformance failed: ${report.failed}/${report.total} case(s):\n${detail}`);
284
+ }
285
+ }
@@ -0,0 +1,68 @@
1
+ // Runnable conformance suite — matched by the pre-declared `test:conformance`
2
+ // glob (src/context/conformance/**/*.conformance.ts). It holds THIS repo's
3
+ // producer validator (`validateMemoryRecord`) to the shared corpus. The consumer
4
+ // (nano-workforce#291) imports the same `corpus` + `runConformance`/
5
+ // `assertConformance` and runs it against its own validator, so both sides prove
6
+ // they agree on the exact same fixtures.
7
+ import { test } from "node:test";
8
+ import assert from "node:assert/strict";
9
+
10
+ import { validateMemoryRecord } from "../schema/index.ts";
11
+ import {
12
+ corpus,
13
+ runConformance,
14
+ assertConformance,
15
+ fromResultValidator,
16
+ validFixtures,
17
+ invalidFixtures,
18
+ type ConformanceValidator,
19
+ type ConformanceReport,
20
+ } from "./index.ts";
21
+
22
+ const validator = fromResultValidator(validateMemoryRecord);
23
+
24
+ test("corpus is non-trivial and partitioned", () => {
25
+ assert.ok(corpus.length >= 50, `expected a substantial corpus, got ${corpus.length}`);
26
+ assert.ok(validFixtures.length > 0);
27
+ assert.ok(invalidFixtures.length > 0);
28
+ assert.equal(validFixtures.length + invalidFixtures.length, corpus.length);
29
+ // Case names must be unique so failures are unambiguous.
30
+ assert.equal(new Set(corpus.map((c) => c.name)).size, corpus.length);
31
+ });
32
+
33
+ test("producer validator satisfies the full conformance corpus", () => {
34
+ const report = runConformance(validator);
35
+ assert.equal(report.failed, 0, `failures:\n${report.failures.map((f) => `${f.case.name}: expected ${f.expected}, got ${f.actual}`).join("\n")}`);
36
+ assert.equal(report.passed, report.total);
37
+ });
38
+
39
+ test("assertConformance does not throw for the producer validator", () => {
40
+ assert.doesNotThrow(() => assertConformance(validator));
41
+ });
42
+
43
+ test("runConformance treats a throwing validator as a rejection instead of crashing", () => {
44
+ const throwing: ConformanceValidator = () => {
45
+ throw new Error("boom");
46
+ };
47
+ // The runner must stay pure: no throw escapes, and every case is scored `reject`.
48
+ let report: ConformanceReport | undefined;
49
+ assert.doesNotThrow(() => {
50
+ report = runConformance(throwing);
51
+ });
52
+ assert.ok(report);
53
+ if (report) {
54
+ assert.equal(report.total, corpus.length);
55
+ // Every case scores `reject`; only the corpus's reject-cases can pass.
56
+ const rejectCount = corpus.filter((c) => c.expect === "reject").length;
57
+ assert.equal(report.passed, rejectCount);
58
+ assert.equal(report.results.every((r) => r.actual === "reject"), true);
59
+ }
60
+ });
61
+
62
+ // Per-case granularity: one sub-test per fixture so a regression names the exact case.
63
+ for (const c of corpus) {
64
+ test(`case: ${c.name}`, () => {
65
+ const actual = validator(c.input).ok ? "accept" : "reject";
66
+ assert.equal(actual, c.expect, `${c.name} — ${c.note}`);
67
+ });
68
+ }