@openwop/openwop-conformance 1.153.0 → 1.154.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/README.md +9 -0
  3. package/dist/cli.js +7 -2
  4. package/package.json +31 -2
  5. package/schemas/CORPUS-STAMP.json +99 -3
  6. package/src/cli.ts +7 -5
  7. package/src/global-setup.ts +13 -0
  8. package/src/lib/corpus-stamp.ts +125 -0
  9. package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
  10. package/src/lib/fork-availability.test.ts +0 -69
  11. package/src/lib/global-setup.test.ts +0 -76
  12. package/src/lib/grpc-framing.test.ts +0 -96
  13. package/src/lib/oidc-issuer.test.ts +0 -328
  14. package/src/lib/otel-collector-grpc.test.ts +0 -191
  15. package/src/lib/otel-collector.test.ts +0 -303
  16. package/src/lib/otlp-protobuf.test.ts +0 -461
  17. package/src/lib/polling.test.ts +0 -80
  18. package/src/lib/requirement-ids.test.ts +0 -83
  19. package/src/lib/requirement-ledger.test.ts +0 -75
  20. package/src/lib/risk-disposition.test.ts +0 -91
  21. package/src/lib/saml-idp.test.ts +0 -127
  22. package/src/lib/spec-coherence-registry.test.ts +0 -155
  23. package/src/lib/webhook-receiver.test.ts +0 -144
  24. package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
  25. package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
  26. package/src/scenarios/capability-example-root-layout.test.ts +0 -272
  27. package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
  28. package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
  29. package/src/scenarios/compensation-profile.test.ts +0 -340
  30. package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
  31. package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
  32. package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
  33. package/src/scenarios/effect-identity-composition.test.ts +0 -129
  34. package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
  35. package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
  36. package/src/scenarios/form-content-packs.test.ts +0 -415
  37. package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
  38. package/src/scenarios/normative-example-extraction.test.ts +0 -242
  39. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
  40. package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
  41. package/src/scenarios/protocol-version-grammar.test.ts +0 -119
  42. package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
  43. package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
  44. package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
  45. package/src/scenarios/semantic-digest-v2.test.ts +0 -128
  46. package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
  47. package/src/scenarios/spec-section-citations.test.ts +0 -132
  48. package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
  49. package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
  50. package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
  51. package/src/scenarios/workload-identity-profile.test.ts +0 -184
