@openwop/openwop-conformance 2.0.4 → 2.0.6

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.
@@ -11,9 +11,27 @@
11
11
  *
12
12
  * An era-2 log carrying one `foo.bar` row is seeded through the event-log seed
13
13
  * seam (lib/era2-seed.ts); the read is driven through poll and, where `replay`
14
- * is advertised, through a fork. A vendor-prefixed control (read under its own
15
- * name) needs an org registered under `extensions` in `spec/v2/declaration.json`
16
- * that the suite does not own, so that half is not driven.
14
+ * is advertised, through a fork.
15
+ *
16
+ * THE CONTROL, and why it was missing. Until this revision only the REFUSAL
17
+ * half was driven, and a host that refuses EVERY type the codemap does not name
18
+ * — including a registered vendor type it is required to pass through — went
19
+ * green on the whole file while violating `persistence.md` §The codemap is data.
20
+ * A one-sided rule is not witnessed by its own negative case. The positive half
21
+ * needs an org registered in the `extensions` object of `spec/v2/declaration.json`
22
+ * that no host owns; the registry did not exist, so the suite could not drive
23
+ * it and this docblock recorded the gap instead of closing it. `extensions` now
24
+ * exists and holds `example`, reserved by the protocol and never assignable to
25
+ * a vendor (RFC 2606 precedent), so `example.thing-happened` is a type that is
26
+ * registered, unmapped, and owned by nobody — exactly the control this file
27
+ * lacked. The two legs are each other's non-vacuity check: refusing both is a
28
+ * defect, accepting both is a defect, and only the pair can tell them apart.
29
+ *
30
+ * BECAUSE THEY CHECK OPPOSITE HALVES, THEY GATE SEPARATELY (`refusalGate` /
31
+ * `controlGate`). Suite 2.0.5 gated both on the control leg's precondition and
32
+ * so disabled the refusal witness wherever the registry did not resolve — on
33
+ * exactly the hosts it exists to catch. A precondition belongs to the leg that
34
+ * needs it, never to the file.
17
35
  *
18
36
  * @see spec/v2/core/persistence.md §The reader rule
19
37
  * @see spec/v2/core/events.md §Reading an era-2 log
@@ -24,11 +42,26 @@ import { v2Discovery, gateFamily } from '../lib/v2.js';
24
42
  import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
25
43
  import { softSkip } from '../lib/soft-skip.js';
26
44
  import { req } from '../lib/requirement-ids.js';
27
- import { era2Gate, eventsOf, forkRun, pollEvents, seedEra2Log, type SeedEvent } from '../lib/era2-seed.js';
45
+ import { codemapV1toV2, era2Gate, eventsOf, forkRun, pollEvents, registeredOrgs, seedEra2Log, unmappedRefusalGate, vendorControlGate, type Gate, type SeedEvent } from '../lib/era2-seed.js';
28
46
 
29
47
  const DOC = 'spec/v2/core/persistence.md §The reader rule';
48
+ const MAP = 'spec/v2/core/persistence.md §The codemap is data';
30
49
  const CODE = 'event_type_unmapped';
50
+ const ID_VENDOR = 'openwop.requirement.0176.vendor-type-passthrough';
31
51
  const UNMAPPED = 'foo.bar';
52
+ /** Registered (`extensions` in the declaration), unmapped, owned by no host. */
53
+ const VENDOR = 'example.thing-happened';
54
+
55
+ /**
56
+ * The gate logic is PURE and lives in lib/era2-seed (`unmappedRefusalGate` /
57
+ * `vendorControlGate`) so its truth table can be measured directly — see
58
+ * `src/lib/era2-unmapped-gates.test.ts`. The rows that matter cannot be
59
+ * exercised from here: they are the layouts where the registry does NOT
60
+ * resolve, which is where the 2.0.5 defect lived and where a scenario running
61
+ * against a live host can never put itself.
62
+ */
63
+ const refusalGate = (): Gate => unmappedRefusalGate(registeredOrgs(), codemapV1toV2(), UNMAPPED);
64
+ const controlGate = (): Gate => vendorControlGate(registeredOrgs(), codemapV1toV2(), VENDOR);
32
65
 
