@openwop/openwop-conformance 1.152.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 (57) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/README.md +9 -0
  3. package/dist/cli.js +7 -2
  4. package/dist/lib/requirement-ids.js +100 -0
  5. package/dist/lib/scenario-disposition.js +22 -0
  6. package/package.json +33 -2
  7. package/requirement-aliases.json +4 -0
  8. package/requirements.json +23554 -0
  9. package/schemas/CORPUS-STAMP.json +99 -3
  10. package/src/cli.ts +7 -5
  11. package/src/global-setup.ts +13 -0
  12. package/src/lib/corpus-stamp.ts +125 -0
  13. package/src/lib/requirement-ids.ts +111 -0
  14. package/src/lib/scenario-disposition.ts +22 -0
  15. package/src/setup.ts +84 -5
  16. package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
  17. package/src/lib/fork-availability.test.ts +0 -69
  18. package/src/lib/global-setup.test.ts +0 -76
  19. package/src/lib/grpc-framing.test.ts +0 -96
  20. package/src/lib/oidc-issuer.test.ts +0 -328
  21. package/src/lib/otel-collector-grpc.test.ts +0 -191
  22. package/src/lib/otel-collector.test.ts +0 -303
  23. package/src/lib/otlp-protobuf.test.ts +0 -461
  24. package/src/lib/polling.test.ts +0 -80
  25. package/src/lib/requirement-ledger.test.ts +0 -75
  26. package/src/lib/risk-disposition.test.ts +0 -91
  27. package/src/lib/saml-idp.test.ts +0 -127
  28. package/src/lib/spec-coherence-registry.test.ts +0 -155
  29. package/src/lib/webhook-receiver.test.ts +0 -144
  30. package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
  31. package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
  32. package/src/scenarios/capability-example-root-layout.test.ts +0 -272
  33. package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
  34. package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
  35. package/src/scenarios/compensation-profile.test.ts +0 -340
  36. package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
  37. package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
  38. package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
  39. package/src/scenarios/effect-identity-composition.test.ts +0 -129
  40. package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
  41. package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
  42. package/src/scenarios/form-content-packs.test.ts +0 -415
  43. package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
  44. package/src/scenarios/normative-example-extraction.test.ts +0 -242
  45. package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
  46. package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
  47. package/src/scenarios/protocol-version-grammar.test.ts +0 -119
  48. package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
  49. package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
  50. package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
  51. package/src/scenarios/semantic-digest-v2.test.ts +0 -128
  52. package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
  53. package/src/scenarios/spec-section-citations.test.ts +0 -132
  54. package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
  55. package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
  56. package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
  57. package/src/scenarios/workload-identity-profile.test.ts +0 -184
