@openwop/openwop-conformance 1.136.7 → 1.136.9
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,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.
|
|
4
|
-
"corpusCommit": "
|
|
3
|
+
"suiteVersion": "1.136.9",
|
|
4
|
+
"corpusCommit": "6d2c33ba0be0e4d6934f5f9471600c66e8c5c183"
|
|
5
5
|
}
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
*/
|
|
41
41
|
|
|
42
42
|
import { describe, it, expect } from 'vitest';
|
|
43
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
43
44
|
import { driver } from '../lib/driver.js';
|
|
44
45
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
45
46
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -224,4 +225,74 @@ describe('RFC 0151 §C — compensation lifecycle (capability-gated behavior)',
|
|
|
224
225
|
}
|
|
225
226
|
}
|
|
226
227
|
});
|
|
228
|
+
|
|
229
|
+
it('a healthy run that DECLARES a compensator reports compensationStatus `none` (SP-11a)', async () => {
|
|
230
|
+
if (!behaviorGate(PROFILE, await advertised())) return;
|
|
231
|
+
|
|
232
|
+
// The rollup folds over the PLAN, and no plan exists until a trigger fires
|
|
233
|
+
// (`compensation.md` §"Run rollup"). So a run that completes successfully while
|
|
234
|
+
// declaring compensable nodes reads `none` — it has nothing to unwind.
|
|
235
|
+
//
|
|
236
|
+
// Nothing observed this. Every other leg of the §21 seam drives a FAILURE, so a
|
|
237
|
+
// host that derived the rollup from the existence of obligation rows — minted per
|
|
238
|
+
// compensable node, healthy or not — reported `pending` on every successful run
|
|
239
|
+
// that declared a compensator and passed all of them. A tier-1 host shipped exactly
|
|
240
|
+
// that (SP-11a / their AP-03). This leg is the healthy case, and it needs no failure:
|
|
241
|
+
// `fail: false` on the unwind seam runs the same compensator-declaring workflow to
|
|
242
|
+
// completion.
|
|
243
|
+
const seam = await driver.post('/v1/host/sample/test/compensation/unwind', { fail: false });
|
|
244
|
+
if (seam.status === 404) {
|
|
245
|
+
return softSkip('blocked', 'the §21 unwind seam is not wired — the healthy-run rollup has no black-box witness');
|
|
246
|
+
}
|
|
247
|
+
if (seam.status >= 400) {
|
|
248
|
+
// The host mounts the seam but rejects `fail: false`. That is the pre-extension
|
|
249
|
+
// shape, not a pass: without it the healthy-run rollup stays unobservable.
|
|
250
|
+
return softSkip(
|
|
251
|
+
'blocked',
|
|
252
|
+
`the §21 unwind seam does not honour \`fail: false\` (HTTP ${seam.status}) — the healthy-run ` +
|
|
253
|
+
'rollup is unobservable until it does (host-sample-test-seams.md §21)',
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const body = seam.json as { runId?: string; events?: { type: string }[] };
|
|
258
|
+
expect(
|
|
259
|
+
typeof body.runId === 'string' && body.runId.length > 0,
|
|
260
|
+
driver.describe('host-sample-test-seams.md §21', '`fail: false` MUST still return the runId it created'),
|
|
261
|
+
).toBe(true);
|
|
262
|
+
|
|
263
|
+
// Positive control: the seam really did run a healthy compensator-declaring
|
|
264
|
+
// workflow. If it emitted a `compensation.requested`, it failed the node anyway and
|
|
265
|
+
// this is not the healthy case — say so rather than assert `none` against a run
|
|
266
|
+
// that legitimately has a plan.
|
|
267
|
+
const types = (body.events ?? []).map((e) => e.type);
|
|
268
|
+
if (types.includes('compensation.requested')) {
|
|
269
|
+
return softSkip(
|
|
270
|
+
'blocked',
|
|
271
|
+
`the seam emitted ${JSON.stringify(types)} under \`fail: false\` — a trigger fired, so this run ` +
|
|
272
|
+
'is not the healthy case the leg needs',
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const snap = await driver.get(`/v1/runs/${encodeURIComponent(body.runId ?? '')}`);
|
|
277
|
+
expect(snap.status, driver.describe('rest-endpoints.md', 'GET /v1/runs/{runId} for the healthy seam run')).toBe(200);
|
|
278
|
+
const snapshot = snap.json as { status?: unknown; compensationStatus?: unknown };
|
|
279
|
+
|
|
280
|
+
expect(
|
|
281
|
+
snapshot.compensationStatus,
|
|
282
|
+
driver.describe(
|
|
283
|
+
'compensation.md §"Run rollup: compensationStatus"',
|
|
284
|
+
'a host advertising `compensation` MUST carry the field on EVERY snapshot — presence is the wire witness of the advert',
|
|
285
|
+
),
|
|
286
|
+
).toBeDefined();
|
|
287
|
+
|
|
288
|
+
expect(
|
|
289
|
+
snapshot.compensationStatus,
|
|
290
|
+
driver.describe(
|
|
291
|
+
'compensation.md §"Run rollup: compensationStatus"',
|
|
292
|
+
'a run that completed while declaring compensable nodes has nothing to unwind: no `compensation.requested` ' +
|
|
293
|
+
'was recorded, so the fold is `none`. `pending` here means the rollup was derived from obligation ROWS ' +
|
|
294
|
+
`rather than from plan state — got \`${String(snapshot.compensationStatus)}\``,
|
|
295
|
+
),
|
|
296
|
+
).toBe('none');
|
|
297
|
+
});
|
|
227
298
|
});
|
|
@@ -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
|
+
});
|