33
66
  async function discovery(): Promise<Record<string, unknown> | null> {
34
67
  try { return await v2Discovery(); } catch { return null; }
@@ -39,7 +72,18 @@ function unmappedLog(): SeedEvent[] {
39
72
  const ts = (i: number) => new Date(t0 + i * 1000).toISOString();
40
73
  return [
41
74
  { type: 'run.started', sequence: 0, payload: { workflowId: 'conformance-noop' }, timestamp: ts(0) },
42
- { type: UNMAPPED, sequence: 1, payload: { note: 'not a codemap row, not a vendor org' }, timestamp: ts(1) },
75
+ { type: UNMAPPED, sequence: 1, payload: {}, timestamp: ts(1) },
76
+ { type: 'run.completed', sequence: 2, payload: { durationMs: 2000 }, timestamp: ts(2) },
77
+ ];
78
+ }
79
+
80
+ /** The same shape carrying a REGISTERED vendor type instead of an unmapped one. */
81
+ function vendorLog(): SeedEvent[] {
82
+ const t0 = Date.parse('2026-01-15T12:00:00.000Z');
83
+ const ts = (i: number) => new Date(t0 + i * 1000).toISOString();
84
+ return [
85
+ { type: 'run.started', sequence: 0, payload: { workflowId: 'conformance-noop' }, timestamp: ts(0) },
86
+ { type: VENDOR, sequence: 1, payload: { marker: 'era2-vendor-passthrough' }, timestamp: ts(1) },
43
87
  { type: 'run.completed', sequence: 2, payload: { durationMs: 2000 }, timestamp: ts(2) },
44
88
  ];
45
89
  }
@@ -50,6 +94,8 @@ describe('RFC 0176 §A.3 — unmapped-type-refused (seam-gated)', () => {
50
94
  if (!doc) return softSkip('blocked', 'discovery unreachable');
51
95
  const gate = era2Gate(doc);
52
96
  if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
97
+ const pre = refusalGate();
98
+ if (!pre.ok) return softSkip(pre.kind, pre.reason);
53
99
  const seeded = await seedEra2Log(unmappedLog(), 'completed');
54
100
  if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);
55
101
  const res = await pollEvents(seeded.runId);
@@ -68,12 +114,41 @@ describe('RFC 0176 §A.3 — unmapped-type-refused (seam-gated)', () => {
68
114
  ).toBe(false);
69
115
  });
70
116
 
117
+ it('a REGISTERED vendor type the codemap does not name is read under its own name, unchanged', async () => {
118
+ const doc = await discovery();
119
+ if (!doc) return softSkip('blocked', 'discovery unreachable');
120
+ const gate = era2Gate(doc);
121
+ if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
122
+ const pre = controlGate();
123
+ if (!pre.ok) return softSkip(pre.kind, pre.reason);
124
+ const seeded = await seedEra2Log(vendorLog(), 'completed');
125
+ if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);
126
+ const res = await pollEvents(seeded.runId);
127
+ if (res === null) return softSkip('blocked', 'GET /runs/{runId}/events/poll unreachable (fetch failed)');
128
+ // The whole point of the control: this is the SAME log shape as the refusal
129
+ // leg with one segment changed, so a 500 here says the host is refusing on
130
+ // "not in the codemap" and not on "not a registered org" — the tolerant
131
+ // reader's mirror image, and just as wrong.
132
+ expect(
133
+ res.status,
134
+ req(ID_VENDOR, MAP, `a vendor-prefixed type whose org IS registered MUST be read under its own name unchanged, not refused — got ${res.status}${res.status === 500 ? ` ${readErrorCode(res.json) ?? ''}: the host refuses every type the codemap does not name, which fails the registered half of the rule` : ''}`.trim()),
135
+ ).toBe(200);
136
+ const types = eventsOf(res.json).map((e) => String(e.type));
137
+ expect(
138
+ types.includes(VENDOR),
139
+ req(ID_VENDOR, MAP, `the ${VENDOR} row MUST appear under its own name — read back [${types.join(', ')}]; a translated or dropped row is a private mapping, and the codemap is the only authority`),
140
+ ).toBe(true);
141
+ });
142
+
71
143
  it('a fork of the same log is refused for the same reason — the rule binds every reader', async () => {
72
144
  const doc = await discovery();
73
145
  if (!doc) return softSkip('blocked', 'discovery unreachable');
74
146
  const gate = era2Gate(doc);
75
147
  if (gate !== null && !gate.ok) return softSkip(gate.kind, gate.reason);
76
148
  if (!(await gateFamily('replay'))) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay) — the fork reader has no surface');
149
+ // The fork leg drives the UNMAPPED type, so it takes the refusal gate.
150
+ const pre = refusalGate();
151
+ if (!pre.ok) return softSkip(pre.kind, pre.reason);
77
152
  const log = unmappedLog();
78
153
  const seeded = await seedEra2Log(log, 'completed');
79
154
  if (!seeded.ok) return softSkip(seeded.kind, seeded.reason);