@@ -1,415 +0,0 @@
1
- /**
2
- * Form-content packs (RFC 0137, `Active`).
3
- *
4
- * A form-content pack (`kind: "form-content"`) distributes FORM TEMPLATES — a
5
- * named, versioned, ordered set of typed input fields a host instantiates into
6
- * an ordinary, fully editable form through its normal create path. It is the
7
- * sixth declarative pack kind under RFC 0107 and is purely inert: no `runtime`,
8
- * no entry point, no handler, no submission surface the host would not
9
- * otherwise accept.
10
- *
11
- * Always-on + server-free. Three parts:
12
- *
13
- * PART 1 — contract present. `form-content-packs.md` carries the
14
- * instantiation rules + the F1 trust boundary; `registry-operations.md`
15
- * §"Validation flow" selects the per-kind source schema for `form-content`
16
- * and skips the runtime check for it. Guards against the requirement being
17
- * silently dropped.
18
- *
19
- * PART 2 — the version-manifest schema admits the kind and still rejects
20
- * malformed ones. Includes an explicit leg for the `anyOf` payload gate:
21
- * extending the `kind` enum and declaring `templates` is NECESSARY BUT NOT
22
- * SUFFICIENT — without a `templates` branch in `anyOf`, every form-content
23
- * manifest is rejected. That omission is believed to be the second face of
24
- * the CI failure that motivated RFC 0137, so it gets its own assertion.
25
- *
26
- * PART 3 — the field vocabulary is SHARED, not forked. `fields[].type` in
27
- * `form-content-pack-manifest.schema.json` MUST be byte-identical to
28
- * `InputField.type` in `chat-card-pack-manifest.schema.json`. Two declarative
29
- * kinds that both collect typed user input, rendered by the same host
30
- * machinery, MUST agree on what a field type means. This is the regression
31
- * guard for RFC 0137 R2: it fails the moment either kind's vocabulary is
32
- * widened alone.
33
- *
34
- * @see spec/v1/form-content-packs.md
35
- * @see spec/v1/chat-card-packs.md §"Input fields — a closed portable subset"
36
- * @see spec/v1/registry-operations.md §"Validation flow"
37
- * @see schemas/form-content-pack-manifest.schema.json
38
- * @see RFCS/0137-form-content-packs.md, RFCS/0107-publishable-declarative-pack-kinds.md
39
- */
40
-
41
- import { describe, it, expect } from 'vitest';
42
- import { readFileSync } from 'node:fs';
43
- import { join } from 'node:path';
44
- import Ajv2020 from 'ajv/dist/2020.js';
45
- import addFormats from 'ajv-formats';
46
- import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
47
-
48
- const why = (specRef: string, requirement: string): string => `${specRef} — ${requirement}`;
49
-
50
- const readSchema = (name: string): Record<string, unknown> =>
51
- JSON.parse(readFileSync(join(SCHEMAS_DIR, name), 'utf8'));
52
-
53
- describe('form-content-packs: contract present in the corpus (RFC 0137, server-free)', () => {
54
- const registryDoc = V1_DIR ? readFileSync(join(V1_DIR, 'registry-operations.md'), 'utf8') : '';
55
- const formDoc = V1_DIR ? readFileSync(join(V1_DIR, 'form-content-packs.md'), 'utf8') : '';
56
-
57
- it.skipIf(V1_DIR === null)('registry-operations.md §Validation flow selects the form-content source schema by `kind`', () => {
58
- expect(
59
- /form-content[\s\S]{0,160}form-content-pack-manifest\.schema\.json/.test(registryDoc),
60
- why('registry-operations.md §Validation flow #3', '`kind: "form-content"` validates against its own source schema (RFC 0137)'),
61
- ).toBe(true);
62
- });
63
-
64
- it.skipIf(V1_DIR === null)('registry-operations.md skips the runtime-support check for form-content', () => {
65
- expect(
66
- /declarative[\s\S]{0,200}form-content/.test(registryDoc),
67
- why('registry-operations.md §Validation flow #7', 'form-content is a declarative kind — the runtime check is skipped'),
68
- ).toBe(true);
69
- });
70
-
71
- it.skipIf(V1_DIR === null)('registry-operations.md extends the declarative-id denormalization to templateId', () => {
72
- expect(
73
- /templates\[\]\.templateId/.test(registryDoc),
74
- why('registry-operations.md §Type-ID indexing', 'a registry SHOULD denormalize `templates[].templateId` (RFC 0137)'),
75
- ).toBe(true);
76
- });
77
-
78
- it.skipIf(V1_DIR === null)('form-content-packs.md requires the host to use its NORMAL create path and execute nothing', () => {
79
- expect(
80
- /MUST[\s\S]{0,120}normal[\s\S]{0,40}create path/i.test(formDoc),
81
- why('form-content-packs.md §Instantiation', 'the host MUST instantiate through its normal create path'),
82
- ).toBe(true);
83
- expect(
84
- /MUST NOT execute anything from the pack/i.test(formDoc),
85
- why('form-content-packs.md §Instantiation', 'the host MUST NOT execute anything from the pack — the kind is inert'),
86
- ).toBe(true);
87
- });
88
-
89
- it.skipIf(V1_DIR === null)('form-content-packs.md carries the F1 trust boundary, incl. "a signature is not content trust"', () => {
90
- expect(
91
- /untrusted/i.test(formDoc) && /contentTrust/.test(formDoc),
92
- why('form-content-packs.md §Trust boundary', 'pack-authored strings are untrusted; prompts propagate meta.contentTrust'),
93
- ).toBe(true);
94
- expect(
95
- /signature proves[\s\S]{0,80}not[\s\S]{0,60}trustworthy|MUST NOT treat pack provenance as content trust/i.test(formDoc),
96
- why('form-content-packs.md §Trust boundary', 'a signature proves authorship, NOT that the authored bytes are safe'),
97
- ).toBe(true);
98
- expect(
99
- /Length bounds are not a trust boundary/i.test(formDoc),
100
- why('form-content-packs.md §Trust boundary', 'maxLength is a resource guard, NOT sanitization'),
101
- ).toBe(true);
102
- });
103
-
104
- it.skipIf(V1_DIR === null)('the spec distinguishes DEGRADE (well-formed extension) from REFUSE (malformed value)', () => {
105
- expect(
106
- /Degrade applies to \*extensions\*, not to malformed values/i.test(formDoc),
107
- why('form-content-packs.md §Instantiation', 'MUST-degrade is scoped to vendor.*/x- extensions, not bare unknowns'),
108
- ).toBe(true);
109
- expect(
110
- /MUST NOT collapse these into one rule in either direction/i.test(formDoc),
111
- why(
112
- 'form-content-packs.md §Instantiation',
113
- 'refusing a well-formed extension breaks forward compat; degrading a malformed value hides an authoring error',
114
- ),
115
- ).toBe(true);
116
- });
117
-
118
- it.skipIf(V1_DIR === null)('form-content-packs.md forbids minting a second field-type vocabulary', () => {
119
- expect(
120
- /MUST NOT\W{0,4}\s*define its own field-type vocabulary/i.test(formDoc),
121
- why('form-content-packs.md §Field types', 'the kind reuses the RFC 0071 portable subset rather than defining its own'),
122
- ).toBe(true);
123
- });
124
- });
125
-
126
- describe('form-content-packs: version-manifest schema admits the kind (RFC 0137, server-free)', () => {
127
- const ajv = new Ajv2020({ allErrors: true, strict: false });
128
- addFormats(ajv);
129
- const versionManifestSchema = readSchema('registry-version-manifest.schema.json');
130
- const validate = ajv.compile(versionManifestSchema);
131
-
132
- const base = { name: 'core.openwop.forms', version: '1.0.0', engines: { openwop: '>=1.0.0' }, integrity: 'sha256-abc=' };
133
- const template = {
134
- templateId: 'core.openwop.form.rsvp',
135
- version: '1.0.0',
136
- label: 'RSVP',
137
- title: 'Will you be joining us?',
138
- fields: [{ id: 'guestName', type: 'text', label: 'Your name' }],
139
- };
140
-
141
- it('`form-content` is in the kind enum and `templates` is a declared property', () => {
142
- const props = (versionManifestSchema.properties ?? {}) as Record<string, { enum?: string[] }>;
143
- expect(props.kind?.enum, why('registry-version-manifest.schema.json', '`form-content` joins the kind enum (RFC 0137)')).toEqual(
144
- expect.arrayContaining(['node', 'artifact-type', 'connection', 'card', 'form-content']),
145
- );
146
- expect(!!props.templates, why('registry-version-manifest.schema.json', '`templates` payload property declared (additionalProperties:false)')).toBe(true);
147
- });
148
-
149
- it('the `anyOf` payload gate carries a `templates` branch (NECESSARY — enum + property alone are not sufficient)', () => {
150
- const anyOf = (versionManifestSchema.anyOf ?? []) as Array<{ required?: string[] }>;
151
- expect(
152
- anyOf.some((branch) => (branch.required ?? []).includes('templates')),
153
- why('registry-version-manifest.schema.json §anyOf', 'without a `templates` branch every form-content manifest is rejected (RFC 0137 §Proposal 1)'),
154
- ).toBe(true);
155
- });
156
-
157
- it('a published form-content version manifest validates (kind + templates, no runtime)', () => {
158
- const ok = validate({ ...base, kind: 'form-content', templates: [template] });
159
- expect(ok, why('registry-operations.md §Validation flow', 'form-content manifest publishes (RFC 0137)')).toBe(true);
160
- });
161
-
162
- it('a form-content manifest carrying `runtime` is REJECTED (declarative kinds carry no runtime)', () => {
163
- const ok = validate({ ...base, kind: 'form-content', templates: [template], runtime: { language: 'javascript' } });
164
- expect(ok, why('registry-version-manifest.schema.json §allOf', 'a declarative kind MUST NOT carry runtime')).toBe(false);
165
- });
166
-
167
- it('an unchanged node version manifest still validates (RFC 0137 is additive)', () => {
168
- const ok = validate({
169
- ...base,
170
- runtime: { language: 'javascript' },
171
- nodes: [{ typeId: 'core.openwop.x.n', version: '1.0.0', category: 'data', role: 'pure' }],
172
- });
173
- expect(ok, why('COMPATIBILITY.md §2.1', 'RFC 0137 is additive — node manifests validate unchanged')).toBe(true);
174
- });
175
- });
176
-
177
- describe('form-content-packs: source manifest contract (RFC 0137, server-free)', () => {
178
- const ajv = new Ajv2020({ allErrors: true, strict: false });
179
- addFormats(ajv);
180
- const sourceSchema = readSchema('form-content-pack-manifest.schema.json');
181
- const validate = ajv.compile(sourceSchema);
182
-
183
- const pack = (templates: unknown[]): Record<string, unknown> => ({
184
- name: 'core.openwop.forms.starters',
185
- version: '1.0.0',
186
- kind: 'form-content',
187
- engines: { openwop: '>=1.1.0 <2.0.0' },
188
- templates,
189
- });
190
- const field = (over: Record<string, unknown> = {}): Record<string, unknown> => ({
191
- id: 'guestName',
192
- type: 'text',
193
- label: 'Your name',
194
- ...over,
195
- });
196
- const template = (over: Record<string, unknown> = {}): Record<string, unknown> => ({
197
- templateId: 'core.openwop.form.rsvp',
198
- version: '1.0.0',
199
- label: 'RSVP',
200
- title: 'Will you be joining us?',
201
- fields: [field()],
202
- ...over,
203
- });
204
-
205
- it('schema discipline: draft 2020-12, canonical $id, closed objects', () => {
206
- expect(sourceSchema.$schema, why('CONTRIBUTING.md §JSON Schemas', 'draft 2020-12')).toBe(
207
- 'https://json-schema.org/draft/2020-12/schema',
208
- );
209
- expect(sourceSchema.$id, why('CONTRIBUTING.md §JSON Schemas', 'canonical $id URL')).toBe(
210
- 'https://openwop.dev/spec/v1/form-content-pack-manifest.schema.json',
211
- );
212
- expect(sourceSchema.additionalProperties, why('CONTRIBUTING.md §JSON Schemas', 'additionalProperties:false')).toBe(false);
213
- });
214
-
215
- it('a well-formed form-content pack validates', () => {
216
- expect(validate(pack([template()])), why('form-content-packs.md §Manifest format', 'the canonical example validates')).toBe(true);
217
- });
218
-
219
- it('the full portable field-type subset is accepted', () => {
220
- for (const type of ['text', 'longtext', 'number', 'boolean', 'select', 'multiselect', 'file', 'artifact-ref']) {
221
- expect(
222
- validate(pack([template({ fields: [field({ type })] })])),
223
- why('chat-card-packs.md §Input fields', `portable type \`${type}\` is accepted (RFC 0071 G9 subset)`),
224
- ).toBe(true);
225
- }
226
- });
227
-
228
- it('vendor.* and x- host extensions are accepted (other hosts degrade to plain text)', () => {
229
- for (const type of ['vendor.myndhyve.color', 'x-signature-pad']) {
230
- expect(
231
- validate(pack([template({ fields: [field({ type })] })])),
232
- why('form-content-packs.md §Field types', `host extension \`${type}\` is accepted`),
233
- ).toBe(true);
234
- }
235
- });
236
-
237
- it('`email` and `textarea` are REJECTED as field types (they are a format and a widget)', () => {
238
- expect(
239
- validate(pack([template({ fields: [field({ type: 'email' })] })])),
240
- why('form-content-packs.md §Validation formats are not types', '`email` is a format constraint, not a data kind — use text + format'),
241
- ).toBe(false);
242
- expect(
243
- validate(pack([template({ fields: [field({ type: 'textarea' })] })])),
244
- why('chat-card-packs.md §Input fields', '`textarea` is a widget name — the portable data kind is `longtext`'),
245
- ).toBe(false);
246
- });
247
-
248
- it('`format: "email"` on a text field IS accepted (the supported spelling)', () => {
249
- expect(
250
- validate(pack([template({ fields: [field({ id: 'email', type: 'text', format: 'email' })] })])),
251
- why('form-content-packs.md §Validation formats are not types', 'email validation rides `format`, not `type`'),
252
- ).toBe(true);
253
- });
254
-
255
- it('a field using `key` instead of `id` is REJECTED (aligns with chat-card InputField.id)', () => {
256
- const bad = { key: 'guestName', type: 'text', label: 'Your name' };
257
- expect(
258
- validate(pack([template({ fields: [bad] })])),
259
- why('form-content-packs.md §Manifest format', 'the field identifier is `id`, not `key`'),
260
- ).toBe(false);
261
- });
262
-
263
- it('an integer `templates[].version` is REJECTED (SemVer axis, not the integer schemaVersion axis)', () => {
264
- expect(
265
- validate(pack([template({ version: 3 })])),
266
- why('form-content-packs.md §Manifest format', '`templates[].version` is SemVer 2.0.0'),
267
- ).toBe(false);
268
- });
269
-
270
- it('an empty `fields[]` and an empty `templates[]` are REJECTED', () => {
271
- expect(validate(pack([template({ fields: [] })])), why('form-content-pack-manifest.schema.json', 'a template MUST declare ≥1 field')).toBe(false);
272
- expect(validate(pack([])), why('form-content-pack-manifest.schema.json', 'a pack MUST declare ≥1 template')).toBe(false);
273
- });
274
-
275
- it('a `runtime` block is REJECTED at the source manifest too (the kind is inert)', () => {
276
- expect(
277
- validate({ ...pack([template()]), runtime: { language: 'javascript' } }),
278
- why('form-content-packs.md §Pack kind', 'a form-content pack carries no runtime'),
279
- ).toBe(false);
280
- });
281
- });
282
-
283
- describe('form-content-packs: a template carries NO submission routing (RFC 0137 §F2, invariant)', () => {
284
- const ajv = new Ajv2020({ allErrors: true, strict: false });
285
- addFormats(ajv);
286
- const sourceSchema = readSchema('form-content-pack-manifest.schema.json') as {
287
- $defs: { FormTemplate: { properties: Record<string, unknown>; additionalProperties?: boolean } };
288
- };
289
- const validate = ajv.compile(sourceSchema);
290
- const formDoc = V1_DIR ? readFileSync(join(V1_DIR, 'form-content-packs.md'), 'utf8') : '';
291
-
292
- const withTemplateKey = (key: string, value: unknown): Record<string, unknown> => ({
293
- name: 'core.openwop.forms.starters',
294
- version: '1.0.0',
295
- kind: 'form-content',
296
- engines: { openwop: '>=1.1.0 <2.0.0' },
297
- templates: [
298
- {
299
- templateId: 'core.openwop.form.rsvp',
300
- version: '1.0.0',
301
- label: 'RSVP',
302
- title: 'RSVP',
303
- fields: [{ id: 'a', type: 'text', label: 'A' }],
304
- [key]: value,
305
- },
306
- ],
307
- });
308
-
309
- it('FormTemplate declares NO routing-shaped property and is closed', () => {
310
- const props = Object.keys(sourceSchema.$defs.FormTemplate.properties);
311
- for (const banned of ['intakeBinding', 'destination', 'webhook', 'webhookUrl', 'listId', 'mailbox', 'crmObject', 'routing', 'submitTo']) {
312
- expect(props, why('form-content-packs.md §No submission routing', `FormTemplate MUST NOT declare a \`${banned}\` property`)).not.toContain(banned);
313
- }
314
- expect(
315
- sourceSchema.$defs.FormTemplate.additionalProperties,
316
- why('form-content-packs.md §No submission routing', 'FormTemplate is closed, so an undeclared routing key is rejected'),
317
- ).toBe(false);
318
- });
319
-
320
- it('a template attempting to carry routing config is REJECTED', () => {
321
- for (const [key, value] of [
322
- ['intakeBinding', { listId: 'abc' }],
323
- ['destination', 'https://attacker.example/collect'],
324
- ['webhookUrl', 'https://attacker.example/hook'],
325
- ] as Array<[string, unknown]>) {
326
- expect(
327
- validate(withTemplateKey(key, value)),
328
- why('form-content-packs.md §No submission routing', `a pack MUST NOT bind a submission destination via \`${key}\``),
329
- ).toBe(false);
330
- }
331
- });
332
-
333
- it.skipIf(V1_DIR === null)('the spec scopes the routing ban to the PACK, leaving operator-configured routing free', () => {
334
- expect(
335
- /This constrains the pack, not the host/i.test(formDoc),
336
- why('form-content-packs.md §No submission routing', 'a host MAY route wherever its OPERATOR configures; only pack-declared routing is banned'),
337
- ).toBe(true);
338
- });
339
-
340
- it.skipIf(V1_DIR === null)('the spec binds FUTURE routing surfaces to operator consent, not pack declaration', () => {
341
- expect(
342
- /MUST NOT let a pack bind a destination unilaterally|routing MUST be a host-side decision/i.test(formDoc),
343
- why('form-content-packs.md §No submission routing', 'a later routing RFC MUST keep the decision host-side, behind operator consent'),
344
- ).toBe(true);
345
- });
346
- });
347
-
348
- describe('form-content-packs: identifier uniqueness + resource bounds (RFC 0137 amendment)', () => {
349
- const formDoc = V1_DIR ? readFileSync(join(V1_DIR, 'form-content-packs.md'), 'utf8') : '';
350
- const sourceSchema = readSchema('form-content-pack-manifest.schema.json') as {
351
- properties: { templates: { maxItems: number } };
352
- $defs: {
353
- FormTemplate: { properties: { fields: { maxItems: number } } };
354
- FormField: { properties: { label: { maxLength: number }; options: { maxItems: number } } };
355
- };
356
- };
357
-
358
- it.skipIf(V1_DIR === null)('duplicate `fields[].id` is a normative refusal, framed as data integrity', () => {
359
- expect(
360
- /each `fields\[\]\.id` MUST be unique within its template/i.test(formDoc),
361
- why('form-content-packs.md §Unique identifiers', 'duplicate field ids MUST be refused'),
362
- ).toBe(true);
363
- expect(
364
- /silently overwrite/i.test(formDoc),
365
- why('form-content-packs.md §Unique identifiers', 'the rationale is silent data loss, not style'),
366
- ).toBe(true);
367
- });
368
-
369
- it('resource bounds admit real-world content (long consent labels, a country list)', () => {
370
- expect(
371
- sourceSchema.$defs.FormField.properties.label.maxLength,
372
- why('form-content-pack-manifest.schema.json', 'a lawful consent label is legitimately long-form'),
373
- ).toBeGreaterThanOrEqual(1000);
374
- expect(
375
- sourceSchema.$defs.FormField.properties.options.maxItems,
376
- why('form-content-pack-manifest.schema.json', 'a country list is ~195 entries — the cap must clear it'),
377
- ).toBeGreaterThanOrEqual(250);
378
- });
379
-
380
- it('outer resource caps exist on both arrays (render-bomb guard, not product policy)', () => {
381
- expect(sourceSchema.properties.templates.maxItems, why('form-content-pack-manifest.schema.json', 'templates[] carries an outer cap')).toBeGreaterThan(0);
382
- expect(sourceSchema.$defs.FormTemplate.properties.fields.maxItems, why('form-content-pack-manifest.schema.json', 'fields[] carries an outer cap')).toBeGreaterThan(0);
383
- });
384
- });
385
-
386
- describe('form-content-packs: the field vocabulary is SHARED with chat-card packs, not forked (RFC 0137 R2)', () => {
387
- const formSchema = readSchema('form-content-pack-manifest.schema.json') as {
388
- $defs: { FormField: { properties: { type: { pattern: string } } } };
389
- };
390
- const cardSchema = readSchema('chat-card-pack-manifest.schema.json') as {
391
- $defs: { InputField: { properties: { type: { pattern: string } } } };
392
- };
393
-
394
- it('`fields[].type` and `inputs[].type` share one byte-identical pattern', () => {
395
- const formPattern = formSchema.$defs.FormField.properties.type.pattern;
396
- const cardPattern = cardSchema.$defs.InputField.properties.type.pattern;
397
- expect(
398
- formPattern,
399
- why(
400
- 'form-content-packs.md §Field types',
401
- 'RFC 0137 reuses the RFC 0071 portable subset VERBATIM — two input-collecting declarative kinds MUST agree on what a field type means. If this fails, one kind\'s vocabulary was widened without the other and the wire contract has forked.',
402
- ),
403
- ).toBe(cardPattern);
404
- });
405
-
406
- it('the shared pattern still admits every portable data kind and both extension prefixes', () => {
407
- const pattern = new RegExp(formSchema.$defs.FormField.properties.type.pattern);
408
- for (const type of ['text', 'longtext', 'number', 'boolean', 'select', 'multiselect', 'file', 'artifact-ref']) {
409
- expect(pattern.test(type), why('chat-card-packs.md §Input fields', `\`${type}\` is in the portable subset`)).toBe(true);
410
- }
411
- expect(pattern.test('vendor.acme.rating'), why('chat-card-packs.md §Input fields', 'vendor.<org>.<kind> extensions are admitted')).toBe(true);
412
- expect(pattern.test('x-rating'), why('chat-card-packs.md §Input fields', 'x-<kind> extensions are admitted')).toBe(true);
413
- expect(pattern.test('textarea'), why('chat-card-packs.md §Input fields', 'widget names are NOT in the subset')).toBe(false);
414
- });
415
- });
@@ -1,175 +0,0 @@
1
- /**
2
- * RFC 0150 §D — record reconciliation and effect authorization are separate
3
- * claims, and the capability vocabulary must not conflate them.
4
- *
5
- * Two defects, both in the corpus rather than in any host.
6
- *
7
- * 1. `crossRegion` read as a safety ladder while meaning something else at the
8
- * top. `schemas/capabilities.schema.json` documented `strict` as
9
- * "cross-region read-visibility is bounded by
10
- * `multiRegion.replicationLagBoundMs`" — a LATENCY claim. A host can
11
- * replicate synchronously at 0 ms and still issue duplicate external effects
12
- * from two regions, because knowing what the other region wrote is not the
13
- * same as being authorized to act. `strict` therefore sat at the top of an
14
- * enum implementers read as an effect-safety claim while promising nothing
15
- * about effects. Its latency content already had its own field, so it is
16
- * removed rather than renamed: `fenced-effects` is a genuinely stronger
17
- * property, and promoting existing `strict` advertisements into it by rename
18
- * would assert evidence no host has produced.
19
- *
20
- * 2. `partitionRecoveryStrategy` offering rules the annex forbids.
21
- * `spec/v1/idempotency.md` §"Guarantees under partition" MUSTs lex-min(runId)
22
- * convergence, "deterministic without coordination", and the `multiRegion`
23
- * block MUSTs that "re-running the same conflict input MUST produce the same
24
- * survivor". Both `last-writer-wins` and `first-writer-wins` are time-ordered:
25
- * under a partition there is no shared clock, so both regions believe they
26
- * wrote last (or first), and neither can produce a reproducible survivor.
27
- * They also select a different survivor than the lex-min rule the same
28
- * document requires. The schema was advertising strategies that violate two
29
- * MUSTs already in force. RFC 0150 §D names only `last-writer-wins`; leaving
30
- * `first-writer-wins` would keep the identical defect under a different label.
31
- *
32
- * This gate reads the schema and the prose. It observes no host, and says
33
- * nothing about whether any engine fences correctly — that is gap G10.
34
- *
35
- * `spec/v1/` ships in the repository and NOT in the published tarball, so the
36
- * prose legs self-skip under the published layout; the schema ships in both.
37
- */
38
-
39
- import { describe, it, expect } from 'vitest';
40
- import { readFileSync } from 'node:fs';
41
- import { join, resolve as pathResolve } from 'node:path';
42
- import { V1_DIR } from '../lib/paths.js';
43
-
44
- const SCHEMA_PATH =
45
- V1_DIR === null ? null : pathResolve(V1_DIR, '..', '..', 'schemas', 'capabilities.schema.json');
46
-
47
- interface EnumLike {
48
- readonly enum?: readonly string[];
49
- readonly anyOf?: readonly EnumLike[];
50
- readonly properties?: Record<string, EnumLike>;
51
- }
52
-
53
- function idempotencyCaps(): EnumLike | null {
54
- if (SCHEMA_PATH === null) return null;
55
- const schema = JSON.parse(readFileSync(SCHEMA_PATH, 'utf8')) as {
56
- properties?: Record<string, EnumLike>;
57
- };
58
- return schema.properties?.['idempotency'] ?? null;
59
- }
60
-
61
- /** Every literal in an `enum`, including those nested under `anyOf`. */
62
- function literals(node: EnumLike | undefined): string[] {
63
- if (node === undefined) return [];
64
- return [...(node.enum ?? []), ...(node.anyOf ?? []).flatMap(literals)];
65
- }
66
-
67
- describe.skipIf(V1_DIR === null)('RFC 0150 §D — multi-region effect vocabulary', () => {
68
- const idem = idempotencyCaps();
69
- const doc = V1_DIR === null ? '' : readFileSync(join(V1_DIR as string, 'idempotency.md'), 'utf8');
70
-
71
- it('the idempotency capability family is found at all', () => {
72
- // Guard: a lookup that silently returned null would make every leg below
73
- // vacuously true — the failure RFC 0148 exists to close.
74
- expect(idem, 'capabilities.schema.json MUST declare an `idempotency` family').not.toBeNull();
75
- expect(
76
- literals(idem?.properties?.['crossRegion']).length,
77
- 'the `crossRegion` enum MUST be non-empty',
78
- ).toBeGreaterThan(0);
79
- });
80
-
81
- it('crossRegion offers the three RFC 0150 §D postures', () => {
82
- const values = literals(idem?.properties?.['crossRegion']);
83
- expect(
84
- [...values].sort(),
85
- 'RFC 0150 §D: the canonical postures are `single-region` (no cross-region guarantee), ' +
86
- '`reconciled-records` (records converge, effects may remain at-least-once), and ' +
87
- '`fenced-effects` (records converge AND every effect is fenced or provider-idempotent).',
88
- ).toEqual(['fenced-effects', 'reconciled-records', 'single-region']);
89
- });
90
-
91
- it('crossRegion does not carry `strict`, which was a latency claim in a safety slot', () => {
92
- const values = literals(idem?.properties?.['crossRegion']);
93
- expect(
94
- values.includes('strict'),
95
- 'RFC 0150 §D: `strict` promised bounded read-visibility, not effect authorization — a host ' +
96
- 'replicating at 0 ms can still issue duplicate effects from two regions. Its latency ' +
97
- 'content belongs to `multiRegion.replicationLagBoundMs`, which already carries it.',
98
- ).toBe(false);
99
- });
100
-
101
- it('partitionRecoveryStrategy offers no time-ordered rule', () => {
102
- const values = literals(idem?.properties?.['multiRegion']?.properties?.['partitionRecoveryStrategy']);
103
- const timeOrdered = values.filter((v) => v === 'last-writer-wins' || v === 'first-writer-wins');
104
- expect(
105
- timeOrdered,
106
- 'RFC 0150 §D: under a partition there is no shared clock, so a time-ordered rule cannot ' +
107
- 'satisfy the annex MUST that "re-running the same conflict input MUST produce the same ' +
108
- 'survivor", and it selects a different survivor than the lex-min(runId) rule the same ' +
109
- 'document requires. Removing only `last-writer-wins` leaves the identical defect under ' +
110
- `a different label. Found: ${timeOrdered.join(', ')}`,
111
- ).toEqual([]);
112
- });
113
-
114
- it('partitionRecoveryStrategy names the rule the annex actually requires', () => {
115
- const values = literals(idem?.properties?.['multiRegion']?.properties?.['partitionRecoveryStrategy']);
116
- expect(
117
- values,
118
- 'the annex MUSTs lex-min(runId) convergence, so that rule MUST be nameable in the ' +
119
- 'advertisement rather than reachable only through a vendor `x-host-*` extension.',
120
- ).toContain('lexicographic-min-run-id');
121
- });
122
-
123
- it('the spec states that reconciliation does not authorize effects', () => {
124
- const plain = doc.replace(/[`*_]/g, '').replace(/\s+/g, ' ');
125
- expect(
126
- /MUST NOT authorize effects/.test(plain),
127
- 'RFC 0150 §D: run-record reconciliation and permission to issue effects are separate. ' +
128
- 'Lexicographic run-ID reconciliation MAY select a surviving record but MUST NOT ' +
129
- 'authorize an external effect — the sentence the whole section rests on.',
130
- ).toBe(true);
131
- });
132
-
133
- it('a host that can neither fence nor rely on provider dedup must say so', () => {
134
- const plain = doc.replace(/[`*_]/g, '').replace(/\s+/g, ' ');
135
- expect(
136
- /at-least-once-risk/.test(plain),
137
- 'RFC 0150 §D: absent a fencing token or a provider guaranteeing duplicate suppression, a ' +
138
- 'host MUST NOT claim strict multi-region effect safety and MUST classify the effect as ' +
139
- '`at-least-once-risk`. An unclassifiable risk is the one operators cannot plan around.',
140
- ).toBe(true);
141
- });
142
-
143
- it('the capability advertisement example parses as JSON', () => {
144
- // RFC 0149 §B found this and deliberately left it: the annex's example is
145
- // fenced as ```json while containing `"single-region" | "best-effort" |
146
- // "strict"`, which no parser accepts. An example that cannot parse is
147
- // exactly what RFC 0150 §D's extraction requirement exists to catch.
148
- const lines = doc.split('\n');
149
- const blocks: { line: number; body: string }[] = [];
150
- for (let i = 0; i < lines.length; i++) {
151
- if (lines[i]!.trim() !== '```json') continue;
152
- const body: string[] = [];
153
- let j = i + 1;
154
- while (j < lines.length && lines[j]!.trim() !== '```') body.push(lines[j++]!);
155
- blocks.push({ line: i + 2, body: body.join('\n') });
156
- i = j;
157
- }
158
- expect(blocks.length, 'idempotency.md MUST contain fenced json examples to check').toBeGreaterThan(0);
159
- const unparseable = blocks
160
- .filter((b) => {
161
- try {
162
- JSON.parse(b.body);
163
- return false;
164
- } catch {
165
- return true;
166
- }
167
- })
168
- .map((b) => `spec/v1/idempotency.md:${b.line}`);
169
- expect(
170
- unparseable,
171
- 'a block fenced as ```json MUST parse as JSON. Union-type notation belongs in prose or a ' +
172
- '```text fence.\n ' + unparseable.join('\n '),
173
- ).toEqual([]);
174
- });
175
- });