@openwop/openwop-conformance 1.136.6 → 1.136.8

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": "@openwop/openwop-conformance",
3
- "version": "1.136.6",
3
+ "version": "1.136.8",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
- "suiteVersion": "1.136.6",
4
- "corpusCommit": "8f907101a7f1b6b630c0d1f2271b6e60d1e7c8fe"
3
+ "suiteVersion": "1.136.8",
4
+ "corpusCommit": "b8114f3d820af7f377c332c977d5cac93fe7bb05"
5
5
  }
@@ -3,8 +3,26 @@
3
3
  * immutable recorded fact: a `replay`-mode fork MUST NOT mint a new
4
4
  * `memoryId` for a write the source run already recorded. This asserts the
5
5
  * "MUST NOT regenerate" half — every `memory.written` on a replayed run
6
- * reuses a `memoryId` the source run recorded (a compliant host that
7
- * suppresses re-mint on replay satisfies this vacuously with zero events).
6
+ * reuses a `memoryId` the source run recorded.
7
+ *
8
+ * H2 (2026-08-18): this file used to say, in this docstring, that "a
9
+ * compliant host that suppresses re-mint on replay satisfies this vacuously
10
+ * with zero events" — and it did: the assertion is a `for` loop over the
11
+ * replayed events, so a host that emits NONE passed trivially. Two hosts
12
+ * diverged under it and both stayed green (openwop-app re-emits the source's
13
+ * events; a tier-2 host suppressed them, which leaves the replayed log SHORT
14
+ * of the source's and breaks RFC 0041 §C byte-equivalence).
15
+ *
16
+ * The divergence is the spec's, not the hosts': RFC 0057 §D says the host
17
+ * "MUST re-emit the recorded events from the log" and then, in a
18
+ * non-normative implementation note in the same section, blesses the
19
+ * reference host for "suppress[ing] rather than re-emit[ting]". Resolving
20
+ * that contradiction is a normative decision (it plausibly makes one shipped
21
+ * host non-conformant), so this leg does NOT pick a side. What it stops
22
+ * doing is passing silently: an empty replay now records `blocked` naming
23
+ * the contradiction, per RFC 0148 §A — a leg that cannot observe MUST NOT
24
+ * read as a pass. Once §D is resolved, the winning side becomes an assertion
25
+ * here.
8
26
  *
9
27
  * Gated on `capabilities.memory.attribution.emitsWriteEvents`; soft-skips
10
28
  * when unadvertised, when the seeded run wrote no memory, or when the host
@@ -50,6 +68,19 @@ describe('memory-attribution-replay-stable (RFC 0057 §D)', () => {
50
68
  }
51
69
 
52
70
  const replayed = await memoryWrittenEvents(forkId);
71
+ if (replayed.length === 0) {
72
+ // NOT a pass. The source run recorded `memory.written` events and the
73
+ // replay carries none, so this host is on the "suppress" side of the
74
+ // RFC 0057 §D contradiction. Whether that is conformant is undecided;
75
+ // that it is unobserved here is not (RFC 0148 §A).
76
+ return softSkip(
77
+ 'blocked',
78
+ `replay emitted no memory.written while the source recorded ${recordedIds.size} — ` +
79
+ 'the host suppresses rather than re-emits. RFC 0057 §D requires re-emission in its ' +
80
+ 'normative half and blesses suppression in its implementation note; until that ' +
81
+ 'contradiction is resolved this leg records the divergence instead of passing on it.',
82
+ );
83
+ }
53
84
  for (const e of replayed) {
54
85
  const id = memoryIdOf(e.payload);
55
86
  expect(
@@ -0,0 +1,132 @@
1
+ /**
2
+ * spec-section-citations — server-free. A citation of the form
3
+ * `<doc>.md §"<Section>"` MUST resolve to a heading that exists in that doc.
4
+ *
5
+ * SP-04 (2026-08-18): `storage-adapters.md §"Claim acquisition"` was cited by
6
+ * FOUR artifacts — `production-profile.md` §Durability, RFC 0009's
7
+ * scenario-citation table, and the docstrings of `staleClaim.test.ts` and
8
+ * `restart-during-run.test.ts` — and the section did not exist. Nothing was
9
+ * checking, so a normative `MUST` ("Storage adapters MUST satisfy
10
+ * `storage-adapters.md` lease and event-log invariants") pointed at nothing for
11
+ * the life of RFC 0009.
12
+ *
13
+ * Scope is deliberately narrow. A corpus-wide sweep of this citation form finds
14
+ * ~1400 citations and ~260 that do not resolve under a heading-only matcher —
15
+ * a mix of genuinely dangling anchors and legitimate informal references to
16
+ * table rows and capability keys rather than headings. Triaging those is its own
17
+ * work item; gating the whole corpus on an untriaged sweep would either fail
18
+ * immediately or need an allowlist so large it would stop meaning anything.
19
+ * So this pins the docs whose section citations are load-bearing for the
20
+ * durability contract, and grows as other docs are triaged.
21
+ *
22
+ * @see spec/v1/storage-adapters.md §"Claim acquisition"
23
+ * @see spec/v1/production-profile.md §Durability
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { readFileSync, readdirSync, existsSync } from 'node:fs';
28
+ import { join } from 'node:path';
29
+ import { V1_DIR, SCENARIOS_DIR } from '../lib/paths.js';
30
+
31
+ /** Docs whose `§"Section"` citations are checked. Add a doc once its citations are triaged. */
32
+ const CHECKED_DOCS = ['storage-adapters.md', 'production-profile.md'] as const;
33
+
34
+ /** `<doc>.md §"Quoted Section"` or `<doc>.md §BareToken`. */
35
+ const CITATION = /([A-Za-z0-9._-]+)\.md\s*§\s*(?:"([^"\n]+)"|([A-Za-z][A-Za-z0-9.-]*))/g;
36
+
37
+ /**
38
+ * Strip decoration, a leading `§` (headings in this corpus carry their own), and
39
+ * trailing sentence punctuation — a docstring legitimately ends a sentence
40
+ * inside the quotes (`… §"Claim acquisition."`) and still names that section.
41
+ */
42
+ const normalize = (t: string): string =>
43
+ t
44
+ .replace(/[`*_"]/g, '')
45
+ .replace(/^§\s*/, '')
46
+ .replace(/[.,;:]+$/, '')
47
+ .trim()
48
+ .toLowerCase();
49
+
50
+ /** A heading matches its full text, or its lead token before an em-dash / period. */
51
+ function headingKeys(raw: string): string[] {
52
+ const h = normalize(raw);
53
+ const keys = new Set<string>([h]);
54
+ const dash = h.split(/\s+[—–-]\s+/)[0]?.trim();
55
+ if (dash) keys.add(dash);
56
+ const dot = h.split(/\.\s+/)[0]?.trim();
57
+ if (dot) keys.add(dot);
58
+ return [...keys];
59
+ }
60
+
61
+ function walk(dir: string, keep: (n: string) => boolean, out: string[] = []): string[] {
62
+ for (const e of readdirSync(dir, { withFileTypes: true })) {
63
+ const p = join(dir, e.name);
64
+ if (e.isDirectory()) walk(p, keep, out);
65
+ else if (keep(e.name)) out.push(p);
66
+ }
67
+ return out;
68
+ }
69
+
70
+ describe.skipIf(V1_DIR === null)('spec-section-citations (SP-04)', () => {
71
+ it('every cited section of a checked doc exists as a heading in that doc', () => {
72
+ const v1 = V1_DIR as string;
73
+ // `V1_DIR` is `<repo>/spec/v1` in a checkout; RFCS/ sits two levels up.
74
+ // Both are absent from the published tarball, which is why the whole file
75
+ // is `skipIf(V1_DIR === null)`.
76
+ const rfcsDir = join(v1, '..', '..', 'RFCS');
77
+
78
+ const sources = [
79
+ ...walk(v1, (n) => n.endsWith('.md')),
80
+ ...(existsSync(rfcsDir) ? walk(rfcsDir, (n) => n.endsWith('.md')) : []),
81
+ ...(SCENARIOS_DIR !== null ? walk(SCENARIOS_DIR, (n) => n.endsWith('.test.ts')) : []),
82
+ ];
83
+
84
+ const headings = new Map<string, Set<string>>();
85
+ for (const doc of CHECKED_DOCS) {
86
+ const text = readFileSync(join(v1, doc), 'utf8');
87
+ const keys = new Set<string>();
88
+ for (const m of text.matchAll(/^#{1,6}\s+(.+?)\s*$/gm)) {
89
+ for (const k of headingKeys(m[1] ?? '')) keys.add(k);
90
+ }
91
+ headings.set(doc, keys);
92
+ }
93
+
94
+ const dangling: string[] = [];
95
+ let checked = 0;
96
+ for (const src of sources) {
97
+ const text = readFileSync(src, 'utf8');
98
+ for (const m of text.matchAll(CITATION)) {
99
+ const doc = `${m[1]}.md`;
100
+ const keys = headings.get(doc);
101
+ if (!keys) continue;
102
+ const section = (m[2] ?? m[3] ?? '').trim();
103
+ if (!section) continue;
104
+ checked++;
105
+ const want = normalize(section);
106
+ // A MULTI-WORD citation must match a heading key exactly: prefix
107
+ // matching would let `§"Claim acquisition"` be satisfied by a heading
108
+ // named `Claim acquisition considered harmful`, which is how a renamed
109
+ // section slips past a gate like this (caught while sabotage-testing
110
+ // this very leg). A single-token citation — `§B`, `§C.2`,
111
+ // `§host.aiProviders` — legitimately prefixes a longer heading
112
+ // (`## §B — Channel resolution …`), so prefix matching stays for those.
113
+ const hit = keys.has(want)
114
+ || (!want.includes(' ') && [...keys].some((k) => k.startsWith(want)));
115
+ if (!hit) dangling.push(`${src.split('/').slice(-2).join('/')} → ${doc} §"${section}"`);
116
+ }
117
+ }
118
+
119
+ // Non-vacuity: these docs ARE cited. A zero here means the matcher stopped
120
+ // matching, not that the corpus got clean — the failure mode this file exists
121
+ // to prevent, one level up.
122
+ expect(
123
+ checked,
124
+ 'spec-section-citations: no citations of the checked docs were found — the matcher is broken, not the corpus clean',
125
+ ).toBeGreaterThan(0);
126
+
127
+ expect(
128
+ dangling,
129
+ `dangling section citations (the cited heading does not exist):\n ${dangling.join('\n ')}`,
130
+ ).toEqual([]);
131
+ });
132
+ });