@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,70 +0,0 @@
1
- /**
2
- * Sub-chain composition — unsupported-host refusal (RFC 0133 §1.3).
3
- *
4
- * A host that does NOT advertise `capabilities.workflowChainPacks.subChains.supported`
5
- * MUST refuse to instantiate a `subChains`-bearing chain with `sub_chain_unsupported`
6
- * (HTTP 422) — it MUST NOT silently flatten the runtime child into the parent
7
- * (flattening erases the child as an editable unit and changes run semantics).
8
- *
9
- * TWO parts:
10
- * A. Always-on — the error code + refusal contract are present in the spec
11
- * corpus (`workflow-chain-packs.md` §"Error codes" + §"Sub-chain composition").
12
- * B. Capability-gated — against a host that expands chains
13
- * (`workflowChainPacks.supported`) but does NOT advertise `subChains`, a
14
- * `from-chain` on a `subChains`-bearing chain returns `sub_chain_unsupported`.
15
- * Soft-skips until a reference host exposes the seam. The refusal is the
16
- * fail-closed complement of `sub-chain-expansion-bounded`: a host that cannot
17
- * run child dispatch never mis-dispatches.
18
- *
19
- * @see spec/v1/workflow-chain-packs.md §"Sub-chain composition (RFC 0133)" + §"Error codes"
20
- * @see RFCS/0133-workflow-chain-composition.md
21
- */
22
-
23
- import { describe, it, expect } from 'vitest';
24
- import { readFileSync } from 'node:fs';
25
- import { join } from 'node:path';
26
- import { V1_DIR } from '../lib/paths.js';
27
- import { behaviorGate } from '../lib/behavior-gate.js';
28
- import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
29
-
30
- const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
31
- // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
32
- // from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
33
- // MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
34
- // repo-layout only: `null` in the published layout and skipped, never thrown.
35
- const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
36
-
37
- describe('chain-subchain-unsupported-refused §A: corpus contract (RFC 0133, always-on)', () => {
38
- it.skipIf(CHAIN_DOC === null)('spec corpus pins sub_chain_unsupported as a 422 refusal (never flatten)', () => {
39
- const doc = readFileSync(CHAIN_DOC as string, 'utf8');
40
- expect(doc.includes('sub_chain_unsupported'), cite('§Error codes', 'the code MUST be registered')).toBe(true);
41
- // The normative refusal-not-flatten rule MUST appear in the composition section.
42
- expect(
43
- /sub_chain_unsupported[\s\S]{0,600}(never|MUST NOT).{0,40}(flatten|silently)/i.test(doc) ||
44
- /(never|MUST NOT).{0,40}(flatten|silently)[\s\S]{0,600}sub_chain_unsupported/i.test(doc),
45
- cite('§Sub-chain composition', 'an unsupported host MUST refuse, MUST NOT silently flatten'),
46
- ).toBe(true);
47
- expect(doc.includes('422'), cite('§Error codes', 'sub_chain_unsupported carries HTTP 422')).toBe(true);
48
- });
49
- });
50
-
51
- describe('chain-subchain-unsupported-refused §B: host refusal (RFC 0133, capability-gated)', () => {
52
- it('a host without subChains support refuses a subChains-bearing chain with sub_chain_unsupported', async () => {
53
- const wcp = await readCapabilityFamily<{ supported?: boolean; subChains?: { supported?: boolean } }>(
54
- 'workflowChainPacks',
55
- );
56
- // This leg targets hosts that DO expand chains but do NOT support runtime
57
- // child dispatch — the exact population that MUST refuse. Gate on the base
58
- // chain-expansion capability; soft-skip hosts that don't expand at all.
59
- if (!behaviorGate('workflowChainPacks.supported', wcp?.supported === true)) return;
60
- if (wcp?.subChains?.supported === true) {
61
- // Host DOES support sub-chains — refusal path is not applicable here; the
62
- // positive path is covered by chain-subchain-fanout §B. Skip cleanly.
63
- return;
64
- }
65
- // Behavioral assertion runs once a host exposes the from-chain seam: a
66
- // subChains-bearing chain MUST return `sub_chain_unsupported` (422), not a
67
- // flattened success.
68
- expect(wcp?.subChains?.supported ?? false, 'unsupported host advertises no subChains block').toBe(false);
69
- });
70
- });
@@ -1,340 +0,0 @@
1
- /**
2
- * RFC 0151 — the compensation profile's shape contract.
3
- *
4
- * **What this proves, stated first because RFC 0147 §A.5 turns on it:** the
5
- * schemas admit exactly the shapes §A and §B describe and reject the ones they
6
- * forbid. That is *shape-only* evidence. It is **not** evidence that any host
7
- * orders an unwind correctly, persists a plan before the first inverse action,
8
- * or resumes one after a crash — and RFC 0151 cannot reach a defensible
9
- * `Accepted` on this alone. §A.5 requires a host executing every normative
10
- * behavioral path in strict mode, and none does.
11
- *
12
- * Saying that here rather than in a register keeps it next to the thing it
13
- * qualifies. A conformance file that verifies structure while its RFC claims
14
- * behavior is how "green suite" and "working protocol" come apart.
15
- *
16
- * The design constraints worth holding onto, each of which the schema encodes:
17
- *
18
- * - **Compensation is a second effect, not an undo.** It can fail, can be
19
- * partially applied, and can itself be harmful (RFC 0147 R9) — hence
20
- * `requiresApproval` and the security-high tier.
21
- * - **Inputs come from recorded facts.** §B forbids prompt/model regeneration
22
- * from constructing a compensation input during replay, because an inverse
23
- * built from a re-inferred value is not the inverse of what was done.
24
- * - **`nodeTypeId` resolves at registration**, so an unwind cannot fail on a
25
- * typo first discovered during a failure — the worst possible moment.
26
- *
27
- * Server-free.
28
- */
29
-
30
- import { describe, it, expect } from 'vitest';
31
- import { readFileSync, readdirSync } from 'node:fs';
32
- import { join, resolve as pathResolve } from 'node:path';
33
- import Ajv2020 from 'ajv/dist/2020.js';
34
- import { FIXTURES_DIR, SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
35
-
36
- /**
37
- * Schemas ship inside the package; RFC prose does not. The old form derived
38
- * BOTH from `V1_DIR` — null in the published tarball — and cast the null away,
39
- * so this file threw at import for every consumer installing from npm.
40
- */
41
- const RFCS_DIR = V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'RFCS');
42
-
43
- function schema(name: string): Record<string, unknown> {
44
- return JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8')) as Record<string, unknown>;
45
- }
46
-
47
- /**
48
- * Validate a candidate node against `WorkflowNode` alone.
49
- *
50
- * Every schema in the directory is registered first: `WorkflowNode` `$ref`s
51
- * siblings by filename, so compiling it in isolation resolves nothing and Ajv
52
- * throws rather than silently accepting — which is the good failure, but only
53
- * if the harness registers what the schema actually depends on.
54
- */
55
- function nodeValidator() {
56
- const ajv = new Ajv2020({ strict: false, allErrors: true });
57
- for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
58
- const s = schema(file);
59
- ajv.addSchema(s, file);
60
- }
61
- const wf = schema('workflow-definition.schema.json') as { $defs: Record<string, unknown> };
62
- return ajv.compile({ ...(wf.$defs['WorkflowNode'] as object), $defs: wf.$defs });
63
- }
64
-
65
- describe('RFC 0151 §A — compensation capability shape', () => {
66
- const caps = schema('capabilities.schema.json') as {
67
- properties: Record<string, { properties?: Record<string, unknown>; required?: string[] }>;
68
- };
69
-
70
- it('the capability family exists and is closed', () => {
71
- const c = caps.properties['compensation'];
72
- expect(c, 'RFC 0151 §A: `compensation` MUST be a declared capability family').toBeDefined();
73
- expect(c.required).toEqual(['supported']);
74
- expect(Object.keys(c.properties ?? {}).sort()).toEqual(
75
- ['manualIntervention', 'orderingModels', 'profileVersion', 'supported'].sort(),
76
- );
77
- });
78
-
79
- it('orderingModels is a closed enum of the two named models', () => {
80
- const models = (caps.properties['compensation'].properties?.['orderingModels'] as {
81
- items: { enum: string[] };
82
- }).items.enum;
83
- expect(
84
- [...models].sort(),
85
- 'RFC 0151 §A: an advertising host MUST implement `reverse-completion` and MAY add ' +
86
- '`dependency-graph`. A third model would be an unadvertised ordering guarantee.',
87
- ).toEqual(['dependency-graph', 'reverse-completion']);
88
- });
89
-
90
- it('profileVersion participates in identity, so it is constrained', () => {
91
- // §C derives the inverse-action id partly from `profileVersion`. An
92
- // unconstrained string there would let two hosts mint colliding identities
93
- // under different ordering rules.
94
- const pv = caps.properties['compensation'].properties?.['profileVersion'] as { pattern?: string };
95
- expect(pv.pattern, 'RFC 0151 §C: profileVersion is part of the inverse-action id').toBe('^[1-9][0-9]*$');
96
- });
97
- });
98
-
99
- describe('RFC 0151 §B — node compensation declaration', () => {
100
- const validate = nodeValidator();
101
- // A minimally VALID node — `WorkflowNode` requires `name`, `position`,
102
- // `config`, and `inputs` independently of this RFC. Building the fixture from
103
- // the real required set keeps the legs below testing `compensation` rather
104
- // than accidentally testing whether the base node is well-formed.
105
- const base = {
106
- id: 'reserve-inventory',
107
- typeId: 'vendor.shop.reserve',
108
- name: 'Reserve inventory',
109
- position: { x: 0, y: 0 },
110
- config: {},
111
- inputs: {},
112
- };
113
-
114
- it('a well-formed declaration validates', () => {
115
- const ok = validate({
116
- ...base,
117
- compensation: {
118
- nodeTypeId: 'vendor.shop.release',
119
- inputMapping: { reservationId: '${nodes.reserve-inventory.output.id}' },
120
- retry: { maxAttempts: 5, backoffMs: 1000 },
121
- requiresApproval: false,
122
- },
123
- });
124
- expect(ok, JSON.stringify(validate.errors)).toBe(true);
125
- });
126
-
127
- it('waiveRequiresApproval (S36) is an OPTIONAL boolean in the closed §B block — absent is valid (default = effective requiresApproval), non-boolean is refused', () => {
128
- const decl = { nodeTypeId: 'vendor.shop.release', requiresApproval: true };
129
- expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: false } }), JSON.stringify(validate.errors)).toBe(true);
130
- expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: true } }), JSON.stringify(validate.errors)).toBe(true);
131
- expect(validate({ ...base, compensation: { ...decl } }), 'absent MUST validate — the default is the obligation\'s effective requiresApproval, so no existing document changes meaning').toBe(true);
132
- expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: 'yes' } }), 'compensation.md §B: a plain boolean, not a policy language').toBe(false);
133
- });
134
-
135
- it('a node without compensation stays valid', () => {
136
- // The profile is optional. Absent MUST NOT become a validation error, or
137
- // every existing workflow in the corpus breaks.
138
- expect(validate({ ...base }), JSON.stringify(validate.errors)).toBe(true);
139
- });
140
-
141
- it('irreversibleEffect (RFC 0151 UQ4) is a sibling boolean, mutually exclusive with a compensation declaration', () => {
142
- // A statement that the effect HAS NO INVERSE. Sibling of `compensation`, so
143
- // `nodeTypeId` stays required and COMPATIBILITY §2.2 is not engaged.
144
- expect(validate({ ...base, irreversibleEffect: true }), JSON.stringify(validate.errors)).toBe(true);
145
- expect(validate({ ...base, irreversibleEffect: false, compensation: { nodeTypeId: 'vendor.shop.release' } }), JSON.stringify(validate.errors)).toBe(true);
146
- // Both = an effect that both has and lacks an inverse. The schema rejects it
147
- // (`if irreversibleEffect === true then not required compensation`).
148
- expect(
149
- validate({ ...base, irreversibleEffect: true, compensation: { nodeTypeId: 'vendor.shop.release' } }),
150
- 'compensation.md §B: a node declaring both irreversibleEffect: true and compensation is contradictory and MUST be rejected',
151
- ).toBe(false);
152
- expect(validate({ ...base, irreversibleEffect: 'yes' }), 'irreversibleEffect is a boolean').toBe(false);
153
- });
154
-
155
- it('the declaration is closed — an unknown key is rejected', () => {
156
- expect(
157
- validate({ ...base, compensation: { nodeTypeId: 'x', onFailure: 'ignore' } }),
158
- 'RFC 0151 §B: `compensation` is closed. An unrecognized key here is a silent behavioral ' +
159
- 'assumption on the unwind path, which is the least observable place to have one.',
160
- ).toBe(false);
161
- });
162
-
163
- it('nodeTypeId is required and non-empty', () => {
164
- expect(validate({ ...base, compensation: {} }), 'nodeTypeId is required').toBe(false);
165
- expect(
166
- validate({ ...base, compensation: { nodeTypeId: '' } }),
167
- 'RFC 0151 §B: `nodeTypeId` MUST resolve at registration — an empty id resolves to nothing, ' +
168
- 'and the failure would surface only during an unwind.',
169
- ).toBe(false);
170
- });
171
-
172
- it('retry bounds are integers within sane floors', () => {
173
- expect(validate({ ...base, compensation: { nodeTypeId: 'x', retry: { maxAttempts: 0 } } })).toBe(false);
174
- expect(validate({ ...base, compensation: { nodeTypeId: 'x', retry: { backoffMs: -1 } } })).toBe(false);
175
- expect(validate({ ...base, compensation: { nodeTypeId: 'x', retry: { maxAttempts: 1, backoffMs: 0 } } })).toBe(true);
176
- });
177
- });
178
-
179
- describe('RFC 0151 §B — the workflow-level compensation policy (`settings.compensation`)', () => {
180
- // `compensation-policy.schema.json` was the last `Affects` artifact of the whole
181
- // RFC 0147 program that did not exist. It says WHEN an unwind starts and HOW it
182
- // runs; the node-level declaration only says WHAT the inverse action is.
183
- const POLICY_SCHEMA = 'compensation-policy.schema.json';
184
-
185
- function ajvAll() {
186
- const ajv = new Ajv2020({ strict: false, allErrors: true });
187
- for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
188
- ajv.addSchema(schema(file), file);
189
- }
190
- return ajv;
191
- }
192
- const policyValidator = () => {
193
- const ajv = ajvAll();
194
- return ajv.getSchema(POLICY_SCHEMA) ?? ajv.compile(schema(POLICY_SCHEMA));
195
- };
196
- const workflowValidator = () => {
197
- const ajv = ajvAll();
198
- return ajv.getSchema('workflow-definition.schema.json') ?? ajv.compile(schema('workflow-definition.schema.json'));
199
- };
200
-
201
- const minimalPolicy = { triggers: ['node-failure'] };
202
- const fullPolicy = {
203
- profileVersion: '1',
204
- orderingModel: 'reverse-completion',
205
- triggers: ['node-failure', 'run-cancel', 'cap-breach', 'operator-request'],
206
- retry: { maxAttempts: 3, backoffMs: 500 },
207
- timeoutMs: 30_000,
208
- exhaustedDisposition: 'manual-intervention',
209
- approvalScope: 'all',
210
- onParentCancel: 'pause',
211
- };
212
-
213
- it('the schema exists, is closed, and requires triggers', () => {
214
- const p = schema(POLICY_SCHEMA) as { additionalProperties?: boolean; required?: string[]; $id?: string };
215
- expect(p.$id).toBe('https://openwop.dev/spec/v1/compensation-policy.schema.json');
216
- expect(p.additionalProperties, 'closed — a host and an author must not be able to disagree about a key').toBe(false);
217
- expect(p.required, 'a policy that names no trigger is not a policy').toEqual(['triggers']);
218
- });
219
-
220
- it('a minimal and a full policy validate', () => {
221
- const validate = policyValidator();
222
- expect(validate(minimalPolicy), JSON.stringify(validate.errors)).toBe(true);
223
- expect(validate(fullPolicy), JSON.stringify(validate.errors)).toBe(true);
224
- });
225
-
226
- it('closed vocabularies: an unknown key, trigger, ordering model, or disposition is rejected', () => {
227
- const validate = policyValidator();
228
- for (const bad of [
229
- { ...minimalPolicy, rollback: true },
230
- { triggers: [] },
231
- { triggers: ['on-error'] },
232
- { triggers: ['node-failure', 'node-failure'] },
233
- { ...minimalPolicy, orderingModel: 'forward' },
234
- { ...minimalPolicy, exhaustedDisposition: 'ignore' },
235
- { ...minimalPolicy, approvalScope: 'none' },
236
- { ...minimalPolicy, onParentCancel: 'abandon' },
237
- { ...minimalPolicy, profileVersion: '0' },
238
- { ...minimalPolicy, retry: { maxAttempts: 0 } },
239
- ]) {
240
- expect(validate(bad), `MUST be rejected: ${JSON.stringify(bad)}`).toBe(false);
241
- }
242
- });
243
-
244
- it('there is no `none` approval scope — a policy can only escalate approval, never strip it', () => {
245
- const p = schema(POLICY_SCHEMA) as { properties: { approvalScope: { enum: string[] } } };
246
- expect(p.properties.approvalScope.enum).toEqual(['declared', 'all']);
247
- });
248
-
249
- it('attaches to WorkflowDefinition as `settings.compensation` and validates through the workflow schema', () => {
250
- const wf = schema('workflow-definition.schema.json') as {
251
- $defs: { WorkflowSettings: { properties: Record<string, { $ref?: string }> } };
252
- };
253
- expect(wf.$defs.WorkflowSettings.properties['compensation']?.$ref).toBe(POLICY_SCHEMA);
254
- const validate = workflowValidator();
255
- // A real, valid workflow fixture — so the leg proves the $ref is enforced
256
- // through the workflow schema rather than that a hand-built object happens
257
- // to satisfy WorkflowDefinition's required set.
258
- const base = JSON.parse(
259
- readFileSync(join(FIXTURES_DIR, 'conformance-subworkflow-child.json'), 'utf8'),
260
- ) as { settings?: Record<string, unknown> };
261
- expect(validate(base), `fixture must be valid on its own: ${JSON.stringify(validate.errors)}`).toBe(true);
262
- const ok = validate({ ...base, settings: { ...(base.settings ?? {}), compensation: fullPolicy } });
263
- expect(ok, JSON.stringify(validate.errors)).toBe(true);
264
- const bad = validate({ ...base, settings: { ...(base.settings ?? {}), compensation: { triggers: [] } } });
265
- expect(bad, 'the $ref must actually be enforced through the workflow schema').toBe(false);
266
- });
267
- });
268
-
269
- describe('RFC 0151 §D — the run rollup `compensationStatus` (RunSnapshot)', () => {
270
- // Resolves RFC 0151 UQ3: `RunSnapshot` is the sole owner. Debug bundles and the
271
- // AsyncAPI `run.snapshot` reuse the snapshot by $ref, so one property covers all
272
- // three surfaces — and one enum keeps them from drifting apart.
273
- const RUN_SNAPSHOT_SCHEMA = 'run-snapshot.schema.json';
274
- const STATUSES = ['none', 'pending', 'running', 'completed', 'partial', 'failed', 'manual'] as const;
275
-
276
- function snapshotValidator() {
277
- const ajv = new Ajv2020({ strict: false, allErrors: true });
278
- for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
279
- ajv.addSchema(schema(file), file);
280
- }
281
- return ajv.getSchema(RUN_SNAPSHOT_SCHEMA) ?? ajv.compile(schema(RUN_SNAPSHOT_SCHEMA));
282
- }
283
-
284
- it('is declared on RunSnapshot as a closed enum of the seven §D values', () => {
285
- const snap = schema(RUN_SNAPSHOT_SCHEMA) as {
286
- properties: Record<string, { type?: string; enum?: string[] }>;
287
- required: string[];
288
- };
289
- const field = snap.properties['compensationStatus'];
290
- expect(field, 'RunSnapshot MUST declare `compensationStatus` (RFC 0151 §D, UQ3)').toBeDefined();
291
- expect(field?.enum, 'the value set is closed and exactly the seven §D values').toEqual([...STATUSES]);
292
- expect(
293
- snap.required.includes('compensationStatus'),
294
- 'OPTIONAL on the schema — presence is governed by the capability gate in prose, not by `required`, ' +
295
- 'so a host that does not advertise `compensation` still validates',
296
- ).toBe(false);
297
- });
298
-
299
- it('every §D value validates and a foreign value is rejected', () => {
300
- const validate = snapshotValidator();
301
- for (const value of STATUSES) {
302
- const ok = validate({ runId: 'r1', workflowId: 'w1', status: 'failed', compensationStatus: value });
303
- expect(ok, `compensationStatus=${value} MUST validate: ${JSON.stringify(validate.errors)}`).toBe(true);
304
- }
305
- for (const bad of ['compensating', 'COMPLETED', 'done', 'skipped', 'paused', 1, null, true]) {
306
- const ok = validate({ runId: 'r1', workflowId: 'w1', status: 'failed', compensationStatus: bad });
307
- expect(ok, `compensationStatus=${JSON.stringify(bad)} MUST be rejected — the fold is closed`).toBe(false);
308
- }
309
- });
310
-
311
- it('a snapshot without the field still validates — the gate lives in prose', () => {
312
- const validate = snapshotValidator();
313
- expect(validate({ runId: 'r1', workflowId: 'w1', status: 'completed' })).toBe(true);
314
- });
315
-
316
- it('the forward `status` enum gained no `compensating` value — §D forbids reinterpreting it', () => {
317
- const snap = schema(RUN_SNAPSHOT_SCHEMA) as { properties: { status: { enum: string[] } } };
318
- expect(snap.properties.status.enum).not.toContain('compensating');
319
- });
320
- });
321
-
322
- describe.skipIf(RFCS_DIR === null)('RFC 0151 — what this file does NOT establish', () => {
323
- it('records that behavioral conformance is absent, per RFC 0147 §A.5', () => {
324
- // Not decoration. RFC 0147 §A.5 forbids `Accepted` on shape-only evidence
325
- // for a behavioral requirement, and this scenario is shape-only by
326
- // construction: it compiles schemas and never contacts a host. The RFC's
327
- // acceptance criteria and `docs/RFC-0147-SELF-AUDIT.md` both record 0151 as
328
- // violating §A.5, and this leg exists so that reading the conformance suite
329
- // alone cannot leave a different impression.
330
- const rfc = readFileSync(
331
- join(RFCS_DIR as string, '0151-compensation-and-partial-failure-profile.md'),
332
- 'utf8',
333
- );
334
- expect(
335
- /Reverse-unwind, retry, crash, partial\/manual, approval, replay, and isolation scenarios pass\.\s*\(/.test(rfc),
336
- 'RFC 0151\'s behavioral acceptance item MUST remain unticked and annotated: no host executes ' +
337
- 'an unwind, so ordering, persistence-before-first-action, and crash resumption are unproven.',
338
- ).toBe(true);
339
- });
340
- });
@@ -1,250 +0,0 @@
1
- /**
2
- * RFC 0155 §B + §C — the stable core manifest and the extension registry.
3
- *
4
- * §B's value is not the inventory. It is the sentence after it: *"prose and code
5
- * profile definitions MUST be generated from or checked against this manifest."*
6
- * Three places describe the core-standard floor independently — the profile
7
- * prose, `PROFILE_FLOOR_SCENARIOS`, and the requirement registry — and before
8
- * this they could only be assumed to agree.
9
- *
10
- * They did not. `PROFILE_FLOOR_SCENARIOS` was an incomplete transcription of
11
- * `profiles.md`, and every profile it omitted verified as floor-proven against
12
- * nothing (RFC 0148 §C). A manifest with a parity gate is the mechanism that
13
- * would have caught that, which is why the manifest is DERIVED and never
14
- * hand-listed: a hand-listed manifest drifts the moment the corpus moves and
15
- * then asserts the drift with a digest attached.
16
- *
17
- * §C's bar for `stable` is deliberately hard — normative prose, schemas,
18
- * non-vacuous conformance, SDK support, and at least one Tier-3 implementation.
19
- * The consequence, stated plainly rather than worked around: **nothing in this
20
- * corpus can currently be `stable`**, because no Tier-3 host exists. That is a
21
- * fact about adoption, not about the work.
22
- *
23
- * Server-free; reads the corpus.
24
- */
25
-
26
- import { describe, it, expect } from 'vitest';
27
- import { readFileSync, existsSync } from 'node:fs';
28
- import { join } from 'node:path';
29
- import { V1_DIR } from '../lib/paths.js';
30
- import { PROFILE_FLOOR_SCENARIOS } from '../lib/profiles.js';
31
- import { requirementsFor } from '../lib/requirement-registry.js';
32
-
33
- const MATURITIES = ['experimental', 'draft', 'stable', 'deprecated'] as const;
34
- type Maturity = (typeof MATURITIES)[number];
35
-
36
- interface Extension {
37
- readonly id: string;
38
- readonly maturity: Maturity;
39
- readonly owningRfc: string | null;
40
- readonly capabilityPath: string;
41
- readonly dependsOn: readonly string[];
42
- readonly securityTier: string;
43
- readonly minimumSuiteVersion: string | null;
44
- readonly evidenceTier: string | null;
45
- readonly note?: string;
46
- }
47
-
48
- /** The capability schema, for `capabilityPath` resolution. */
49
- function caps(): Record<string, unknown> {
50
- return JSON.parse(
51
- readFileSync(join(V1_DIR as string, '..', '..', 'schemas', 'capabilities.schema.json'), 'utf8'),
52
- ) as Record<string, unknown>;
53
- }
54
-
55
- function readJson<T>(name: string): T {
56
- return JSON.parse(readFileSync(join(V1_DIR as string, name), 'utf8')) as T;
57
- }
58
-
59
- describe.skipIf(V1_DIR === null)('RFC 0155 §B — core-standard manifest parity', () => {
60
- const manifest = V1_DIR === null ? null : readJson<{
61
- profile: string;
62
- digest: string;
63
- floor: { requiredScenarios: string[]; requiredAnyPrefix: string[] };
64
- requirementIds: string[];
65
- openapiOperations: string[];
66
- schemas: { file: string; $id: string | null }[];
67
- }>('core-standard-manifest.json');
68
-
69
- it('the manifest exists and is non-trivial', () => {
70
- // Guard: an empty manifest would make every parity leg below vacuously true,
71
- // which is the exact shape RFC 0148 §C found in the floor verifier.
72
- expect(manifest, 'RFC 0155 §B: the manifest MUST be published').not.toBeNull();
73
- const m = manifest as NonNullable<typeof manifest>;
74
- expect(m.profile).toBe('openwop-core-standard');
75
- expect(m.digest, 'the manifest MUST carry a digest').toMatch(/^[0-9a-f]{64}$/);
76
- expect(m.floor.requiredScenarios.length).toBeGreaterThan(5);
77
- expect(m.openapiOperations.length).toBeGreaterThan(20);
78
- expect(m.schemas.length).toBeGreaterThan(20);
79
- });
80
-
81
- it('the manifest floor matches the floor the suite actually enforces', () => {
82
- // The parity §B asks for. If these drift, the manifest is asserting a floor
83
- // nobody runs — worse than no manifest, because it looks authoritative.
84
- const m = manifest as NonNullable<typeof manifest>;
85
- const live = PROFILE_FLOOR_SCENARIOS['openwop-core-standard'];
86
- expect(live, 'the suite MUST define a core-standard floor').toBeDefined();
87
- expect([...m.floor.requiredScenarios].sort()).toEqual([...(live?.required ?? [])].sort());
88
- expect([...m.floor.requiredAnyPrefix].sort()).toEqual([...(live?.requiredAnyPrefix ?? [])].sort());
89
- });
90
-
91
- it('the manifest requirement IDs match the requirement registry', () => {
92
- const m = manifest as NonNullable<typeof manifest>;
93
- const fromRegistry = requirementsFor('openwop-core-standard');
94
- expect(fromRegistry, 'core-standard MUST have registered requirements').not.toBeNull();
95
- expect([...m.requirementIds].sort()).toEqual([...(fromRegistry as readonly string[])].sort());
96
- });
97
-
98
- it('every schema the manifest lists declares an $id', () => {
99
- const m = manifest as NonNullable<typeof manifest>;
100
- const missing = m.schemas.filter((s) => s.$id === null).map((s) => s.file);
101
- expect(missing, 'CONTRIBUTING.md: every schema carries an `$id` under openwop.dev/spec/v1/').toEqual([]);
102
- });
103
- });
104
-
105
- describe.skipIf(V1_DIR === null)('RFC 0155 §C — extension registry', () => {
106
- const registry = V1_DIR === null ? null : readJson<{ extensions: Extension[] }>('extensions.json');
107
-
108
- it('the registry exists and every record is closed', () => {
109
- expect(registry, 'RFC 0155 §C: `spec/v1/extensions.json` MUST exist').not.toBeNull();
110
- const exts = (registry as NonNullable<typeof registry>).extensions;
111
- expect(exts.length, 'the registry MUST cover the program extensions').toBeGreaterThan(3);
112
- for (const e of exts) {
113
- for (const k of ['id', 'maturity', 'owningRfc', 'capabilityPath', 'dependsOn', 'securityTier']) {
114
- expect(e[k as keyof Extension], `${e.id} MUST declare ${k}`).toBeDefined();
115
- }
116
- expect(MATURITIES, `${e.id}: maturity is a closed enum`).toContain(e.maturity);
117
- }
118
- expect(new Set(exts.map((e) => e.id)).size, 'extension ids MUST be unique').toBe(exts.length);
119
- });
120
-
121
- it('no extension is `stable` without a Tier-3 implementation', () => {
122
- // §C: stable requires normative prose, schemas, non-vacuous conformance, SDK
123
- // support where applicable, and at least one Tier-3 implementation. The last
124
- // one is the binding constraint here, and it is external to this repo — no
125
- // Tier-3 host exists, so NOTHING can currently be stable. Recording that
126
- // ceiling is the honest move; promoting anything past it would be the
127
- // overclaim RFC 0147 §A bans.
128
- const overclaimed = (registry as NonNullable<typeof registry>).extensions
129
- .filter((e) => e.maturity === 'stable' && (e.evidenceTier === null || e.evidenceTier === undefined))
130
- .map((e) => e.id);
131
- expect(
132
- overclaimed,
133
- 'RFC 0155 §C: `stable` requires at least one Tier-3 implementation, recorded in `evidenceTier`. ' +
134
- 'An extension marked stable with no evidence tier is a claim the corpus cannot substantiate.',
135
- ).toEqual([]);
136
- });
137
-
138
- it('every dependency resolves to a known profile or listed extension', () => {
139
- // A dependency on something that does not exist is a closure hole: the
140
- // record looks complete and the graph does not.
141
- const exts = (registry as NonNullable<typeof registry>).extensions;
142
- const known = new Set<string>([...Object.keys(PROFILE_FLOOR_SCENARIOS), ...exts.map((e) => e.id)]);
143
- const dangling = exts.flatMap((e) =>
144
- e.dependsOn.filter((d) => !known.has(d)).map((d) => `${e.id} -> ${d}`),
145
- );
146
- expect(dangling, 'RFC 0155 §C: `dependsOn` MUST resolve').toEqual([]);
147
- });
148
-
149
- it('every capabilityPath resolves against the capability schema', () => {
150
- // Fifth axis of the named-list check, and it found four of six broken.
151
- // Three were typos introduced when this registry was written —
152
- // `a2a.protocolVersion` for `protocolVersions`, the same for MCP, and
153
- // `workloadIdentity.supported` omitting its `auth.` parent. The fourth,
154
- // `idempotency.supported`, pointed at a field the corpus USES in its own
155
- // examples but had never DECLARED; it validated only because that family
156
- // carries `additionalProperties: true`, so a typo like `suported` was
157
- // accepted silently.
158
- //
159
- // An extension whose capabilityPath does not resolve is unreachable: a
160
- // consumer following the registry to find the flag finds nothing, and the
161
- // registry looks complete while pointing at empty space.
162
- const schema = caps() as { properties: Record<string, unknown> };
163
- const unresolved: string[] = [];
164
- for (const e of (registry as NonNullable<typeof registry>).extensions) {
165
- let node = schema.properties as Record<string, { properties?: Record<string, unknown> }> | undefined;
166
- let ok = true;
167
- for (const part of e.capabilityPath.split('.')) {
168
- if (node === undefined || !(part in node)) {
169
- ok = false;
170
- break;
171
- }
172
- node = node[part]?.properties as typeof node;
173
- }
174
- if (!ok) unresolved.push(`${e.id} -> ${e.capabilityPath}`);
175
- }
176
- expect(
177
- unresolved,
178
- 'RFC 0155 §C: `capabilityPath` MUST resolve to a declared property in ' +
179
- '`capabilities.schema.json`. An unresolvable path makes the extension unreachable — a ' +
180
- 'consumer following the registry to find the flag finds nothing, while the registry ' +
181
- 'still reads as complete.\n ' + unresolved.join('\n '),
182
- ).toEqual([]);
183
- });
184
-
185
- it('the coverage block is present and accounts for every capability family (RFC 0155 §C, acceptance item 3)', () => {
186
- // "Unlisted means uncovered" was a sentence; this makes it a checked list.
187
- // The block is DERIVED by scripts/generate-extension-registry-coverage.mjs
188
- // (--check runs in openwop:check); here we assert the invariant it encodes
189
- // so a tarball consumer sees it too: every top-level capability family is
190
- // either a core predicate field, covered by a record's capabilityPath, or
191
- // listed as uncovered — and nothing is in two buckets.
192
- const reg = registry as unknown as {
193
- coverage?: {
194
- familiesTotal: number;
195
- coreFields: string[];
196
- metadataFields?: string[];
197
- metadataRationale?: Record<string, string>;
198
- covered: string[];
199
- uncovered: string[];
200
- };
201
- extensions: Extension[];
202
- };
203
- expect(reg.coverage, 'RFC 0155 §C: the registry MUST carry a derived `coverage` block').toBeDefined();
204
- const cov = reg.coverage as NonNullable<typeof reg.coverage>;
205
- const metadata = cov.metadataFields ?? [];
206
- const families = Object.keys((caps().properties as Record<string, unknown>) ?? {}).sort();
207
- expect(cov.familiesTotal).toBe(families.length);
208
- const all = [...cov.coreFields, ...metadata, ...cov.covered, ...cov.uncovered].sort();
209
- expect(all, 'core + metadata + covered + uncovered MUST partition the family set exactly').toEqual(families);
210
- expect(new Set(all).size, 'no family may sit in two buckets').toBe(all.length);
211
- const reached = new Set(reg.extensions.map((e) => e.capabilityPath.split('.')[0]));
212
- for (const f of cov.covered) expect(reached.has(f), `${f} listed as covered MUST be reached by a record`).toBe(true);
213
- for (const f of cov.uncovered) expect(reached.has(f), `${f} listed as uncovered MUST NOT be reached by a record`).toBe(false);
214
- // Metadata is the one bucket a family can be moved INTO by hand, so it is
215
- // the one that could hide an extension: every entry MUST carry a stated
216
- // rationale, and no metadata key may carry a `supported` flag — a key that
217
- // gates behaviour is a family, not a description of the document.
218
- for (const f of metadata) {
219
- expect(typeof cov.metadataRationale?.[f], `${f}: a metadata field MUST state why it is not an extension`).toBe('string');
220
- const props = (caps().properties as Record<string, { properties?: Record<string, unknown> }>)[f]?.properties ?? {};
221
- expect('supported' in props, `${f} is listed as metadata but carries a \`supported\` flag — that is an extension family`).toBe(false);
222
- }
223
- // The honest number, asserted so it cannot silently shrink by deletion of the
224
- // uncovered list rather than by adding records.
225
- expect(cov.uncovered.length + cov.covered.length + cov.coreFields.length + metadata.length).toBe(families.length);
226
- });
227
-
228
- it('every record names the RFC — or, for a v1 base advertisement, the spec document — that owns it', () => {
229
- // Vendor extensions may not use an `openwop-*` id without an accepted RFC
230
- // (§F). The owning RFC is what makes that checkable. Six advertisements
231
- // predate the RFC process (they shipped in the v1 base corpus: `secrets`,
232
- // `webhooks`, `i18n`, `aiProviders`, `envelopeContracts`, `envelopeStrictness`);
233
- // those carry `owningRfc: null` and an `owningDoc` under spec/v1/ that MUST
234
- // exist — the steward's own corpus is the RFC-equivalent authority for them.
235
- for (const e of (registry as NonNullable<typeof registry>).extensions) {
236
- const rec = e as Extension & { owningDoc?: string; securityTier?: string };
237
- if (rec.owningRfc === null) {
238
- expect(typeof rec.owningDoc, `${e.id}: \`owningRfc: null\` requires an \`owningDoc\``).toBe('string');
239
- expect(rec.owningDoc, `${e.id}: owningDoc MUST be a spec/v1 document`).toMatch(/^spec\/v1\/[a-z0-9-]+\.md$/);
240
- if (V1_DIR !== null) {
241
- const file = join(V1_DIR, (rec.owningDoc as string).replace(/^spec\/v1\//, ''));
242
- expect(existsSync(file), `${e.id}: owningDoc ${rec.owningDoc} MUST exist`).toBe(true);
243
- }
244
- } else {
245
- expect(e.owningRfc, `${e.id} MUST name an owning RFC`).toMatch(/^\d{4}$/);
246
- }
247
- expect(['high', 'medium', 'low'], `${e.id}: securityTier is a closed enum`).toContain(rec.securityTier);
248
- }
249
- });
250
- });