@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.
- package/CHANGELOG.md +8 -0
- package/README.md +9 -0
- package/dist/cli.js +7 -2
- package/package.json +31 -2
- package/schemas/CORPUS-STAMP.json +99 -3
- package/src/cli.ts +7 -5
- package/src/global-setup.ts +13 -0
- package/src/lib/corpus-stamp.ts +125 -0
- package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
- package/src/lib/fork-availability.test.ts +0 -69
- package/src/lib/global-setup.test.ts +0 -76
- package/src/lib/grpc-framing.test.ts +0 -96
- package/src/lib/oidc-issuer.test.ts +0 -328
- package/src/lib/otel-collector-grpc.test.ts +0 -191
- package/src/lib/otel-collector.test.ts +0 -303
- package/src/lib/otlp-protobuf.test.ts +0 -461
- package/src/lib/polling.test.ts +0 -80
- package/src/lib/requirement-ids.test.ts +0 -83
- package/src/lib/requirement-ledger.test.ts +0 -75
- package/src/lib/risk-disposition.test.ts +0 -91
- package/src/lib/saml-idp.test.ts +0 -127
- package/src/lib/spec-coherence-registry.test.ts +0 -155
- package/src/lib/webhook-receiver.test.ts +0 -144
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
- package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
- package/src/scenarios/capability-example-root-layout.test.ts +0 -272
- package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
- package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
- package/src/scenarios/compensation-profile.test.ts +0 -340
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
- package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
- package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
- package/src/scenarios/effect-identity-composition.test.ts +0 -129
- package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
- package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
- package/src/scenarios/form-content-packs.test.ts +0 -415
- package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
- package/src/scenarios/normative-example-extraction.test.ts +0 -242
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
- package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
- package/src/scenarios/protocol-version-grammar.test.ts +0 -119
- package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
- package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
- package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
- package/src/scenarios/semantic-digest-v2.test.ts +0 -128
- package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
- package/src/scenarios/spec-section-citations.test.ts +0 -132
- package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
- package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
- package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
- 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
|
-
});
|