@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,130 @@
1
+ /**
2
+ * v2 — the mutable prompt library (`spec/v2/core/host-services.md` §`prompts`
3
+ * → §Library; `api/v2/openapi.yaml` `createPromptTemplate`,
4
+ * `updatePromptTemplate`, `deletePromptTemplate`; RFC 0028 §A). The v1 twin is
5
+ * `prompt-mutable-lifecycle`; the legs live in `lib/prompt-library-witness.ts`.
6
+ *
7
+ * One round trip on a fresh user template, observed once
8
+ * (`driveLifecycle`) and judged per step (`judgeLifecycle`, pure):
9
+ * create → 201 + Location; read → `meta.source: user`; duplicate
10
+ * (templateId, version) → 409; update with a greater SemVer → 200 and stored;
11
+ * a lower SemVer → 409; delete → 204, then 404.
12
+ * Plus: a host built-in is read-only (DELETE → 403); NEW at v2, writes MUST be
13
+ * authenticated (unauthenticated POST → 401); and, unaided, with
14
+ * `mutableLibrary` unadvertised a write answers `404 not_found` (errors.md
15
+ * §Unadvertised operations).
16
+ *
17
+ * Dispositions: no target ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`;
18
+ * `prompts` absent, or `endpointsSupported`/`mutableLibrary` not `true` ⇒ the
19
+ * write legs are `inapplicable` (gated by presence, never a strict-mode
20
+ * failure) and the gate-off leg binds; no host built-in listed ⇒ read-only leg
21
+ * `inapplicable`.
22
+ *
23
+ * Proven against a scratch double in `lib/prompt-library-witness.test.ts`.
24
+ *
25
+ * @see spec/v2/core/host-services.md §Library
26
+ */
27
+
28
+ import { describe, it, expect } from 'vitest';
29
+ import { v2Discovery } from '../lib/v2.js';
30
+ import { softSkip } from '../lib/soft-skip.js';
31
+ import { req } from '../lib/requirement-ids.js';
32
+ import { majorProfile } from '../lib/major-profile.js';
33
+ import { driveLifecycle, gateOffWriteLeg, judgeLifecycle, promptAdverts, readOnlyLeg, unauthenticatedWriteLeg, type LifecycleObservation, type PromptAdverts, type Skip } from '../lib/prompt-library-witness.js';
34
+
35
+ const PROFILE = majorProfile(2);
36
+ const ID_CREATE = 'openwop.requirement.prompts.create-user-template';
37
+ const ID_READ = 'openwop.requirement.prompts.created-template-source-user';
38
+ const ID_DUP = 'openwop.requirement.prompts.duplicate-version-409';
39
+ const ID_UPDATE = 'openwop.requirement.prompts.update-greater-semver';
40
+ const ID_NONMONO = 'openwop.requirement.prompts.update-non-monotonic-409';
41
+ const ID_DELETE = 'openwop.requirement.prompts.delete-user-template';
42
+ const ID_READONLY = 'openwop.requirement.prompts.builtin-read-only';
43
+ const ID_AUTH = 'openwop.requirement.prompts.writes-authenticated';
44
+ const ID_GATE = 'openwop.requirement.prompts.mutable-gate-off-404';
45
+ const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
46
+
47
+ type Ready = { ok: true; a: PromptAdverts } | { ok: false; skip: () => undefined };
48
+ async function ready(): Promise<Ready> {
49
+ if (HTTP_SKIP) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
50
+ let doc: Record<string, unknown> | null;
51
+ try { doc = await v2Discovery(); } catch { doc = null; }
52
+ if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
53
+ return { ok: true, a: promptAdverts(PROFILE, doc) };
54
+ }
55
+
56
+ /** The round trip runs once per file; each step's `it` judges its own part. */
57
+ let lifecycle: Promise<Skip | LifecycleObservation> | undefined;
58
+ type Step = { ok: true; o: LifecycleObservation } | { ok: false; skip: () => undefined };
59
+ async function step(): Promise<Step> {
60
+ const r = await ready();
61
+ if (!r.ok) return r;
62
+ lifecycle ??= driveLifecycle(PROFILE, r.a);
63
+ const o = await lifecycle;
64
+ if ('kind' in o) return { ok: false, skip: () => softSkip(o.disposition, o.reason) };
65
+ return { ok: true, o };
66
+ }
67
+
68
+ describe('v2 prompt library: user-template round trip (host-services.md §Library)', () => {
69
+ it('POST /prompts creates a user template with 201 and a Location', async () => {
70
+ const s = await step();
71
+ if (!s.ok) return s.skip();
72
+ for (const x of judgeLifecycle(s.o).create) expect(x.ok, req(ID_CREATE, x.doc, x.message)).toBe(true);
73
+ });
74
+
75
+ it('the created template reads back with meta.source user', async () => {
76
+ const s = await step();
77
+ if (!s.ok) return s.skip();
78
+ for (const x of judgeLifecycle(s.o).read) expect(x.ok, req(ID_READ, x.doc, x.message)).toBe(true);
79
+ });
80
+
81
+ it('a second POST of the same templateId and version answers 409', async () => {
82
+ const s = await step();
83
+ if (!s.ok) return s.skip();
84
+ for (const x of judgeLifecycle(s.o).duplicate) expect(x.ok, req(ID_DUP, x.doc, x.message)).toBe(true);
85
+ });
86
+
87
+ it('PUT with a greater SemVer replaces the template', async () => {
88
+ const s = await step();
89
+ if (!s.ok) return s.skip();
90
+ for (const x of judgeLifecycle(s.o).update) expect(x.ok, req(ID_UPDATE, x.doc, x.message)).toBe(true);
91
+ });
92
+
93
+ it('PUT with a lower SemVer answers 409', async () => {
94
+ const s = await step();
95
+ if (!s.ok) return s.skip();
96
+ for (const x of judgeLifecycle(s.o).nonMonotonic) expect(x.ok, req(ID_NONMONO, x.doc, x.message)).toBe(true);
97
+ });
98
+
99
+ it('DELETE answers 204 and the template then reads 404', async () => {
100
+ const s = await step();
101
+ if (!s.ok) return s.skip();
102
+ for (const x of judgeLifecycle(s.o).remove) expect(x.ok, req(ID_DELETE, x.doc, x.message)).toBe(true);
103
+ });
104
+ });
105
+
106
+ describe('v2 prompt library: write guards (host-services.md §Library; errors.md §Unadvertised operations)', () => {
107
+ it('a host built-in template is read-only: DELETE answers 403', async () => {
108
+ const r = await ready();
109
+ if (!r.ok) return r.skip();
110
+ const out = await readOnlyLeg(PROFILE, r.a);
111
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
112
+ for (const x of out.findings) expect(x.ok, req(ID_READONLY, x.doc, x.message)).toBe(true);
113
+ });
114
+
115
+ it('an unauthenticated write is refused 401', async () => {
116
+ const r = await ready();
117
+ if (!r.ok) return r.skip();
118
+ const out = await unauthenticatedWriteLeg(r.a);
119
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
120
+ for (const x of out.findings) expect(x.ok, req(ID_AUTH, x.doc, x.message)).toBe(true);
121
+ });
122
+
123
+ it('with mutableLibrary unadvertised, POST /prompts answers 404 not_found', async () => {
124
+ const r = await ready();
125
+ if (!r.ok) return r.skip();
126
+ const out = await gateOffWriteLeg(PROFILE, r.a);
127
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
128
+ for (const x of out.findings) expect(x.ok, req(ID_GATE, x.doc, x.message)).toBe(true);
129
+ });
130
+ });
@@ -0,0 +1,78 @@
1
+ /**
2
+ * v2 — prompt packs on the library surface (`spec/v2/core/host-services.md`
3
+ * §`prompts` → §Library; `capabilities.schema.json` §prompts.packsSupported;
4
+ * RFC 0028 §B–§C). The v1 twin is `prompt-pack-install`; the legs live in
5
+ * `lib/prompt-library-witness.ts`.
6
+ *
7
+ * listing `GET /prompts?source=pack` answers 200 and lists only pack
8
+ * templates; NEW at v2, with `packsSupported` not advertised it
9
+ * lists none ("packs are not loaded"). With the operator flag
10
+ * `OPENWOP_TEST_PROMPT_PACK_INSTALLED=true` (client-side, as at
11
+ * v1) it MUST list at least one;
12
+ * stamps every pack template carries `meta.packName` and a SemVer
13
+ * `meta.packVersion` and validates;
14
+ * reference the in-tree reference pack `vendor.openwop.prompt-sample`:
15
+ * `writer-system` is fetched by id under its `libraryId` and
16
+ * keeps its pack provenance.
17
+ *
18
+ * Dispositions: no target ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`;
19
+ * `prompts` absent or `endpointsSupported` not `true` ⇒ `inapplicable`;
20
+ * `packsSupported` not `true` ⇒ stamps and reference `inapplicable`; no pack
21
+ * installed ⇒ stamps `inapplicable` (zero packs is conformant); the reference
22
+ * pack not installed ⇒ reference `inapplicable`, never `blocked`.
23
+ *
24
+ * Proven against a scratch double in `lib/prompt-library-witness.test.ts`.
25
+ *
26
+ * @see spec/v2/core/host-services.md §Library
27
+ * @see schemas/v2/prompt-pack-manifest.schema.json
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { v2Discovery, v2Validator } from '../lib/v2.js';
32
+ import { softSkip } from '../lib/soft-skip.js';
33
+ import { req } from '../lib/requirement-ids.js';
34
+ import { majorProfile } from '../lib/major-profile.js';
35
+ import { packListLeg, packStampLeg, promptAdverts, referencePackLeg, type PromptAdverts } from '../lib/prompt-library-witness.js';
36
+
37
+ const PROFILE = majorProfile(2);
38
+ const ID_LIST = 'openwop.requirement.prompts.pack-listing';
39
+ const ID_STAMPS = 'openwop.requirement.prompts.pack-template-stamps';
40
+ const ID_REFERENCE = 'openwop.requirement.prompts.reference-pack-fetch';
41
+ const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
42
+ const REQUIRE_INSTALLED = process.env['OPENWOP_TEST_PROMPT_PACK_INSTALLED'] === 'true';
43
+ const template = v2Validator('prompt-template');
44
+
45
+ type Ready = { ok: true; a: PromptAdverts } | { ok: false; skip: () => undefined };
46
+ async function ready(): Promise<Ready> {
47
+ if (HTTP_SKIP) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
48
+ let doc: Record<string, unknown> | null;
49
+ try { doc = await v2Discovery(); } catch { doc = null; }
50
+ if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
51
+ return { ok: true, a: promptAdverts(PROFILE, doc) };
52
+ }
53
+
54
+ describe('v2 prompt packs (host-services.md §Library)', () => {
55
+ it('GET /prompts?source=pack lists only pack templates, and none unless packsSupported', async () => {
56
+ const r = await ready();
57
+ if (!r.ok) return r.skip();
58
+ const out = await packListLeg(r.a, { requireInstalled: REQUIRE_INSTALLED });
59
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
60
+ for (const x of out.findings) expect(x.ok, req(ID_LIST, x.doc, x.message)).toBe(true);
61
+ });
62
+
63
+ it('every pack template carries meta.packName and meta.packVersion', async () => {
64
+ const r = await ready();
65
+ if (!r.ok) return r.skip();
66
+ const out = await packStampLeg(r.a, template);
67
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
68
+ for (const x of out.findings) expect(x.ok, req(ID_STAMPS, x.doc, x.message)).toBe(true);
69
+ });
70
+
71
+ it('the reference pack template is fetched by id and keeps its pack provenance', async () => {
72
+ const r = await ready();
73
+ if (!r.ok) return r.skip();
74
+ const out = await referencePackLeg(PROFILE, r.a);
75
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
76
+ for (const x of out.findings) expect(x.ok, req(ID_REFERENCE, x.doc, x.message)).toBe(true);
77
+ });
78
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * v2 — `renderPromptTemplate` (`spec/v2/core/host-services.md` §`prompts` →
3
+ * §Library, §Composition; `api/v2/openapi.yaml` `POST /prompts:render`; RFC
4
+ * 0028 §A). The v1 twin is `prompt-render-deterministic`; the legs live in
5
+ * `lib/prompt-library-witness.ts`. The render path is `prompts.library.
6
+ * renderEndpoint` when advertised, else `/prompts:render`.
7
+ *
8
+ * deterministic identical (ref, variables) render the identical `hash` and
9
+ * `variableHashes` (the hash MUST equal the dispatch-time
10
+ * `prompt.composed` hash, so it cannot vary per call);
11
+ * varies a changed required binding changes `hash` and its
12
+ * `variableHashes` entry;
13
+ * shape `hash` and every `variableHashes` value are
14
+ * `sha256:<hex64>`; under `observability: full` `composed`
15
+ * carries the body for every kind (a few-shot or schema-hint
16
+ * template preferred), otherwise `composed` is absent —
17
+ * bodies only under `full` (NEW at v2: the v1 file required a
18
+ * body unconditionally);
19
+ * unbound NEW at v2: a required variable left unbound is refused
20
+ * `400 validation_error`.
21
+ *
22
+ * Dispositions: no target ⇒ `inapplicable`; discovery unreadable ⇒ `blocked`;
23
+ * `prompts` absent or `endpointsSupported` not `true` ⇒ `inapplicable` (gated
24
+ * by presence); no renderable template (none listed, or each needs a secret
25
+ * binding) or none with a required variable ⇒ `inapplicable`.
26
+ *
27
+ * Proven against a scratch double in `lib/prompt-library-witness.test.ts`.
28
+ *
29
+ * @see spec/v2/core/host-services.md §Library
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 { deterministicLeg, promptAdverts, renderShapeLeg, unboundRequiredLeg, variesLeg, type PromptAdverts } from '../lib/prompt-library-witness.js';
38
+
39
+ const PROFILE = majorProfile(2);
40
+ const ID_DETERMINISTIC = 'openwop.requirement.prompts.render-deterministic';
41
+ const ID_VARIES = 'openwop.requirement.prompts.render-binding-sensitive';
42
+ const ID_SHAPE = 'openwop.requirement.prompts.render-shape';
43
+ const ID_UNBOUND = 'openwop.requirement.prompts.render-unbound-required-refused';
44
+ const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
45
+
46
+ type Ready = { ok: true; a: PromptAdverts } | { ok: false; skip: () => undefined };
47
+ async function ready(): Promise<Ready> {
48
+ if (HTTP_SKIP) return { ok: false, skip: () => softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset') };
49
+ let doc: Record<string, unknown> | null;
50
+ try { doc = await v2Discovery(); } catch { doc = null; }
51
+ if (!doc) return { ok: false, skip: () => softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0') };
52
+ return { ok: true, a: promptAdverts(PROFILE, doc) };
53
+ }
54
+
55
+ describe('v2 prompt render (host-services.md §Library, §Composition)', () => {
56
+ it('identical ref and variables render the identical hash and variableHashes', async () => {
57
+ const r = await ready();
58
+ if (!r.ok) return r.skip();
59
+ const out = await deterministicLeg(r.a);
60
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
61
+ for (const x of out.findings) expect(x.ok, req(ID_DETERMINISTIC, x.doc, x.message)).toBe(true);
62
+ });
63
+
64
+ it('a changed binding changes the hash and its variableHash', async () => {
65
+ const r = await ready();
66
+ if (!r.ok) return r.skip();
67
+ const out = await variesLeg(r.a);
68
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
69
+ for (const x of out.findings) expect(x.ok, req(ID_VARIES, x.doc, x.message)).toBe(true);
70
+ });
71
+
72
+ it('hashes are sha256 hex and the body is present only under observability full', async () => {
73
+ const r = await ready();
74
+ if (!r.ok) return r.skip();
75
+ const out = await renderShapeLeg(r.a);
76
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
77
+ for (const x of out.findings) expect(x.ok, req(ID_SHAPE, x.doc, x.message)).toBe(true);
78
+ });
79
+
80
+ it('an unbound required variable is refused 400 validation_error', async () => {
81
+ const r = await ready();
82
+ if (!r.ok) return r.skip();
83
+ const out = await unboundRequiredLeg(r.a);
84
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
85
+ for (const x of out.findings) expect(x.ok, req(ID_UNBOUND, x.doc, x.message)).toBe(true);
86
+ });
87
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * v2 — the resolution chain on the production wire
3
+ * (`spec/v2/core/host-services.md` §`prompts` → §Resolution; RFC 0029). The v1
4
+ * twin is `prompt-resolution-chain-event`; the judge lives in
5
+ * `lib/prompt-events-witness.ts`.
6
+ *
7
+ * A run of `conformance-prompt-end-to-end` is created with `POST /runs` and its
8
+ * durable log read with `GET /runs/{runId}/events/poll` — no seam. Every
9
+ * `agent.prompt-resolved` (v1 `agent.promptResolved`) carries a non-empty
10
+ * `chain[]`, one entry per layer tried, each naming a layer and a boolean
11
+ * `applied`; at most one is applied, and `resolved` mirrors its `source` (or is
12
+ * `null` with none applied).
13
+ *
14
+ * Stricter than v1: at v2 the emission is a MUST ("MUST emit
15
+ * agent.prompt-resolved"), so a host advertising `prompts` that emits none
16
+ * fails, where the v1 file soft-skipped `blocked`.
17
+ *
18
+ * Dispositions: `prompts` not advertised or the fixture not in `fixtures[]` ⇒
19
+ * `inapplicable`; discovery unreadable ⇒ `blocked`.
20
+ *
21
+ * Proven against a scratch double in `lib/prompt-events-witness.test.ts`.
22
+ *
23
+ * @see spec/v2/core/host-services.md §Resolution
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 { END_TO_END_FIXTURE, driveFixture, judgeChain } from '../lib/prompt-events-witness.js';
32
+
33
+ const PROFILE = majorProfile(2);
34
+ const ID_CHAIN = 'openwop.requirement.prompts.resolution-chain-record';
35
+
36
+ describe('v2 prompt events: the resolution chain record (host-services.md §Resolution)', () => {
37
+ it('every agent.prompt-resolved records the chain it walked, with one applied layer mirrored by resolved', async () => {
38
+ if (!process.env['OPENWOP_BASE_URL']) return softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
39
+ let doc: Record<string, unknown> | null;
40
+ try { doc = await v2Discovery(); } catch { doc = null; }
41
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
42
+ const o = await driveFixture(PROFILE, doc, END_TO_END_FIXTURE);
43
+ if ('kind' in o) return softSkip(o.disposition, o.reason);
44
+ for (const x of judgeChain(o)) expect(x.ok, req(ID_CHAIN, x.doc, x.message)).toBe(true);
45
+ }, 60_000);
46
+ });
@@ -0,0 +1,112 @@
1
+ /**
2
+ * v2 — prompt wire shapes (`schemas/v2/prompt-kind.schema.json`,
3
+ * `prompt-template.schema.json`, `prompt-ref.schema.json`,
4
+ * `prompt-pack-manifest.schema.json`; `spec/v2/core/host-services.md`
5
+ * §`prompts`; RFCs 0027, 0028). The v1 twin is `prompt-template-shape`.
6
+ *
7
+ * Server-free: the four-kind enum; PromptTemplate round-trips and is CLOSED at
8
+ * v2 (an unknown property is rejected, an `x-`/`vendor.`/`openwop-` extension
9
+ * is admitted); a `source: pack` meta without `packName`/`packVersion` is
10
+ * rejected; PromptRef's two forms; and a `kind: "prompt"` pack manifest
11
+ * carries no `nodes[]` or `chains[]` (host-services.md §Library).
12
+ *
13
+ * Live (gated by the presence of the v2 `prompts` record, `inapplicable` when
14
+ * absent): the advertised record validates against the v2 capabilities schema
15
+ * — no `supported` seat, `maxTemplateBytes` ≤ 65536, the closed facet set.
16
+ *
17
+ * @see spec/v2/core/host-services.md §prompts
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { readFileSync } from 'node:fs';
22
+ import { join } from 'node:path';
23
+ import { SCHEMAS_DIR } from '../lib/paths.js';
24
+ import { familyAdvertised, v2Discovery, v2RefValidator, v2Validator } from '../lib/v2.js';
25
+ import { softSkip } from '../lib/soft-skip.js';
26
+ import { req } from '../lib/requirement-ids.js';
27
+
28
+ const DOC = 'schemas/v2/prompt-template.schema.json';
29
+ const ID_KIND = 'openwop.requirement.prompts.prompt-kind-enum';
30
+ const ID_TEMPLATE = 'openwop.requirement.prompts.prompt-template-shape';
31
+ const ID_CLOSED = 'openwop.requirement.prompts.prompt-template-closed';
32
+ const ID_REF = 'openwop.requirement.prompts.prompt-ref-forms';
33
+ const ID_PACK = 'openwop.requirement.prompts.prompt-pack-manifest-shape';
34
+ const ID_RECORD = 'openwop.requirement.prompts.capability-record-shape';
35
+ const HTTP_SKIP = !process.env['OPENWOP_BASE_URL'];
36
+
37
+ const template = v2Validator('prompt-template');
38
+ const ref = v2Validator('prompt-ref');
39
+ const manifest = v2Validator('prompt-pack-manifest');
40
+ const record = v2RefValidator('capabilities.schema.json#/properties/prompts');
41
+ const minimal = { templateId: 'writer-system', version: '1.0.0', kind: 'system', text: 'You are a careful editorial writer.' };
42
+
43
+ describe('v2 prompt shapes: PromptKind and PromptTemplate (server-free)', () => {
44
+ it('prompt-kind is a string enum of exactly the four kinds', () => {
45
+ const k = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'prompt-kind.schema.json'), 'utf8')) as { type?: unknown; enum?: unknown };
46
+ expect(k.type, req(ID_KIND, 'schemas/v2/prompt-kind.schema.json', 'prompt-kind MUST be type string')).toBe('string');
47
+ expect(k.enum, req(ID_KIND, 'schemas/v2/prompt-kind.schema.json', 'prompt-kind MUST be exactly system, user, few-shot, schema-hint')).toEqual(['system', 'user', 'few-shot', 'schema-hint']);
48
+ });
49
+
50
+ it('a minimal and a full PromptTemplate validate; a missing text, a non-SemVer version, a bad templateId or kind, or a dashed variable name is rejected', () => {
51
+ expect(template(minimal).ok, req(ID_TEMPLATE, DOC, `a minimal PromptTemplate MUST validate: ${template(minimal).errors}`)).toBe(true);
52
+ const full = {
53
+ templateId: 'writer-user', version: '1.2.3', kind: 'user', text: 'Write about: {{topic}}\nTone: {{tone}}', name: 'Writer', description: 'Two variables.',
54
+ variables: [{ name: 'topic', type: 'string', required: true, source: 'input' }, { name: 'tone', type: 'string', required: false, source: 'input', defaultValue: 'neutral' }],
55
+ modelHints: { modelClass: 'writing', temperature: 0.7 }, tags: ['editorial'], meta: { author: 'openwop-conformance', createdAt: '2026-05-20T10:00:00Z', source: 'host' },
56
+ };
57
+ expect(template(full).ok, req(ID_TEMPLATE, DOC, `a full PromptTemplate MUST validate: ${template(full).errors}`)).toBe(true);
58
+ const { text: _t, ...noText } = minimal;
59
+ expect(template(noText).ok, req(ID_TEMPLATE, DOC, 'text is REQUIRED')).toBe(false);
60
+ expect(template({ ...minimal, version: 'v1' }).ok, req(ID_TEMPLATE, DOC, 'version MUST be SemVer')).toBe(false);
61
+ // v2 templateId is the author-minted ids.schema.json grammar `^[A-Za-z0-9._~:-]{1,128}$` (v1 rejected upper case; v2 does not).
62
+ expect(template({ ...minimal, templateId: 'writer/system' }).ok, req(ID_TEMPLATE, 'schemas/v2/ids.schema.json §templateId', 'templateId MUST match the templateId grammar (no /)')).toBe(false);
63
+ expect(template({ ...minimal, kind: 'made-up-kind' }).ok, req(ID_TEMPLATE, DOC, 'kind MUST be one of the four')).toBe(false);
64
+ expect(template({ ...minimal, variables: [{ name: 'has-dash', type: 'string', required: true }] }).ok, req(ID_TEMPLATE, DOC, 'a variable name MUST be a templating identifier (no dashes)')).toBe(false);
65
+ });
66
+
67
+ it('PromptTemplate is closed: an unknown property is rejected, an extension-prefixed one admitted, and a pack meta must carry its stamps', () => {
68
+ expect(template({ ...minimal, unknownExtra: 'x' }).ok, req(ID_CLOSED, DOC, 'an unknown top-level property MUST be rejected')).toBe(false);
69
+ expect(template({ ...minimal, 'x-acme-note': 'ok' }).ok, req(ID_CLOSED, `${DOC} patternProperties (RFC 0138)`, 'an x- extension property MUST be admitted')).toBe(true);
70
+ expect(template({ ...minimal, variables: [{ name: 'topic', type: 'string', required: true, extra: 1 }] }).ok, req(ID_CLOSED, DOC, 'PromptVariable is closed')).toBe(false);
71
+ expect(template({ ...minimal, meta: { source: 'pack' } }).ok, req(ID_CLOSED, 'host-services.md §Library', 'a pack template MUST carry meta.packName and meta.packVersion')).toBe(false);
72
+ expect(template({ ...minimal, meta: { source: 'pack', packName: 'vendor.openwop.prompt-sample', packVersion: '1.0.0' } }).ok, req(ID_CLOSED, DOC, 'a stamped pack template MUST validate')).toBe(true);
73
+ });
74
+ });
75
+
76
+ describe('v2 prompt shapes: PromptRef (server-free)', () => {
77
+ it('the string and object forms validate; a missing prefix, a non-SemVer version, a missing templateId or an unknown property is rejected', () => {
78
+ for (const ok of ['prompt:writer-system', 'prompt:writer-system@1.2.3', 'prompt:vendor.acme.writer.v2@2.0.0']) {
79
+ expect(ref(ok).ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', `${ok} MUST validate`)).toBe(true);
80
+ }
81
+ expect(ref({ templateId: 'writer-system' }).ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', 'the object form with templateId alone MUST validate')).toBe(true);
82
+ expect(ref({ libraryId: 'vendor.acme.editorial-prompts', templateId: 'writer-system', version: '1.0.0', variableOverrides: { tone: 'formal' } }).ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', 'the full object form MUST validate')).toBe(true);
83
+ expect(ref('writer-system').ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', 'a string ref MUST start with prompt:')).toBe(false);
84
+ expect(ref('prompt:writer-system@latest').ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', 'a string ref version MUST be SemVer')).toBe(false);
85
+ expect(ref({ version: '1.0.0' }).ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', 'the object form MUST carry templateId')).toBe(false);
86
+ expect(ref({ templateId: 'writer-system', unknownExtra: true }).ok, req(ID_REF, 'schemas/v2/prompt-ref.schema.json', 'the object form is closed')).toBe(false);
87
+ });
88
+ });
89
+
90
+ describe('v2 prompt shapes: the prompt pack manifest (server-free)', () => {
91
+ it('a kind prompt pack validates and carries no nodes or chains', () => {
92
+ const good = { name: 'vendor.openwop.prompt-sample', version: '1.0.0', kind: 'prompt', engines: { openwop: '>=2.0.0' }, prompts: [minimal] };
93
+ expect(manifest(good).ok, req(ID_PACK, 'schemas/v2/prompt-pack-manifest.schema.json', `a conforming prompt pack MUST validate: ${manifest(good).errors}`)).toBe(true);
94
+ expect(manifest({ ...good, nodes: [] }).ok, req(ID_PACK, 'host-services.md §Library', 'a kind prompt pack MUST NOT carry nodes[]')).toBe(false);
95
+ expect(manifest({ ...good, chains: [] }).ok, req(ID_PACK, 'host-services.md §Library', 'a kind prompt pack MUST NOT carry chains[]')).toBe(false);
96
+ expect(manifest({ ...good, kind: 'node' }).ok, req(ID_PACK, 'schemas/v2/prompt-pack-manifest.schema.json', 'kind MUST be prompt')).toBe(false);
97
+ expect(manifest({ ...good, prompts: [] }).ok, req(ID_PACK, 'schemas/v2/prompt-pack-manifest.schema.json', 'a prompt pack MUST ship at least one template')).toBe(false);
98
+ });
99
+ });
100
+
101
+ describe('v2 prompt shapes: the advertised prompts record (live)', () => {
102
+ it('the prompts record, when advertised, validates against the v2 capabilities schema', async () => {
103
+ if (HTTP_SKIP) return softSkip('inapplicable', 'no target: OPENWOP_BASE_URL is unset');
104
+ let doc: Record<string, unknown> | null;
105
+ try { doc = await v2Discovery(); } catch { doc = null; }
106
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
107
+ const prompts = await familyAdvertised('prompts');
108
+ if (prompts === null) return softSkip('inapplicable', 'the host does not advertise prompts');
109
+ const v = record(prompts);
110
+ expect(v.ok, req(ID_RECORD, 'schemas/v2/capabilities.schema.json §prompts', `the prompts record MUST validate (closed, no supported seat, maxTemplateBytes ≤ 65536): ${v.errors}`)).toBe(true);
111
+ });
112
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * v2 — run execution bounds (`spec/v2/core/runs.md` §`run` section; RFC 0058).
3
+ * The v1 twin is `run-execution-bounds-shape`; the legs live in
4
+ * `lib/run-bounds-witness.ts`, and what differs between majors (the run path,
5
+ * the closed `configurable` shape, the event name) is the profile row.
6
+ *
7
+ * shape advertised `limits.maxRunDurationMs` (≥ 1000) and
8
+ * `limits.maxLoopIterations` (≥ 1) are well-formed;
9
+ * refused NEW at v2, unaided: `configurable.run.runTimeoutMs: 0` is refused
10
+ * `400 validation_error` at create (`runs.md`: "An out-of-range
11
+ * recursionLimit or runTimeoutMs MUST return 400");
12
+ * breach the `conformance-run-duration-breach` fixture with a 1 s timeout
13
+ * ends `failed` / `run_timeout` and emits `cap.breached
14
+ * { kind: 'run-duration' }` with `observed > limit`.
15
+ *
16
+ * Dispositions: `limits` not advertised, or neither facet ⇒ shape
17
+ * `inapplicable`; no advertised fixture to name ⇒ refused `inapplicable`; the
18
+ * breach fixture unadvertised ⇒ breach `inapplicable` (the fixture is the
19
+ * opt-in, as for the budget and table-schema witnesses).
20
+ *
21
+ * Proven against a scratch double in `lib/run-bounds-witness.test.ts`.
22
+ *
23
+ * @see spec/v2/core/runs.md §run section, §Limits
24
+ */
25
+
26
+ import { describe, it, expect } from 'vitest';
27
+ import { familyAdvertised, v2Discovery } from '../lib/v2.js';
28
+ import { softSkip } from '../lib/soft-skip.js';
29
+ import { req } from '../lib/requirement-ids.js';
30
+ import { getAdvertisedFixtures } from '../lib/fixtures.js';
31
+ import { majorProfile } from '../lib/major-profile.js';
32
+ import { driveBreach, judgeBreach, refusedLeg, shapeLeg, TIMEOUT_FIXTURE } from '../lib/run-bounds-witness.js';
33
+
34
+ const PROFILE = majorProfile(2);
35
+ const ID_SHAPE = 'openwop.requirement.runs.limits-bounds-shape';
36
+ const ID_REFUSED = 'openwop.requirement.runs.run-timeout-out-of-range-refused';
37
+ const ID_BREACH = 'openwop.requirement.runs.run-timeout-breach';
38
+
39
+ /** Any advertised fixture names a valid workflow, so a refusal can only be about runTimeoutMs. */
40
+ function aFixture(): string | undefined {
41
+ const all = [...(getAdvertisedFixtures() ?? [])].sort();
42
+ return all.includes('conformance-noop') ? 'conformance-noop' : all.includes(TIMEOUT_FIXTURE) ? TIMEOUT_FIXTURE : all[0];
43
+ }
44
+
45
+ describe('v2 run execution bounds (runs.md §run section)', () => {
46
+ it('advertised limits.maxRunDurationMs and maxLoopIterations are well-formed', async () => {
47
+ let doc: Record<string, unknown> | null;
48
+ try { doc = await v2Discovery(); } catch { doc = null; }
49
+ if (!doc) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 JSON under OpenWOP-Version: 2.0');
50
+ if (!(await familyAdvertised('limits'))) return softSkip('inapplicable', 'the host does not advertise limits');
51
+ const out = shapeLeg(PROFILE, doc);
52
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
53
+ for (const x of out.findings) expect(x.ok, req(ID_SHAPE, x.doc, x.message)).toBe(true);
54
+ });
55
+
56
+ it('an out-of-range runTimeoutMs is refused 400 validation_error at create', async () => {
57
+ const out = await refusedLeg(PROFILE, aFixture());
58
+ if (out.kind === 'skip') return softSkip(out.disposition, out.reason);
59
+ for (const x of out.findings) expect(x.ok, req(ID_REFUSED, x.doc, x.message)).toBe(true);
60
+ });
61
+
62
+ it('a run exceeding runTimeoutMs fails run_timeout with cap.breached { kind: run-duration }', async () => {
63
+ const o = await driveBreach(PROFILE);
64
+ if ('kind' in o) return softSkip(o.disposition, o.reason);
65
+ for (const x of judgeBreach(o)) expect(x.ok, req(ID_BREACH, x.doc, x.message)).toBe(true);
66
+ }, 60_000);
67
+ });