@@ -1,124 +0,0 @@
1
- /**
2
- * artifact-type-legacy-ids — RFC 0141, legacy identifiers and the replay
3
- * migration constraint.
4
- *
5
- * Hosts predating RFC 0071 carry artifact-type ids outside the canonical
6
- * reverse-DNS pattern (`doc.one-pager`, `canvas.checklist`). The corpus never
7
- * said what those values are or how a host may migrate — and the constraint
8
- * that matters (an identifier migration MUST NOT rewrite the run-event log)
9
- * was only derivable by composing THREE documents. It got mis-derived once,
10
- * in a merged RFC, before being corrected (RFC 0138, 2026-08-07). RFC 0141
11
- * states the composed conclusion in one normative place; these legs pin it.
12
- *
13
- * PROSE-PINNING, always-on, server-free (the RFC 0138 part-3 pattern): the
14
- * schema cannot express "MUST NOT rewrite history", so the corpus stating the
15
- * rule IS the testable surface. Leg 1 anchors the prose to the schema pattern
16
- * it interprets, so the two cannot drift apart silently.
17
- *
18
- * WHAT THIS DOES NOT COVER — stated, not discovered later:
19
- * - No leg exercises a HOST's alias behavior. That a host resolves aliases in
20
- * validation is witnessed at openwop-app by its own sabotage-verified tests
21
- * (openwop-app#3030), not by this suite.
22
- * - No leg can detect a host that rewrote its log — a rewritten log is
23
- * indistinguishable from an honest one after the fact. The prohibition is
24
- * enforceable only by the host's own replay integrity.
25
- *
26
- * @see spec/v1/artifact-type-packs.md §"Legacy identifiers and migration"
27
- * @see RFCS/0141-legacy-artifact-type-identifiers.md
28
- * @see spec/v1/replay.md §"Determinism guarantees"
29
- */
30
-
31
- import { describe, it, expect } from 'vitest';
32
- import { readFileSync } from 'node:fs';
33
- import { join } from 'node:path';
34
- import Ajv2020 from 'ajv/dist/2020.js';
35
- import addFormats from 'ajv-formats';
36
- import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
37
-
38
- const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
39
-
40
- describe('artifact-type-legacy-ids: the canonical pattern is the anchor (RFC 0141, server-free)', () => {
41
- const ajv = new Ajv2020({ allErrors: true, strict: false });
42
- addFormats(ajv);
43
- const schema = JSON.parse(
44
- readFileSync(join(SCHEMAS_DIR, 'artifact-type-pack-manifest.schema.json'), 'utf8'),
45
- ) as Record<string, unknown>;
46
- const validate = ajv.compile(schema);
47
- const pack = (artifactTypeId: string): Record<string, unknown> => ({
48
- name: 'community.openwop.legacy-probe',
49
- version: '1.0.0',
50
- kind: 'artifact-type',
51
- engines: { openwop: '>=1.1.0 <2.0.0' },
52
- artifactTypes: [{ artifactTypeId, schemaRef: 'schemas/x.json' }],
53
- });
54
-
55
- it('leg 1a — a bare legacy id (`doc.one-pager`) is REJECTED by the manifest schema', () => {
56
- expect(
57
- validate(pack('doc.one-pager')),
58
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'the canonical pattern is definitional, not aspirational — a legacy spelling has never been a conformant published identifier'),
59
- ).toBe(false);
60
- });
61
-
62
- it('leg 1b — a canonical id still validates (the anchor is not over-tight)', () => {
63
- expect(
64
- validate(pack('community.openwop.doc.one-pager')),
65
- why('artifact-type-packs.md §"The `ArtifactType` declaration"', 'the reverse-DNS form the migration targets is accepted'),
66
- ).toBe(true);
67
- });
68
- });
69
-
70
- describe('artifact-type-legacy-ids: the corpus states the migration rules normatively (RFC 0141)', () => {
71
- const doc = V1_DIR ? readFileSync(join(V1_DIR, 'artifact-type-packs.md'), 'utf8') : '';
72
-
73
- it.skipIf(V1_DIR === null)('leg 2 — never wire-conformant, no grandfather clause, no obligation to migrate', () => {
74
- expect(
75
- /never\W{0,4}\s*wire-conformant/i.test(doc),
76
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'legacy identifiers were NEVER wire-conformant — status is stated, not implied'),
77
- ).toBe(true);
78
- expect(
79
- /no grandfather clause/i.test(doc),
80
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'no grandfather clause exists or is implied'),
81
- ).toBe(true);
82
- expect(
83
- /no obligation to migrate/i.test(doc),
84
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'the unregistered tier is a legitimate permanent home — migration is optional'),
85
- ).toBe(true);
86
- });
87
-
88
- it.skipIf(V1_DIR === null)('leg 3 — a migrating host MUST NOT rewrite historical event-carried identifiers, tied to replay', () => {
89
- expect(
90
- /MUST NOT\W{0,4}\s*rewrite historical\W{0,4}\s*`artifact\.created\.artifactType`/i.test(doc),
91
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'the rewrite prohibition is normative, naming the event field — not derivable-only across three documents'),
92
- ).toBe(true);
93
- expect(
94
- /replay\.md.{0,80}fixed history|fixed history.{0,120}replay/is.test(doc),
95
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'the prohibition is tied to replay.md fixed-history semantics, so the derivation travels with the rule'),
96
- ).toBe(true);
97
- expect(
98
- /silently demoting a typed artifact|silently demot/i.test(doc),
99
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'the silent-demotion failure mode is named — the reason a rewrite is worse than the non-conformant name'),
100
- ).toBe(true);
101
- });
102
-
103
- it.skipIf(V1_DIR === null)('leg 4 — the alias is permanent and MUST resolve where validation is decided', () => {
104
- expect(
105
- /permanent, not transitional/i.test(doc),
106
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'immutable history makes the alias permanent — a host planning to delete it later has misunderstood the constraint'),
107
- ).toBe(true);
108
- expect(
109
- /everywhere registration is decided\W{0,4}\s*schema validation included/i.test(doc),
110
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'aliasing lookup but not validation silently demotes typed artifacts behind a green result — the MUST covers validation by name'),
111
- ).toBe(true);
112
- });
113
-
114
- it.skipIf(V1_DIR === null)('leg 5 — an alias map is a host shim, NOT a conformance claim', () => {
115
- expect(
116
- /compatibility shim\W{0,4}\s*not a conformance claim/i.test(doc),
117
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'a host serving legacy ids through an alias is not "conformant under its old names"'),
118
- ).toBe(true);
119
- expect(
120
- /MUST NOT\W{0,4}\s*advertise or imply otherwise/i.test(doc),
121
- why('artifact-type-packs.md §"Legacy identifiers and migration"', 'the shim MUST NOT be advertised as conformance'),
122
- ).toBe(true);
123
- });
124
- });
@@ -1,272 +0,0 @@
1
- /**
2
- * RFC 0149 §B — normative discovery examples place capability families at the
3
- * document root.
4
- *
5
- * RFC 0073 (`Accepted`) made the root layout "the normative MUST since Phase 1"
6
- * and, at Phase 4, made the suite enforce it: a wrapper-only host "grades as
7
- * non-conformant". The `capabilities` wrapper survives only as a deprecated
8
- * shape that runtime discovery tolerates through the v1.x window, retiring at
9
- * v2.0 when `capabilities.schema.json` tightens.
10
- *
11
- * Tolerating a shape at runtime and teaching it in a normative example are
12
- * different things. Eight `spec/v1` examples — several under headings like
13
- * "Capability advertisement (normative)", introduced by prose saying hosts
14
- * "advertise it under `/.well-known/openwop`" — showed the deprecated wrapper.
15
- * An implementer copying one produced a document RFC 0073 grades as
16
- * non-conformant, and no gate noticed, because a fenced example is prose to
17
- * every validator in the corpus.
18
- *
19
- * This gate is authoring-time only. It says nothing about what a server may
20
- * emit: RFC 0149 §B is explicit that the runtime schema MUST NOT reject an
21
- * otherwise legal unknown server-emitted property, and nothing here reads a
22
- * host.
23
- *
24
- * **Canonical-typo leg (RFC 0149 §B, second bullet; UQ2 decided 2026-08-16).** A
25
- * root key within edit distance ONE of a canonical family, in a discovery-shaped
26
- * example, that is not itself canonical and not vendor-namespaced, is a typo
27
- * (`compensaton`, `interupts`) — an implementer copying it advertises nothing.
28
- * UQ2 asked what rule avoids false positives on legitimate extension names; the
29
- * answer was measured, not guessed. Over every fenced root object in `spec/v1` +
30
- * `RFCS/` (218 on 2026-08-16), plain distance-one produced six near-misses —
31
- * `ts`/`fs`, `agent`/`agents`, `secret`/`secrets`, `context`/`content`,
32
- * `prompt`/`prompts`, `schemaVersion`/`schemaVersions` — every one of them a key
33
- * of an EVENT or RUN object, not a discovery document. So the predicate is
34
- * *discovery-shaped*: every root key is canonical, vendor-namespaced
35
- * (`host-extensions.md` §"Canonical prefixes"), the legacy `capabilities`
36
- * wrapper, or within distance one of a canonical family — and at least one key
37
- * is canonical-or-near. That excludes events (`ts`/`type`/`payload` are none of
38
- * those) while still catching a typo-only snippet whose single key is misspelt.
39
- * Scope: `spec/v1` and RFCs numbered >= 0149 (the rule's own RFC); older RFCs
40
- * are the dated record, on the same boundary logic as the wrapper leg's 0073.
41
- * Under that predicate and scope the corpus measured 53 discovery-shaped
42
- * objects and 0 findings; the one out-of-scope near-miss is RFC 0109's
43
- * `{ "agent": … }` payload fragment.
44
- *
45
- * `spec/v1/` and `RFCS/` ship in the repository, NOT in the published tarball,
46
- * so this self-skips under the published layout. That asymmetry has produced
47
- * three defects in this corpus — the `CORPUS-STAMP` gate, the link-checker's
48
- * filesystem walk, and RFC 0146 leg A4, which ENOENT'd for every adopter
49
- * running from the package.
50
- */
51
-
52
- import { describe, it, expect } from 'vitest';
53
- import { readFileSync, readdirSync, existsSync } from 'node:fs';
54
- import { join, resolve as pathResolve } from 'node:path';
55
- import { V1_DIR } from '../lib/paths.js';
56
-
57
- /** RFC 0073 established the root layout; examples in earlier RFCs are historical record. */
58
- const ROOT_LAYOUT_RFC = 73;
59
-
60
- const RFCS_DIR = V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'RFCS');
61
-
62
- interface FencedExample {
63
- readonly file: string;
64
- readonly line: number;
65
- readonly rootWrapper: boolean;
66
- }
67
-
68
- /** Fenced ```json / ```jsonc blocks, flagged when the root object's first key is `capabilities`. */
69
- function fencedExamples(dir: string): FencedExample[] {
70
- const found: FencedExample[] = [];
71
- for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
72
- const lines = readFileSync(join(dir, name), 'utf8').split('\n');
73
- for (let i = 0; i < lines.length; i++) {
74
- if (!/^```(json|jsonc)\s*$/.test(lines[i]!.trim())) continue;
75
- const body: string[] = [];
76
- let j = i + 1;
77
- while (j < lines.length && lines[j]!.trim() !== '```') {
78
- body.push(lines[j]!);
79
- j++;
80
- }
81
- const rootWrapper =
82
- body.length > 1 && /^\s*\{\s*$/.test(body[0]!) && /^\s*"capabilities"\s*:\s*\{\s*$/.test(body[1]!);
83
- found.push({ file: name, line: i + 2, rootWrapper });
84
- i = j;
85
- }
86
- }
87
- return found;
88
- }
89
-
90
- /** RFC 0149 introduced the typo lint; examples in earlier RFCs are historical record. */
91
- const TYPO_LINT_RFC = 149;
92
-
93
- const SCHEMA_PATH =
94
- V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'schemas', 'capabilities.schema.json');
95
-
96
- /** The canonical families, read from the schema rather than hand-listed. */
97
- function canonicalFamilies(): Set<string> {
98
- if (SCHEMA_PATH === null) return new Set();
99
- const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')) as { properties?: Record<string, unknown> };
100
- return new Set(Object.keys(schema.properties ?? {}));
101
- }
102
-
103
- /** `host-extensions.md` §"Canonical prefixes". */
104
- function isVendorKey(key: string): boolean {
105
- return /^x-host-/.test(key) || /^(vendor|private)\./.test(key);
106
- }
107
-
108
- /** Levenshtein distance exactly one (one substitution, insertion, or deletion). */
109
- export function withinOne(a: string, b: string): boolean {
110
- if (a === b) return false;
111
- if (Math.abs(a.length - b.length) > 1) return false;
112
- if (a.length === b.length) {
113
- let diff = 0;
114
- for (let i = 0; i < a.length; i++) if (a[i] !== b[i] && ++diff > 1) return false;
115
- return diff === 1;
116
- }
117
- const [short, long] = a.length < b.length ? [a, b] : [b, a];
118
- let i = 0;
119
- let j = 0;
120
- let skipped = false;
121
- while (i < short.length && j < long.length) {
122
- if (short[i] === long[j]) {
123
- i++;
124
- j++;
125
- } else if (skipped) {
126
- return false;
127
- } else {
128
- skipped = true;
129
- j++;
130
- }
131
- }
132
- return true;
133
- }
134
-
135
- export interface TypoFinding {
136
- readonly key: string;
137
- readonly near: readonly string[];
138
- }
139
-
140
- /**
141
- * The UQ2 rule. Returns the near-miss root keys of a DISCOVERY-SHAPED object, or
142
- * `null` when the object is not discovery-shaped (and so is out of scope: an
143
- * event, a run body, a manifest). Exported so the predicate is pinned below.
144
- */
145
- export function canonicalTypos(value: unknown, families: Set<string>): TypoFinding[] | null {
146
- if (value === null || typeof value !== 'object' || Array.isArray(value)) return null;
147
- const keys = Object.keys(value as Record<string, unknown>);
148
- if (keys.length === 0) return null;
149
- const near = new Map<string, string[]>();
150
- for (const k of keys) {
151
- if (families.has(k) || isVendorKey(k) || k === 'capabilities') continue;
152
- const n = [...families].filter((f) => withinOne(k, f));
153
- if (n.length === 0) return null; // a key that is none of the four kinds ⇒ not discovery-shaped
154
- near.set(k, n);
155
- }
156
- if (!keys.some((k) => families.has(k) || near.has(k))) return null;
157
- return [...near.entries()].map(([key, n]) => ({ key, near: n }));
158
- }
159
-
160
- /** Every parseable fenced json/jsonc root object under `dir`, with its source line. */
161
- function fencedObjects(dir: string): { file: string; line: number; value: unknown }[] {
162
- const out: { file: string; line: number; value: unknown }[] = [];
163
- for (const name of readdirSync(dir).filter((f) => f.endsWith('.md')).sort()) {
164
- const lines = readFileSync(join(dir, name), 'utf8').split('\n');
165
- for (let i = 0; i < lines.length; i++) {
166
- if (!/^```(json|jsonc)\s*$/.test(lines[i]!.trim())) continue;
167
- const body: string[] = [];
168
- let j = i + 1;
169
- while (j < lines.length && lines[j]!.trim() !== '```') body.push(lines[j++]!);
170
- try {
171
- out.push({ file: name, line: i + 2, value: JSON.parse(body.join('\n')) });
172
- } catch {
173
- // Unparseable blocks are RFC 0150 §D's problem, not this gate's.
174
- }
175
- i = j;
176
- }
177
- }
178
- return out;
179
- }
180
-
181
- describe.skipIf(V1_DIR === null)('RFC 0149 §B — discovery examples use the document-root layout', () => {
182
- const v1Dir = V1_DIR as string;
183
-
184
- it('the scan finds fenced examples at all', () => {
185
- // Guard: an extractor that silently matched nothing would make the
186
- // assertions below vacuously true, which is the failure mode RFC 0148
187
- // exists to close. This gate must not become an instance of it.
188
- const examples = fencedExamples(v1Dir);
189
- expect(examples.length, 'spec/v1 MUST contain fenced json/jsonc examples to check').toBeGreaterThan(20);
190
- });
191
-
192
- it('no spec/v1 example wraps capability families in a top-level `capabilities` object', () => {
193
- const offenders = fencedExamples(v1Dir)
194
- .filter((e) => e.rootWrapper)
195
- .map((e) => `spec/v1/${e.file}:${e.line}`);
196
- expect(
197
- offenders,
198
- 'RFC 0073: capability families are a property of the DOCUMENT ROOT; there is no `capabilities` wrapper. ' +
199
- 'A normative example showing the deprecated shape teaches a document RFC 0073 grades as non-conformant.\n ' +
200
- offenders.join('\n '),
201
- ).toEqual([]);
202
- });
203
-
204
- it('every RFC still showing the wrapper predates RFC 0073, which established root layout', () => {
205
- // RFCs are a dated record of what was proposed, so their examples are NOT
206
- // rewritten to match a later layout — that would make the record lie. The
207
- // exemption is asserted rather than assumed: a NEW post-0073 RFC that
208
- // introduces a wrapper fails here, so the historical carve-out cannot widen
209
- // into a licence.
210
- if (RFCS_DIR === null || !existsSync(RFCS_DIR)) return;
211
- const late = fencedExamples(RFCS_DIR)
212
- .filter((e) => e.rootWrapper)
213
- .filter((e) => {
214
- const n = Number.parseInt(e.file.slice(0, 4), 10);
215
- return Number.isFinite(n) && n >= ROOT_LAYOUT_RFC;
216
- })
217
- .map((e) => `RFCS/${e.file}:${e.line}`);
218
- expect(
219
- late,
220
- `an RFC numbered >= ${ROOT_LAYOUT_RFC} uses the deprecated \`capabilities\` wrapper. ` +
221
- 'Pre-0073 RFCs keep theirs as historical record; a later one has no such excuse.\n ' +
222
- late.join('\n '),
223
- ).toEqual([]);
224
- });
225
- it('the UQ2 predicate is pinned: a typo-only snippet is flagged, an event object is out of scope, a vendor key is exempt', () => {
226
- const fams = new Set(['compensation', 'interrupts', 'fs', 'agents', 'content']);
227
- // A misspelt single-key discovery snippet: all keys near-canonical ⇒ in scope, flagged.
228
- expect(canonicalTypos({ compensaton: { supported: true } }, fams)).toEqual([{ key: 'compensaton', near: ['compensation'] }]);
229
- // Canonical + a typo ⇒ flagged.
230
- expect(canonicalTypos({ compensation: {}, interupts: {} }, fams)).toEqual([{ key: 'interupts', near: ['interrupts'] }]);
231
- // An event: `ts` is one from `fs` but `type`/`payload` are none of the four kinds ⇒ not discovery-shaped.
232
- expect(canonicalTypos({ ts: 1, type: 'x', payload: {} }, fams)).toBeNull();
233
- // Vendor-namespaced keys are exempt (RFC 0149 §B) and the legacy wrapper is the wrapper leg's business.
234
- expect(canonicalTypos({ compensation: {}, 'x-host-acme-agent': {} }, fams)).toEqual([]);
235
- expect(canonicalTypos({ capabilities: {} }, fams)).toBeNull();
236
- // Distance exactly one, both directions.
237
- expect(withinOne('agent', 'agents')).toBe(true);
238
- expect(withinOne('agents', 'agent')).toBe(true);
239
- expect(withinOne('agent', 'agentz')).toBe(true);
240
- expect(withinOne('agent', 'agenzs')).toBe(false);
241
- expect(withinOne('agents', 'agents')).toBe(false);
242
- });
243
-
244
- it('no discovery-shaped example in spec/v1 or a post-0149 RFC has a root key within one edit of a canonical family', () => {
245
- const families = canonicalFamilies();
246
- expect(families.size, 'capabilities.schema.json MUST declare families').toBeGreaterThan(50);
247
- const findings: string[] = [];
248
- let shaped = 0;
249
- const scan = (dir: string, rel: string, minRfc: number | null): void => {
250
- for (const { file, line, value } of fencedObjects(dir)) {
251
- if (minRfc !== null) {
252
- const n = Number.parseInt(file.slice(0, 4), 10);
253
- if (!Number.isFinite(n) || n < minRfc) continue;
254
- }
255
- const typos = canonicalTypos(value, families);
256
- if (typos === null) continue;
257
- shaped++;
258
- for (const t of typos) findings.push(`${rel}/${file}:${line} → \`${t.key}\` (did you mean ${t.near.map((x) => '`' + x + '`').join(' / ')}?)`);
259
- }
260
- };
261
- scan(v1Dir, 'spec/v1', null);
262
- if (RFCS_DIR !== null && existsSync(RFCS_DIR)) scan(RFCS_DIR, 'RFCS', TYPO_LINT_RFC);
263
- expect(shaped, 'the scan MUST find discovery-shaped examples, or the leg is vacuous').toBeGreaterThan(20);
264
- expect(
265
- findings,
266
- 'RFC 0149 §B: a root key within one edit of a canonical family, in a discovery-shaped example, is a ' +
267
- 'typo — an implementer copying it advertises nothing. Vendor surface goes under `x-host-*` / ' +
268
- '`vendor.*` / `private.*` (host-extensions.md).\n ' +
269
- findings.join('\n '),
270
- ).toEqual([]);
271
- });
272
- });
@@ -1,204 +0,0 @@
1
- /**
2
- * RFC 0148 §C / gap G6 — an undefined floor set is UNPROVABLE, never proven.
3
- *
4
- * `conformance-certification.md` §B(2) requires every floor scenario of a
5
- * claimed profile to appear in `results.passed`, and `profiles.md` §"Claiming
6
- * vs passing" says a host claims a profile "by satisfying its predicate AND
7
- * passing the conformance scenarios labelled with the profile tag."
8
- *
9
- * `PROFILE_FLOOR_SCENARIOS` transcribed that prose for `openwop-core-standard`
10
- * alone. For every other profile the floor was `undefined`, and the verifier
11
- * computed `floorProven` from `missingFloor.length === 0 && prefixOk` — both
12
- * vacuously true over an absent floor. So a claim with nothing behind it
13
- * verified as PROVEN. That is a third vacuity mode alongside the two RFC 0148
14
- * §"Motivation" names: those let an unexecuted assertion count as a pass, this
15
- * lets an entire profile claim verify against nothing.
16
- *
17
- * These legs are server-free and always-on. A floor rule that only ran when a
18
- * host advertised something would be gated on the very claim it exists to
19
- * check.
20
- */
21
-
22
- import { describe, it, expect } from 'vitest';
23
- import { existsSync, readFileSync } from 'node:fs';
24
- import { join } from 'node:path';
25
- import { verifyBundle, verifyBundleProfile, PROFILE_FLOOR_SCENARIOS } from '../lib/profiles.js';
26
- import { SCENARIOS_DIR } from '../lib/paths.js';
27
-
28
- /** Derives `openwop-core` and `openwop-stream-sse` (transports omitted ⇒ all). */
29
- const streamingDiscovery = {
30
- protocolVersion: '1.0',
31
- supportedEnvelopes: ['final', 'clarification.request'],
32
- schemaVersions: { 'workflow-definition': '1.0' },
33
- limits: { clarificationRounds: 3, schemaRounds: 2, envelopesPerTurn: 8 },
34
- };
35
-
36
- const bundleClaiming = (profiles: readonly string[], passed: readonly string[], failed: readonly string[] = []) => ({
37
- discovery: { document: streamingDiscovery },
38
- claimedProfiles: profiles,
39
- results: { passed, failed },
40
- });
41
-
42
- describe('RFC 0148 §C — floor enforcement is not vacuous', () => {
43
- it('a profile with NO floor definition is unprovable, not proven', () => {
44
- // A profile the floor map does not know (here a name outside the catalog;
45
- // until 2026-08-16 `openwop-interrupts` played this role — its floor is now
46
- // transcribed, RFC 0148 §C G7). Claiming it must fail — the corpus cannot
47
- // substantiate it, which is a different thing from the host having failed
48
- // something.
49
- const v = verifyBundleProfile(bundleClaiming(['openwop-not-in-catalog'], []), 'openwop-not-in-catalog');
50
- expect(v.floorUnspecified, 'no floor set is defined for a profile outside the floor map').toBe(true);
51
- expect(v.floorProven, 'RFC 0148 §C: an undefined floor MUST NOT satisfy the floor condition').toBe(false);
52
- expect(v.valid).toBe(false);
53
- expect(v.missingFloor, 'nothing is "missing" — the floor was never evaluable').toEqual([]);
54
- });
55
-
56
- it('G7: a discovery-conditional floor (openwop-replay-fork) requires the branch the document advertises — and nothing when it advertises none', () => {
57
- const doc = (modes: string[]) => ({ protocolVersion: '1.0', supportedEnvelopes: [], schemaVersions: {}, limits: { clarificationRounds: 1, schemaRounds: 1, envelopesPerTurn: 1 }, replay: { supported: true, modes } });
58
- const claim = (modes: string[], passed: string[]) => ({ discovery: { document: doc(modes) }, claimedProfiles: ['openwop-replay-fork'], results: { passed, failed: [] } });
59
- // replay-only host: replayDeterminism suffices; replay-fork is NOT required
60
- let v = verifyBundleProfile(claim(['replay'], ['replayDeterminism.test.ts', 'replay-side-effect-suppression.test.ts']), 'openwop-replay-fork');
61
- expect(v.floorUnspecified).toBe(false);
62
- expect(v.floorProven).toBe(true);
63
- // determinism alone does NOT prove replay mode: caveat 1 (no re-fire) is part of the branch
64
- v = verifyBundleProfile(claim(['replay'], ['replayDeterminism.test.ts']), 'openwop-replay-fork');
65
- expect(v.floorProven).toBe(false);
66
- expect(v.missingFloor).toEqual(['replay-side-effect-suppression.test.ts']);
67
- // branch-only host: replay-fork suffices
68
- v = verifyBundleProfile(claim(['branch'], ['replay-fork.test.ts']), 'openwop-replay-fork');
69
- expect(v.floorProven).toBe(true);
70
- // both advertised: both required
71
- v = verifyBundleProfile(claim(['replay', 'branch'], ['replay-fork.test.ts', 'replay-side-effect-suppression.test.ts']), 'openwop-replay-fork');
72
- expect(v.floorProven).toBe(false);
73
- expect(v.missingFloor).toEqual(['replayDeterminism.test.ts']);
74
- // a mode the floor does not know: nothing required → unprovable, not proven
75
- v = verifyBundleProfile(claim(['exotic'], []), 'openwop-replay-fork');
76
- expect(v.floorProven).toBe(false);
77
- });
78
-
79
- it('a discovery-only profile with an EXPLICIT empty floor is proven by its predicate alone', () => {
80
- // The distinction the fix turns on: `openwop-fixtures` is discovery-payload
81
- // -only by `profiles.md`, so an empty floor is a decision on record. Without
82
- // `discoveryOnly`, "legitimately empty" and "not yet written" are the same
83
- // value, which is what produced the defect.
84
- const v = verifyBundleProfile(bundleClaiming(['openwop-fixtures'], []), 'openwop-fixtures');
85
- expect(PROFILE_FLOOR_SCENARIOS['openwop-fixtures']?.discoveryOnly).toBe(true);
86
- expect(v.floorUnspecified).toBe(false);
87
- expect(v.floorProven).toBe(true);
88
- });
89
-
90
- it('a claimed streaming profile whose floor scenarios FAILED is rejected', () => {
91
- // The shape of the real defect: the one published v1 bundle claimed
92
- // `openwop-stream-sse` while all three `stream-modes*` scenarios sat in its
93
- // own `results.failed`, and the verifier accepted it.
94
- const v = verifyBundleProfile(
95
- bundleClaiming(
96
- ['openwop-stream-sse'],
97
- [],
98
- ['stream-modes.test.ts', 'stream-modes-buffer.test.ts', 'stream-modes-mixed.test.ts'],
99
- ),
100
- 'openwop-stream-sse',
101
- );
102
- expect(v.derivable, 'the discovery document does derive the profile').toBe(true);
103
- expect(v.floorProven, 'profiles.md §openwop-stream-sse: predicate AND those scenarios pass').toBe(false);
104
- expect(v.missingFloor).toContain('stream-modes.test.ts');
105
- expect(v.valid).toBe(false);
106
- });
107
-
108
- it('a claimed streaming profile whose floor scenarios all PASSED is accepted', () => {
109
- const v = verifyBundleProfile(
110
- bundleClaiming(['openwop-stream-sse'], [
111
- 'stream-modes.test.ts',
112
- 'stream-modes-buffer.test.ts',
113
- 'stream-modes-mixed.test.ts',
114
- ]),
115
- 'openwop-stream-sse',
116
- );
117
- expect(v.floorProven).toBe(true);
118
- expect(v.valid).toBe(true);
119
- });
120
-
121
- it('one unprovable claim invalidates the whole bundle', () => {
122
- // A bundle is valid iff EVERY claim is. Mixing a provable claim with an
123
- // unprovable one must not average out to valid.
124
- const r = verifyBundle(bundleClaiming(['openwop-core', 'openwop-interrupts'], []));
125
- expect(r.verdicts.find((v) => v.profile === 'openwop-core')?.valid).toBe(true);
126
- expect(r.verdicts.find((v) => v.profile === 'openwop-interrupts')?.valid).toBe(false);
127
- expect(r.valid).toBe(false);
128
- });
129
-
130
- it('every transcribed floor names scenario files that exist in this suite', () => {
131
- // Guards the transcription itself: a floor citing a renamed or deleted
132
- // scenario can never be satisfied, which would fail honest hosts for a
133
- // reason unrelated to their behavior.
134
- const all = Object.entries(PROFILE_FLOOR_SCENARIOS).flatMap(([profile, floor]) =>
135
- floor.required.map((scenario) => ({ profile, scenario })),
136
- );
137
- expect(all.length, 'the floor map MUST NOT be empty — an empty map re-opens the vacuity').toBeGreaterThan(0);
138
- for (const { profile, scenario } of all) {
139
- expect(scenario, `${profile} floor cites a non-scenario filename`).toMatch(/\.test\.ts$/);
140
- }
141
-
142
- // 2026-08-18: this leg's NAME promised "files that exist" and it only
143
- // matched the `.test.ts` suffix — a string check wearing an existence
144
- // check's name. The phantom `audit-log-verification.test.ts` floor row sat
145
- // in `openwop-core-standard` until `--certify` hit it against a live host,
146
- // because nothing here opened the directory.
147
- if (SCENARIOS_DIR === null) return; // published layout ships src/, but stay honest if it ever does not
148
- const dir = SCENARIOS_DIR as string;
149
- const missing = all
150
- .filter(({ scenario }) => !existsSync(join(dir, scenario)))
151
- .map(({ profile, scenario }) => `${profile} → ${scenario}`);
152
- expect(
153
- missing,
154
- 'a floor cites a scenario file that does not exist — that requirement can never be satisfied, ' +
155
- 'so the profile can never certify, for a reason unrelated to any host',
156
- ).toEqual([]);
157
- });
158
-
159
- it('no floor scenario is corpus-only — a floor must be provable from the published package', () => {
160
- // openwop-app's suggestion, and it is right that this be a red test rather
161
- // than a paragraph: the failure mode is SOMEONE LATER adding a corpus
162
- // self-check to a floor, and prose in a PR body will not be in front of
163
- // them.
164
- //
165
- // A scenario that can only run in a repo checkout (it reads `spec/` or
166
- // `RFCS/` through `V1_DIR`) records `blocked` in the published layout,
167
- // where the package ships no corpus by design. `blocked` is correct there
168
- // — RFC 0148 §A defines it as a missing dependency — but a `blocked`
169
- // requirement in a claimed profile invalidates that profile. So a
170
- // corpus-only scenario in a floor makes the profile UNCERTIFIABLE from the
171
- // npm tarball, permanently, for a reason no host can fix. It is the mirror
172
- // of the phantom-row trap above: that one named a file that does not
173
- // exist, this one names a file that cannot execute where certification is
174
- // measured.
175
- if (SCENARIOS_DIR === null) return;
176
- const dir = SCENARIOS_DIR as string;
177
-
178
- /** Every `it`/`test` in the file is V1_DIR-guarded, or every `describe` is. */
179
- const isCorpusOnly = (source: string): boolean => {
180
- if (!source.includes('V1_DIR')) return false;
181
- const guard = /\(V1_DIR === null\)/;
182
- const its = [...source.matchAll(/\b(?:it|test)(\.skipIf\([^)]*\))?\s*\(/g)];
183
- const describes = [...source.matchAll(/\bdescribe(\.skipIf\([^)]*\))?\s*\(/g)];
184
- const allItsGuarded = its.length > 0 && its.every((m) => guard.test(m[1] ?? ''));
185
- const allDescribesGuarded = describes.length > 0 && describes.every((m) => guard.test(m[1] ?? ''));
186
- return allItsGuarded || allDescribesGuarded;
187
- };
188
-
189
- const offenders = Object.entries(PROFILE_FLOOR_SCENARIOS)
190
- .flatMap(([profile, floor]) => floor.required.map((scenario) => ({ profile, scenario })))
191
- .filter(({ scenario }) => {
192
- const path = join(dir, scenario);
193
- return existsSync(path) && isCorpusOnly(readFileSync(path, 'utf8'));
194
- })
195
- .map(({ profile, scenario }) => `${profile} → ${scenario}`);
196
-
197
- expect(
198
- offenders,
199
- 'a profile floor cites a CORPUS-ONLY scenario (all of its tests are guarded on `V1_DIR === null`). ' +
200
- 'It records `blocked` in the published layout, so that profile can never certify from the npm ' +
201
- 'tarball — no host can fix it. Keep corpus self-checks out of floors.',
202
- ).toEqual([]);
203
- });
204
- });