@openwop/openwop-conformance 2.45.14 → 2.45.15

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 (43) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +2 -2
  3. package/dist/spec-artifacts.lock.json +2 -2
  4. package/package.json +2 -2
  5. package/requirements.json +1391 -4
  6. package/scenario-majors.json +65 -2
  7. package/schemas/CORPUS-STAMP.json +9 -9
  8. package/src/lib/backpressure-witness.ts +2 -2
  9. package/src/lib/budget-witness.ts +3 -3
  10. package/src/lib/envelope-schema-static.ts +166 -0
  11. package/src/lib/exhaustion-facet-witness.ts +1 -1
  12. package/src/lib/family-advert-witness.ts +212 -0
  13. package/src/lib/i18n-double.ts +99 -0
  14. package/src/lib/i18n-negotiation-witness.ts +173 -0
  15. package/src/lib/localized-content-witness.ts +49 -0
  16. package/src/lib/model-capability-insufficient-witness.ts +121 -0
  17. package/src/lib/prompt-double.ts +217 -0
  18. package/src/lib/prompt-events-witness.ts +211 -0
  19. package/src/lib/prompt-library-witness.ts +406 -0
  20. package/src/lib/run-bounds-witness.ts +111 -0
  21. package/src/lib/scratch-host.ts +4 -1
  22. package/src/lib/table-schema-witness.ts +3 -1
  23. package/src/scenarios/v2-ai-envelope-shape-static.test.ts +93 -0
  24. package/src/scenarios/v2-ai-providers-advertisement.test.ts +94 -0
  25. package/src/scenarios/v2-envelope-reasoning-shape.test.ts +81 -0
  26. package/src/scenarios/v2-envelope-tier-one-subset-static.test.ts +77 -0
  27. package/src/scenarios/v2-envelope-variant-discriminator-static.test.ts +57 -0
  28. package/src/scenarios/v2-envelopes-advertisement.test.ts +85 -0
  29. package/src/scenarios/v2-i18n-negotiation.test.ts +91 -0
  30. package/src/scenarios/v2-limits-envelope-caps-advertisement.test.ts +61 -0
  31. package/src/scenarios/v2-localized-content-delivery.test.ts +136 -0
  32. package/src/scenarios/v2-media-inline-cap-static.test.ts +59 -0
  33. package/src/scenarios/v2-model-capabilities-advertisement.test.ts +61 -0
  34. package/src/scenarios/v2-model-capability-insufficient.test.ts +63 -0
  35. package/src/scenarios/v2-prompt-all-four-kinds-events.test.ts +69 -0
  36. package/src/scenarios/v2-prompt-end-to-end-events.test.ts +71 -0
  37. package/src/scenarios/v2-prompt-list-and-fetch.test.ts +103 -0
  38. package/src/scenarios/v2-prompt-mutable-lifecycle.test.ts +130 -0
  39. package/src/scenarios/v2-prompt-pack-install.test.ts +78 -0
  40. package/src/scenarios/v2-prompt-render-deterministic.test.ts +87 -0
  41. package/src/scenarios/v2-prompt-resolution-chain-event.test.ts +46 -0
  42. package/src/scenarios/v2-prompt-template-shape.test.ts +112 -0
  43. package/src/scenarios/v2-run-execution-bounds.test.ts +67 -0
@@ -0,0 +1,93 @@
1
+ /**
2
+ * v2 — the AI envelope wire shape, statically (`spec/v2/core/events.md`
3
+ * §"AI envelopes: E1–E5"; `schemas/v2/ai-envelope.schema.json`). The v1 twin is
4
+ * the static half of `ai-envelope-shape`; its live halves (the
5
+ * `envelope/accept` seam legs) are seam-only and not ported.
6
+ *
7
+ * compile `ai-envelope.schema.json` and each universal-kind payload
8
+ * schema compile under Ajv 2020 with the v2 tree registered;
9
+ * positive a well-formed envelope validates — with `correlationId` and
10
+ * `meta.source`, which v2 makes REQUIRED (migration C4.16);
11
+ * meta an envelope without `meta` is rejected;
12
+ * closed an unknown top-level property is rejected;
13
+ * correlation NEW at v2: an envelope without `correlationId` is rejected
14
+ * ("`correlationId` and `meta.source` are REQUIRED on every
15
+ * envelope … nothing is synthesized");
16
+ * source NEW at v2: an envelope whose `meta` has no `source` is rejected.
17
+ *
18
+ * The `<org>.` namespacing rule for kinds is not witnessed here: the schema's
19
+ * `type` is an open string, so the rule binds the engine's acceptance, not the
20
+ * wire shape.
21
+ *
22
+ * Host-free: every leg runs with no OPENWOP_BASE_URL.
23
+ *
24
+ * @see spec/v2/core/events.md §"AI envelopes: E1–E5"
25
+ * @see spec/v1/migrations.json openwop.migration.C4.16
26
+ */
27
+
28
+ import { describe, it, expect } from 'vitest';
29
+ import { v2Validator } from '../lib/v2.js';
30
+ import { req } from '../lib/requirement-ids.js';
31
+ import { UNIVERSAL_KINDS } from '../lib/envelope-schema-static.js';
32
+
33
+ const DOC = 'events.md §AI envelopes: E1–E5';
34
+ const ID_COMPILE = 'openwop.requirement.ai-envelope.schemas-compile';
35
+ const ID_POSITIVE = 'openwop.requirement.ai-envelope.well-formed-accepted';
36
+ const ID_META = 'openwop.requirement.ai-envelope.meta-required';
37
+ const ID_CLOSED = 'openwop.requirement.ai-envelope.top-level-closed';
38
+ const ID_CORRELATION = 'openwop.requirement.ai-envelope.correlation-id-required';
39
+ const ID_SOURCE = 'openwop.requirement.ai-envelope.meta-source-required';
40
+
41
+ const WELL_FORMED = {
42
+ type: 'clarification.request',
43
+ schemaVersion: 1,
44
+ envelopeId: 'env-v2-positive-1',
45
+ correlationId: 'run-1:node-2:turn-0:abc123',
46
+ payload: { questions: [{ id: 'q1', question: 'Which provider?' }] },
47
+ meta: { source: 'ai-generation', ts: '2026-10-04T10:00:00Z', contentTrust: 'trusted' },
48
+ } as const;
49
+
50
+ /** The well-formed envelope with one top-level field removed. */
51
+ function without(field: string): Record<string, unknown> {
52
+ const e: Record<string, unknown> = { ...WELL_FORMED };
53
+ delete e[field];
54
+ return e;
55
+ }
56
+
57
+ describe('v2 AI envelope shape: schemas compile', () => {
58
+ it('ai-envelope.schema.json compiles under Ajv 2020', () => {
59
+ expect(() => v2Validator('ai-envelope'), req(ID_COMPILE, DOC, 'schemas/v2/ai-envelope.schema.json MUST compile')).not.toThrow();
60
+ });
61
+
62
+ for (const kind of UNIVERSAL_KINDS) {
63
+ it(`envelopes/${kind}.schema.json compiles under Ajv 2020`, () => {
64
+ expect(() => v2Validator(`envelopes/${kind}`), req(ID_COMPILE, DOC, `schemas/v2/envelopes/${kind}.schema.json MUST compile`)).not.toThrow();
65
+ });
66
+ }
67
+ });
68
+
69
+ describe('v2 AI envelope shape: round-trip', () => {
70
+ const validate = v2Validator('ai-envelope');
71
+
72
+ it('accepts a well-formed envelope carrying correlationId and meta.source', () => {
73
+ const r = validate(WELL_FORMED);
74
+ expect(r.ok, req(ID_POSITIVE, DOC, `a well-formed envelope MUST validate (${r.errors})`)).toBe(true);
75
+ });
76
+
77
+ it('rejects an envelope missing meta', () => {
78
+ expect(validate(without('meta')).ok, req(ID_META, DOC, 'an envelope without meta MUST be rejected')).toBe(false);
79
+ });
80
+
81
+ it('rejects an envelope with an unknown top-level property', () => {
82
+ expect(validate({ ...WELL_FORMED, unknownTopLevel: 'x' }).ok, req(ID_CLOSED, 'schemas/v2/ai-envelope.schema.json', 'the envelope is closed: an unknown top-level property MUST be rejected')).toBe(false);
83
+ });
84
+
85
+ it('rejects an envelope missing correlationId', () => {
86
+ expect(validate(without('correlationId')).ok, req(ID_CORRELATION, DOC, 'correlationId is REQUIRED on every envelope at v2 — an envelope without it MUST be rejected, never synthesized')).toBe(false);
87
+ });
88
+
89
+ it('rejects an envelope whose meta carries no source', () => {
90
+ const { source: _source, ...meta } = WELL_FORMED.meta;
91
+ expect(validate({ ...WELL_FORMED, meta }).ok, req(ID_SOURCE, DOC, 'meta.source is REQUIRED on every envelope at v2 — an envelope without it MUST be rejected')).toBe(false);
92
+ });
93
+ });
@@ -0,0 +1,94 @@
1
+ /**
2
+ * v2 — the `aiProviders` advertisement facets that survive at major 2
3
+ * (`spec/v2/core/host-services.md` §aiProviders; migration C2.11 narrowed the
4
+ * v1 facets). The v1 twins are the advertisement halves of
5
+ * `aiproviders-selfhosted-shape`, `byok-auth-modes` and `media-url-inline-cap`;
6
+ * the legs live in `lib/family-advert-witness.ts`.
7
+ *
8
+ * record the record validates against its seat in
9
+ * `schemas/v2/capabilities.schema.json`;
10
+ * selfHosted one boolean (the v1 `selfHosted[]` id list retired);
11
+ * authModes shape one flat list (the v1 per-provider map retired);
12
+ * (not a leg) authModes vocabulary: the prose names `apiKey`, `oauth-pkce`,
13
+ * `oauth-device`, `none`, `subscription`, but its only
14
+ * MUSTs bind clients ("a client MUST ignore an unknown
15
+ * mode"), and the v2 schema seat is `string[]` with no
16
+ * enum. No host-side MUST exists, so a misspelt mode is an
17
+ * interop defect the host should fix, not a failing row.
18
+ * Restoring the v1 enum would need an RFC.
19
+ * maxInlineMediaBytes an integer ≥ 1;
20
+ * promptPrefixCache one boolean (the v1 `{ supported, providers[] }` retired).
21
+ *
22
+ * The v1 rules C2.11 retired (`selfHosted[]` ⊆ `supported[]`, the per-provider
23
+ * `authModes` keys/byok/`["none"]` rules) are not ported: they have no v2 seat.
24
+ *
25
+ * Dispositions: discovery unreadable ⇒ `blocked`; `aiProviders` absent, or the
26
+ * facet a leg reads absent ⇒ `inapplicable`.
27
+ *
28
+ * @see spec/v2/core/host-services.md §aiProviders
29
+ * @see spec/v1/migrations.json openwop.migration.C2.11
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { v2Discovery } from '../lib/v2.js';
34
+ import { softSkip } from '../lib/soft-skip.js';
35
+ import { req } from '../lib/requirement-ids.js';
36
+ import { majorProfile } from '../lib/major-profile.js';
37
+ import {
38
+ aiProvidersAuthModesShapeLeg, aiProvidersInlineMediaLeg, aiProvidersPromptPrefixCacheLeg, aiProvidersSelfHostedLeg,
39
+ recordSchemaLeg,
40
+ } from '../lib/family-advert-witness.js';
41
+
42
+ const PROFILE = majorProfile(2);
43
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
44
+ const ID_RECORD = 'openwop.requirement.ai-providers.advert-record-schema';
45
+ const ID_SELF_HOSTED = 'openwop.requirement.ai-providers.self-hosted-boolean';
46
+ const ID_AUTH_SHAPE = 'openwop.requirement.ai-providers.auth-modes-flat-list';
47
+ const ID_INLINE_MEDIA = 'openwop.requirement.ai-providers.max-inline-media-bytes';
48
+ const ID_PREFIX_CACHE = 'openwop.requirement.ai-providers.prompt-prefix-cache-boolean';
49
+
50
+ async function discovery(): Promise<Record<string, unknown> | null> {
51
+ try { return await v2Discovery(); } catch { return null; }
52
+ }
53
+
54
+ describe('v2 aiProviders advertisement (host-services.md §aiProviders)', () => {
55
+ it('the aiProviders record validates against its capabilities-schema seat', async () => {
56
+ const doc = await discovery();
57
+ if (!doc) return softSkip('blocked', UNREADABLE);
58
+ const out = recordSchemaLeg(PROFILE, doc, 'aiProviders', 'host-services.md §aiProviders');
59
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
60
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
61
+ });
62
+
63
+ it('aiProviders.selfHosted is one boolean', async () => {
64
+ const doc = await discovery();
65
+ if (!doc) return softSkip('blocked', UNREADABLE);
66
+ const out = aiProvidersSelfHostedLeg(PROFILE, doc);
67
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
68
+ for (const x of out.findings) expect(x.ok, req(ID_SELF_HOSTED, x.doc, x.message)).toBe(true);
69
+ });
70
+
71
+ it('aiProviders.authModes is one flat list of modes', async () => {
72
+ const doc = await discovery();
73
+ if (!doc) return softSkip('blocked', UNREADABLE);
74
+ const out = aiProvidersAuthModesShapeLeg(PROFILE, doc);
75
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
76
+ for (const x of out.findings) expect(x.ok, req(ID_AUTH_SHAPE, x.doc, x.message)).toBe(true);
77
+ });
78
+
79
+ it('aiProviders.maxInlineMediaBytes is an integer of at least 1', async () => {
80
+ const doc = await discovery();
81
+ if (!doc) return softSkip('blocked', UNREADABLE);
82
+ const out = aiProvidersInlineMediaLeg(PROFILE, doc);
83
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
84
+ for (const x of out.findings) expect(x.ok, req(ID_INLINE_MEDIA, x.doc, x.message)).toBe(true);
85
+ });
86
+
87
+ it('aiProviders.promptPrefixCache is one boolean', async () => {
88
+ const doc = await discovery();
89
+ if (!doc) return softSkip('blocked', UNREADABLE);
90
+ const out = aiProvidersPromptPrefixCacheLeg(PROFILE, doc);
91
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
92
+ for (const x of out.findings) expect(x.ok, req(ID_PREFIX_CACHE, x.doc, x.message)).toBe(true);
93
+ });
94
+ });
@@ -0,0 +1,81 @@
1
+ /**
2
+ * v2 — the optional `reasoning` payload field (`spec/v2/core/events.md`
3
+ * §`envelopes`; RFC 0030 §A). Static: reads `schemas/v2/envelopes/` only. The
4
+ * v1 twin is `envelope-reasoning-shape`; its two advertisement legs
5
+ * (`envelopes.reasoning`, `envelopes.tierOneSubsetCompliance`) are ported to
6
+ * `v2-envelopes-advertisement`, not repeated here.
7
+ *
8
+ * declared `clarification.request`, `schema.request` and `error` declare an
9
+ * OPTIONAL `reasoning: string` ("A vendor kind whose payload needs
10
+ * multi-step reasoning SHOULD declare it as optional");
11
+ * omitted `schema.response` (a side-channel ack) does not declare it;
12
+ * absent ok each of the three validates WITHOUT `reasoning` ("A host MUST NOT
13
+ * reject an envelope without it, whatever `promptDirective` says");
14
+ * present ok each validates WITH a string `reasoning`;
15
+ * typed a non-string `reasoning` is rejected.
16
+ *
17
+ * Host-free: every leg runs with no OPENWOP_BASE_URL.
18
+ *
19
+ * @see spec/v2/core/events.md §envelopes
20
+ * @see schemas/v2/envelopes/
21
+ */
22
+
23
+ import { describe, it, expect } from 'vitest';
24
+ import { v2Validator } from '../lib/v2.js';
25
+ import { req } from '../lib/requirement-ids.js';
26
+ import { loadV2EnvelopeSchema } from '../lib/envelope-schema-static.js';
27
+
28
+ const DOC = 'events.md §envelopes';
29
+ const ID_DECLARED = 'openwop.requirement.envelopes.reasoning-declared-optional-string';
30
+ const ID_OMITTED = 'openwop.requirement.envelopes.reasoning-omitted-from-schema-response';
31
+ const ID_ABSENT_OK = 'openwop.requirement.envelopes.reasoning-absent-accepted';
32
+ const ID_PRESENT_OK = 'openwop.requirement.envelopes.reasoning-present-accepted';
33
+ const ID_TYPED = 'openwop.requirement.envelopes.reasoning-non-string-rejected';
34
+
35
+ /** A valid payload per kind, without `reasoning`. */
36
+ const BASE: Readonly<Record<string, Record<string, unknown>>> = {
37
+ 'clarification.request': { questions: [{ id: 'q1', question: 'Which provider?' }] },
38
+ 'schema.request': { envelopeType: 'acme.prd.create' },
39
+ error: { code: 'validation_failed', message: 'Could not match the schema.' },
40
+ };
41
+ const KINDS = Object.keys(BASE);
42
+
43
+ describe('v2 envelope reasoning: payload schemas (events.md §envelopes)', () => {
44
+ for (const kind of KINDS) {
45
+ it(`${kind} declares an optional reasoning of type string`, () => {
46
+ const schema = loadV2EnvelopeSchema(kind);
47
+ const props = (schema?.['properties'] ?? {}) as Record<string, Record<string, unknown>>;
48
+ const required = (schema?.['required'] ?? []) as unknown[];
49
+ expect(schema !== null, req(ID_DECLARED, DOC, `schemas/v2/envelopes/${kind}.schema.json MUST exist`)).toBe(true);
50
+ expect(props['reasoning']?.['type'], req(ID_DECLARED, DOC, `${kind} MUST declare reasoning as type string`)).toBe('string');
51
+ expect(required.includes('reasoning'), req(ID_DECLARED, DOC, `${kind} MUST NOT require reasoning — it is optional`)).toBe(false);
52
+ });
53
+ }
54
+
55
+ it('schema.response does not declare reasoning (a side-channel ack)', () => {
56
+ const schema = loadV2EnvelopeSchema('schema.response');
57
+ const props = (schema?.['properties'] ?? {}) as Record<string, unknown>;
58
+ expect(schema !== null, req(ID_OMITTED, 'RFC 0030 §A', 'schemas/v2/envelopes/schema.response.schema.json MUST exist')).toBe(true);
59
+ expect('reasoning' in props, req(ID_OMITTED, 'RFC 0030 §A', 'schema.response MUST NOT declare reasoning')).toBe(false);
60
+ });
61
+ });
62
+
63
+ describe('v2 envelope reasoning: round-trip (events.md §envelopes)', () => {
64
+ for (const kind of KINDS) {
65
+ const validate = v2Validator(`envelopes/${kind}`);
66
+
67
+ it(`${kind} validates without reasoning`, () => {
68
+ const r = validate(BASE[kind]);
69
+ expect(r.ok, req(ID_ABSENT_OK, DOC, `a ${kind} payload without reasoning MUST validate — a host MUST NOT reject it (${r.errors})`)).toBe(true);
70
+ });
71
+
72
+ it(`${kind} validates with a string reasoning`, () => {
73
+ const r = validate({ reasoning: 'The input named X twice; I asked which one.', ...BASE[kind] });
74
+ expect(r.ok, req(ID_PRESENT_OK, DOC, `a ${kind} payload with a string reasoning MUST validate (${r.errors})`)).toBe(true);
75
+ });
76
+
77
+ it(`${kind} rejects a non-string reasoning`, () => {
78
+ expect(validate({ reasoning: 42, ...BASE[kind] }).ok, req(ID_TYPED, DOC, `${kind}: a numeric reasoning MUST be rejected — reasoning is a plain string`)).toBe(false);
79
+ });
80
+ }
81
+ });
@@ -0,0 +1,77 @@
1
+ /**
2
+ * v2 — the Tier-1 structured-output subset, statically (RFC 0030 §B;
3
+ * `envelopes.tierOneSubsetCompliance` in `schemas/v2/capabilities.schema.json`).
4
+ * The v1 twin is `envelope-tier-one-subset-static`; the walker lives in
5
+ * `lib/envelope-schema-static.ts`.
6
+ *
7
+ * load-bearing ALWAYS ON, host-free: each universal-kind payload schema in
8
+ * `schemas/v2/envelopes/` uses none of the keywords that fail
9
+ * across several Tier-1 vendors (`oneOf`, `allOf`, `not`,
10
+ * `if`/`then`/`else`, `dependencies`, `prefixItems`,
11
+ * `propertyNames`), nests at most 5 deep and has at most 100
12
+ * properties;
13
+ * strict LIVE: a host advertising `tierOneSubsetCompliance: "strict"`
14
+ * has, for every kind it lists in `supportedEnvelopes.kinds`
15
+ * that resolves to a corpus schema, a schema meeting the whole
16
+ * intersection (closed objects, every property required, no
17
+ * string/number/array bounds). Host-served vendor schemas are
18
+ * not fetched, as at v1.
19
+ *
20
+ * The strict rule is stated by the schema seat's description ("`strict`: every
21
+ * host-served envelope schema passes the static subset-compliance check"). The
22
+ * v2 prose (`events.md` §envelopes) states a different, behavioural rule — the
23
+ * host MUST accept an envelope restricted to the subset from any advertised
24
+ * provider — and does not distinguish strict, warn and off.
25
+ *
26
+ * Dispositions (strict leg): discovery unreadable ⇒ `blocked`; `envelopes` not
27
+ * advertised, or `tierOneSubsetCompliance` other than "strict" ⇒
28
+ * `inapplicable`; no advertised kind resolving to a corpus schema ⇒
29
+ * `inapplicable`.
30
+ *
31
+ * @see RFCS/0030-envelope-reasoning-and-tier-one-subset.md §B
32
+ * @see spec/v2/core/events.md §envelopes
33
+ */
34
+
35
+ import { describe, it, expect } from 'vitest';
36
+ import { familyAdvertised, v2Discovery } from '../lib/v2.js';
37
+ import { softSkip } from '../lib/soft-skip.js';
38
+ import { req } from '../lib/requirement-ids.js';
39
+ import { loadV2EnvelopeSchema, tierOneViolations, UNIVERSAL_KINDS, type SchemaViolation } from '../lib/envelope-schema-static.js';
40
+
41
+ const ID_LOAD_BEARING = 'openwop.requirement.envelopes.tier-one-load-bearing-universal-kinds';
42
+ const ID_STRICT = 'openwop.requirement.envelopes.tier-one-strict-advertised-kinds';
43
+
44
+ describe('v2 Tier-1 subset: universal kinds meet the load-bearing rules (always on)', () => {
45
+ for (const kind of UNIVERSAL_KINDS) {
46
+ it(`${kind} uses no keyword that fails across Tier-1 vendors`, () => {
47
+ const schema = loadV2EnvelopeSchema(kind);
48
+ expect(schema !== null, req(ID_LOAD_BEARING, 'RFC 0030 §B', `schemas/v2/envelopes/${kind}.schema.json MUST exist`)).toBe(true);
49
+ const violations = schema === null ? [] : tierOneViolations(schema, 'load-bearing');
50
+ expect(violations, req(ID_LOAD_BEARING, 'RFC 0030 §B', `${kind} load-bearing Tier-1 violations: ${JSON.stringify(violations)}`)).toEqual([]);
51
+ });
52
+ }
53
+ });
54
+
55
+ describe('v2 Tier-1 subset: a strict host\'s advertised kinds meet the whole intersection', () => {
56
+ it('a host advertising tierOneSubsetCompliance strict has advertised kinds whose corpus schemas satisfy the Tier-1 intersection', async () => {
57
+ let doc: Record<string, unknown> | null;
58
+ try { doc = await v2Discovery(); } catch { doc = null; }
59
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
60
+ const envelopes = await familyAdvertised('envelopes');
61
+ if (envelopes === null) return softSkip('inapplicable', 'the host does not advertise envelopes');
62
+ if (envelopes['tierOneSubsetCompliance'] !== 'strict') return softSkip('inapplicable', `envelopes.tierOneSubsetCompliance is ${JSON.stringify(envelopes['tierOneSubsetCompliance'])}, not "strict" — the strict walk binds only a strict host`);
63
+ const catalog = await familyAdvertised('supportedEnvelopes');
64
+ const kinds = Array.isArray(catalog?.['kinds']) ? (catalog['kinds'] as unknown[]).filter((k): k is string => typeof k === 'string') : [];
65
+ const walked: Record<string, SchemaViolation[]> = {};
66
+ let resolved = 0;
67
+ for (const kind of kinds) {
68
+ const schema = loadV2EnvelopeSchema(kind);
69
+ if (schema === null) continue;
70
+ resolved++;
71
+ const v = tierOneViolations(schema, 'strict');
72
+ if (v.length > 0) walked[kind] = v;
73
+ }
74
+ if (resolved === 0) return softSkip('inapplicable', `none of the ${kinds.length} kinds in supportedEnvelopes.kinds resolves to a corpus schema; host-served vendor schemas are not fetched`);
75
+ expect(walked, req(ID_STRICT, 'capabilities.schema.json §envelopes.tierOneSubsetCompliance', `a strict host's advertised kinds MUST satisfy the Tier-1 intersection: ${JSON.stringify(walked)}`)).toEqual({});
76
+ });
77
+ });
@@ -0,0 +1,57 @@
1
+ /**
2
+ * v2 — variant payload discrimination, statically (RFC 0031 §A). The v1 twin
3
+ * is `envelope-variant-discriminator-static`; the walker lives in
4
+ * `lib/envelope-schema-static.ts`.
5
+ *
6
+ * For every payload schema in `schemas/v2/envelopes/`:
7
+ * no oneOf `oneOf` appears at no depth (Gemini drops it silently, so a
8
+ * model gets a looser schema than declared);
9
+ * discriminated in a variant union — an `anyOf` where two or more branches
10
+ * can be objects — every object branch declares, in `required`,
11
+ * a property of `type: string` with a one-value `enum`; a `$ref`
12
+ * branch is accepted unresolved, as at v1. A type union with at
13
+ * most one object branch (`string | binding`, as
14
+ * `ui.a2ui-surface` payload v2's `dyn*` defs declare) is
15
+ * selected by JSON type and is not judged: a scalar branch has
16
+ * no property to discriminate on. This scoping is new at v2 —
17
+ * the v1 walker flagged every non-`$ref` branch, which no v1
18
+ * schema exercised.
19
+ *
20
+ * No v2 core document states this rule: `spec/v2/core/` carries no text on
21
+ * variant payload discrimination (the nearest is `ext/a2uiSurface/README.md`,
22
+ * whose components are "discriminated by a single-string-enum `component`").
23
+ * The requirement is cited to RFC 0031 §A, its only normative home.
24
+ *
25
+ * Host-free: every leg runs with no OPENWOP_BASE_URL.
26
+ *
27
+ * @see RFCS/0031-envelope-variants-and-model-capabilities.md §A
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { req } from '../lib/requirement-ids.js';
32
+ import { anyOfDiscriminatorViolations, listV2EnvelopeKinds, loadV2EnvelopeSchema, oneOfViolations, UNIVERSAL_KINDS } from '../lib/envelope-schema-static.js';
33
+
34
+ const DOC = 'RFC 0031 §A';
35
+ const ID_PRESENT = 'openwop.requirement.envelopes.payload-schemas-present';
36
+ const ID_NO_ONEOF = 'openwop.requirement.envelopes.variant-no-oneof';
37
+ const ID_DISCRIMINATED = 'openwop.requirement.envelopes.variant-anyof-discriminated';
38
+
39
+ const KINDS = listV2EnvelopeKinds();
40
+
41
+ describe('v2 envelope variant discrimination (RFC 0031 §A)', () => {
42
+ it('schemas/v2/envelopes carries a payload schema for each universal kind', () => {
43
+ for (const k of UNIVERSAL_KINDS) expect(KINDS.includes(k), req(ID_PRESENT, DOC, `schemas/v2/envelopes/${k}.schema.json MUST exist`)).toBe(true);
44
+ });
45
+
46
+ for (const kind of KINDS) {
47
+ it(`${kind} contains no oneOf at any depth`, () => {
48
+ const v = oneOfViolations(loadV2EnvelopeSchema(kind));
49
+ expect(v, req(ID_NO_ONEOF, DOC, `${kind} MUST NOT use oneOf — use anyOf with a single-string-enum discriminator: ${JSON.stringify(v)}`)).toEqual([]);
50
+ });
51
+
52
+ it(`${kind} discriminates every anyOf branch by a single-string-enum property`, () => {
53
+ const v = anyOfDiscriminatorViolations(loadV2EnvelopeSchema(kind));
54
+ expect(v, req(ID_DISCRIMINATED, DOC, `${kind} anyOf branches without a discriminator: ${JSON.stringify(v)}`)).toEqual([]);
55
+ });
56
+ }
57
+ });
@@ -0,0 +1,85 @@
1
+ /**
2
+ * v2 — the `envelopes` advertisement (`spec/v2/core/events.md` §`envelopes`;
3
+ * RFCs 0030, 0032, 0033). A claims-check family: what the host says about its
4
+ * envelope posture is what is witnessed. The v1 twins are the advertisement
5
+ * legs of `envelope-reasoning-shape`, `envelope-retry-attempted` /
6
+ * `envelope-refusal-shape` and `envelope-completion-distinguishes-truncation`;
7
+ * the legs live in `lib/family-advert-witness.ts`.
8
+ *
9
+ * record the record validates against its seat in
10
+ * `schemas/v2/capabilities.schema.json` (no v1 `supported` seat);
11
+ * reasoning `reasoning.promptDirective` is mandatory | advisory | off;
12
+ * tier-one `tierOneSubsetCompliance` is strict | warn | off;
13
+ * reliability `reliability.events[]` names only reliability events and, with
14
+ * the four v1 dotted names folded to their v2 equivalents
15
+ * (aliases until 3.0, RFC 0228 §G), lists the two MUST-tier
16
+ * events `envelope.retry-exhausted` and `envelope.refusal`;
17
+ * completion `reliability.completion.distinguishesTruncation` is a boolean
18
+ * and `truncationBudgetMultiplier` is in [1, 8].
19
+ *
20
+ * Dispositions: discovery unreadable ⇒ `blocked`; `envelopes` absent, or the
21
+ * facet a leg reads absent ⇒ `inapplicable`.
22
+ *
23
+ * @see spec/v2/core/events.md §envelopes
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { v2Discovery } from '../lib/v2.js';
28
+ import { softSkip } from '../lib/soft-skip.js';
29
+ import { req } from '../lib/requirement-ids.js';
30
+ import { majorProfile } from '../lib/major-profile.js';
31
+ import { envelopesCompletionLeg, envelopesReasoningLeg, envelopesReliabilityLeg, envelopesTierOneLeg, recordSchemaLeg } from '../lib/family-advert-witness.js';
32
+
33
+ const PROFILE = majorProfile(2);
34
+ const ID_RECORD = 'openwop.requirement.envelopes.advert-record-schema';
35
+ const ID_REASONING = 'openwop.requirement.envelopes.reasoning-advert-shape';
36
+ const ID_TIER_ONE = 'openwop.requirement.envelopes.tier-one-subset-compliance-value';
37
+ const ID_RELIABILITY = 'openwop.requirement.envelopes.reliability-events-advert';
38
+ const ID_COMPLETION = 'openwop.requirement.envelopes.completion-advert-shape';
39
+
40
+ async function discovery(): Promise<Record<string, unknown> | null> {
41
+ try { return await v2Discovery(); } catch { return null; }
42
+ }
43
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
44
+
45
+ describe('v2 envelopes advertisement (events.md §envelopes)', () => {
46
+ it('the envelopes record validates against its capabilities-schema seat', async () => {
47
+ const doc = await discovery();
48
+ if (!doc) return softSkip('blocked', UNREADABLE);
49
+ const out = recordSchemaLeg(PROFILE, doc, 'envelopes', 'events.md §envelopes');
50
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
51
+ for (const x of out.findings) expect(x.ok, req(ID_RECORD, x.doc, x.message)).toBe(true);
52
+ });
53
+
54
+ it('envelopes.reasoning.promptDirective is mandatory, advisory or off', async () => {
55
+ const doc = await discovery();
56
+ if (!doc) return softSkip('blocked', UNREADABLE);
57
+ const out = envelopesReasoningLeg(PROFILE, doc);
58
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
59
+ for (const x of out.findings) expect(x.ok, req(ID_REASONING, x.doc, x.message)).toBe(true);
60
+ });
61
+
62
+ it('envelopes.tierOneSubsetCompliance is strict, warn or off', async () => {
63
+ const doc = await discovery();
64
+ if (!doc) return softSkip('blocked', UNREADABLE);
65
+ const out = envelopesTierOneLeg(PROFILE, doc);
66
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
67
+ for (const x of out.findings) expect(x.ok, req(ID_TIER_ONE, x.doc, x.message)).toBe(true);
68
+ });
69
+
70
+ it('envelopes.reliability.events lists only reliability events and includes retry-exhausted and refusal', async () => {
71
+ const doc = await discovery();
72
+ if (!doc) return softSkip('blocked', UNREADABLE);
73
+ const out = envelopesReliabilityLeg(PROFILE, doc);
74
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
75
+ for (const x of out.findings) expect(x.ok, req(ID_RELIABILITY, x.doc, x.message)).toBe(true);
76
+ });
77
+
78
+ it('envelopes.reliability.completion.distinguishesTruncation is a boolean', async () => {
79
+ const doc = await discovery();
80
+ if (!doc) return softSkip('blocked', UNREADABLE);
81
+ const out = envelopesCompletionLeg(PROFILE, doc);
82
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
83
+ for (const x of out.findings) expect(x.ok, req(ID_COMPLETION, x.doc, x.message)).toBe(true);
84
+ });
85
+ });
@@ -0,0 +1,91 @@
1
+ /**
2
+ * v2 — locale negotiation (`spec/v2/core/i18n.md` §`Accept-Language`,
3
+ * §Fallback, §Error envelopes, §The `i18n` record). The v1 twin is
4
+ * `i18n-negotiation`; the legs live in `lib/i18n-negotiation-witness.ts`, and
5
+ * what differs between majors (the unversioned run path, the `~`-projected
6
+ * bound id, presence-as-advertisement) is the profile row.
7
+ *
8
+ * shape the `i18n` record validates against the v2 capabilities
9
+ * schema, and `supportedLocales` contains `defaultLocale`;
10
+ * unsupported an unsupported `Accept-Language` is answered exactly as the
11
+ * same request without it; `Content-Language`, when sent, is a
12
+ * BCP 47 tag naming the default locale;
13
+ * malformed a malformed `Accept-Language` never yields `400`;
14
+ * error code the `error` code and the `details` keys are identical under
15
+ * the default and a negotiated locale.
16
+ *
17
+ * The probe reads a run id no host minted (`GET /runs/{bound id}`). Its answer
18
+ * is not pinned — `404 not_found`, or `403 id_tenant_mismatch` because the
19
+ * tenant segment is not the caller's — since every leg compares against the
20
+ * same request with no `Accept-Language`.
21
+ *
22
+ * Dispositions: no target ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`;
23
+ * `i18n` not advertised ⇒ `inapplicable` (gated with the record's presence,
24
+ * never a strict-mode failure); a probe that answers no 4xx envelope ⇒
25
+ * `blocked` (no baseline).
26
+ *
27
+ * Proven against a scratch double in `lib/i18n-negotiation-witness.test.ts`.
28
+ *
29
+ * @see spec/v2/core/i18n.md
30
+ * @see schemas/v2/capabilities.schema.json §i18n
31
+ */
32
+
33
+ import { describe, it, expect } from 'vitest';
34
+ import { familyAdvertised, v2Discovery, v2RefValidator } from '../lib/v2.js';
35
+ import { softSkip } from '../lib/soft-skip.js';
36
+ import { req } from '../lib/requirement-ids.js';
37
+ import { majorProfile } from '../lib/major-profile.js';
38
+ import { errorCodeLeg, i18nAdverts, malformedLeg, shapeLeg, unsupportedLeg, type I18nAdverts } from '../lib/i18n-negotiation-witness.js';
39
+
40
+ const PROFILE = majorProfile(2);
41
+ const ID_SHAPE = 'openwop.requirement.i18n.record-shape';
42
+ const ID_UNSUPPORTED = 'openwop.requirement.i18n.unsupported-locale-falls-back';
43
+ const ID_MALFORMED = 'openwop.requirement.i18n.malformed-accept-language-not-400';
44
+ const ID_CODE = 'openwop.requirement.i18n.error-code-locale-invariant';
45
+ const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
46
+ const validRecord = v2RefValidator('capabilities.schema.json#/properties/i18n');
47
+
48
+ type Ready = { ok: true; a: I18nAdverts } | { ok: false; skip: () => undefined };
49
+ async function ready(): Promise<Ready> {
50
+ if (HTTP_SKIP) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
51
+ let doc: Record<string, unknown> | null;
52
+ try { doc = await v2Discovery(); } catch { doc = null; }
53
+ if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
54
+ if ((await familyAdvertised('i18n')) === null) return { ok: false, skip: () => softSkip('inapplicable', 'the host does not advertise i18n — it serves one locale and ignores Accept-Language (i18n.md §The i18n record)') };
55
+ return { ok: true, a: i18nAdverts(PROFILE, doc) };
56
+ }
57
+
58
+ describe('v2 i18n negotiation (i18n.md §Accept-Language, §Fallback, §Error envelopes)', () => {
59
+ it('the advertised i18n record is well-formed and lists its default locale', async () => {
60
+ const r = await ready();
61
+ if (!r.ok) return r.skip();
62
+ const v = validRecord(r.a.record);
63
+ const out = shapeLeg(r.a);
64
+ expect(v.ok, req(ID_SHAPE, 'schemas/v2/capabilities.schema.json §i18n', `the i18n record MUST validate: ${v.errors}`)).toBe(true);
65
+ if (out.kind === 'observed') for (const x of out.findings) expect(x.ok, req(ID_SHAPE, x.doc, x.message)).toBe(true);
66
+ });
67
+
68
+ it('an unsupported Accept-Language falls back and Content-Language never lies', async () => {
69
+ const r = await ready();
70
+ if (!r.ok) return r.skip();
71
+ const out = await unsupportedLeg(PROFILE, r.a);
72
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
73
+ for (const x of out.findings) expect(x.ok, req(ID_UNSUPPORTED, x.doc, x.message)).toBe(true);
74
+ });
75
+
76
+ it('a malformed Accept-Language does not cause 400', async () => {
77
+ const r = await ready();
78
+ if (!r.ok) return r.skip();
79
+ const out = await malformedLeg(PROFILE, r.a);
80
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
81
+ for (const x of out.findings) expect(x.ok, req(ID_MALFORMED, x.doc, x.message)).toBe(true);
82
+ });
83
+
84
+ it('the error code and details keys are identical under every negotiated locale', async () => {
85
+ const r = await ready();
86
+ if (!r.ok) return r.skip();
87
+ const out = await errorCodeLeg(PROFILE, r.a);
88
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
89
+ for (const x of out.findings) expect(x.ok, req(ID_CODE, x.doc, x.message)).toBe(true);
90
+ });
91
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * v2 — the three envelope caps on `limits` (`spec/v2/core/runs.md` §Limits:
3
+ * "`limits` always carries `clarificationRounds` (per task), `schemaRounds`
4
+ * (per envelope) and `envelopesPerTurn` (per chat turn)"). The v1 twin is the
5
+ * advertisement half of `aiEnvelope.capBreached`; the legs live in
6
+ * `lib/family-advert-witness.ts`. The other `limits` facets
7
+ * (`maxRunDurationMs`, `maxLoopIterations`) are `v2-run-execution-bounds`.
8
+ *
9
+ * Each cap MUST be present and validate against its subschema in
10
+ * `schemas/v2/capabilities.schema.json` (a non-negative integer).
11
+ *
12
+ * Dispositions: discovery unreadable ⇒ `blocked`; `limits` absent ⇒
13
+ * `inapplicable` (at v2 presence of the record is the claim; the v1 rule that
14
+ * `limits` itself is always present has no v2 seat — the root schema requires
15
+ * only `protocolVersions` and `preferredVersion`).
16
+ *
17
+ * @see spec/v2/core/runs.md §Limits
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { v2Discovery } from '../lib/v2.js';
22
+ import { softSkip } from '../lib/soft-skip.js';
23
+ import { req } from '../lib/requirement-ids.js';
24
+ import { majorProfile } from '../lib/major-profile.js';
25
+ import { limitsCapLeg } from '../lib/family-advert-witness.js';
26
+
27
+ const PROFILE = majorProfile(2);
28
+ const UNREADABLE = 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0';
29
+ const ID_CLARIFICATION = 'openwop.requirement.limits.clarification-rounds-advertised';
30
+ const ID_SCHEMA = 'openwop.requirement.limits.schema-rounds-advertised';
31
+ const ID_ENVELOPES = 'openwop.requirement.limits.envelopes-per-turn-advertised';
32
+
33
+ async function discovery(): Promise<Record<string, unknown> | null> {
34
+ try { return await v2Discovery(); } catch { return null; }
35
+ }
36
+
37
+ describe('v2 limits envelope caps (runs.md §Limits)', () => {
38
+ it('limits.clarificationRounds is present and a non-negative integer', async () => {
39
+ const doc = await discovery();
40
+ if (!doc) return softSkip('blocked', UNREADABLE);
41
+ const out = limitsCapLeg(PROFILE, doc, 'clarificationRounds');
42
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
43
+ for (const x of out.findings) expect(x.ok, req(ID_CLARIFICATION, x.doc, x.message)).toBe(true);
44
+ });
45
+
46
+ it('limits.schemaRounds is present and a non-negative integer', async () => {
47
+ const doc = await discovery();
48
+ if (!doc) return softSkip('blocked', UNREADABLE);
49
+ const out = limitsCapLeg(PROFILE, doc, 'schemaRounds');
50
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
51
+ for (const x of out.findings) expect(x.ok, req(ID_SCHEMA, x.doc, x.message)).toBe(true);
52
+ });
53
+
54
+ it('limits.envelopesPerTurn is present and a non-negative integer', async () => {
55
+ const doc = await discovery();
56
+ if (!doc) return softSkip('blocked', UNREADABLE);
57
+ const out = limitsCapLeg(PROFILE, doc, 'envelopesPerTurn');
58
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
59
+ for (const x of out.findings) expect(x.ok, req(ID_ENVELOPES, x.doc, x.message)).toBe(true);
60
+ });
61
+ });