@openwop/openwop-conformance 1.136.1 → 1.136.3

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.
@@ -542,6 +542,31 @@ export const PROFILE_FLOOR_SCENARIOS = {
542
542
  // `profiles.md` §`openwop-trigger-bridge`: "The runtime conformance scenarios
543
543
  // (`trigger-bridge-delivery.test.ts`, profile-gated) verify …" plus the shape.
544
544
  'openwop-trigger-bridge': { required: ['trigger-bridge-shape.test.ts', 'trigger-bridge-delivery.test.ts'] },
545
+ // `agent-platform-profile.md` §C — the operational annex's aggregate-evidence
546
+ // rule: the platform claim is BACKED BY the constituent capabilities' runtime
547
+ // scenarios, never asserted on shape alone. S42 (2026-08-17): the profile was
548
+ // pushed onto `claimedProfiles` by the discovery predicate but had NO entry
549
+ // here, so `requirementsFor()` returned null and the verdict was always
550
+ // "NOT certifiable (no floor defined)" — a host on which every `agent-*`
551
+ // scenario passed could never certify. Floor = the §C list; replay is
552
+ // discovery-conditional ("or the host's declared nondeterminism").
553
+ 'openwop-agent-platform': {
554
+ required: [
555
+ 'agent-platform-aggregate-evidence.test.ts',
556
+ 'agent-manifest-runtime.test.ts',
557
+ 'agent-live-runtime-shape.test.ts',
558
+ 'tool-descriptor-shape.test.ts',
559
+ 'tool-hooks-shape.test.ts',
560
+ 'safefetch-behavior.test.ts',
561
+ 'provider-usage.test.ts',
562
+ 'prompt-list-and-fetch.test.ts',
563
+ 'memory-capability-model-shape.test.ts',
564
+ 'feedback-capability-shape.test.ts',
565
+ ],
566
+ conditional: [
567
+ { path: 'replay.modes', includes: 'replay', required: ['replayDeterminism.test.ts'] },
568
+ ],
569
+ },
545
570
  // `profiles.md` §`openwop-replay-fork`: "This profile gates
546
571
  // `replayDeterminism.test.ts` and `replay-fork.test.ts` … the conformance
547
572
  // scenarios pass on whichever mode the host advertises." Discovery-conditional
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwop/openwop-conformance",
3
- "version": "1.136.1",
3
+ "version": "1.136.3",
4
4
  "description": "Production-ready black-box conformance suite for OpenWOP v1.0 compliant servers.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "_comment": "Provenance of this vendored schemas/ copy. See conformance/README.md \u00a7\"Resolving the contract\". Compare against the stamp in your installed @openwop/openwop-conformance to detect a stale hand-copied contract.",
3
- "suiteVersion": "1.136.1",
4
- "corpusCommit": "9db354d7a8215e24252beda93be717a6413a02df"
3
+ "suiteVersion": "1.136.3",
4
+ "corpusCommit": "54f29548af7c1b388553b982a6819ecd7d7ae4d9"
5
5
  }
@@ -488,6 +488,10 @@
488
488
  "requiresApproval": {
489
489
  "type": "boolean",
490
490
  "description": "RFC 0151 §C — an inverse action can itself be harmful (RFC 0147 R9), so it may be gated behind the same approval surface as a forward effect."
491
+ },
492
+ "waiveRequiresApproval": {
493
+ "type": "boolean",
494
+ "description": "RFC 0151 §B/§E (S36, 2026-08-17) — OPTIONAL. When true, ABANDONING this inverse action (§E `skip with justification` or `terminate as uncompensated`) requires an RFC 0051 approval to have resolved `approved` first — if a human must authorize RUNNING an inverse, a human must authorize deciding it will NEVER run. DEFAULT = the obligation's EFFECTIVE `requiresApproval` (after the policy's `approvalScope` escalation, §B) — NOT false — so the field is purely additive: no existing document changes meaning. Stamped onto the obligation at MINT time like `requiresApproval` (a mid-flight redefinition cannot change who had to authorize). Does NOT apply to `substitute` (still an attempt to undo; a substitution is a new planVersion whose own `requiresApproval` governs it). No policy-level counterpart: because the default inherits the effective value, an `approvalScope` escalation escalates waives with it."
491
495
  }
492
496
  }
493
497
  },
@@ -498,11 +502,21 @@
498
502
  },
499
503
  "additionalProperties": false,
500
504
  "if": {
501
- "properties": { "irreversibleEffect": { "const": true } },
502
- "required": ["irreversibleEffect"]
505
+ "properties": {
506
+ "irreversibleEffect": {
507
+ "const": true
508
+ }
509
+ },
510
+ "required": [
511
+ "irreversibleEffect"
512
+ ]
503
513
  },
504
514
  "then": {
505
- "not": { "required": ["compensation"] }
515
+ "not": {
516
+ "required": [
517
+ "compensation"
518
+ ]
519
+ }
506
520
  }
507
521
  },
508
522
  "FragmentEdge": {
@@ -291,6 +291,10 @@
291
291
  "requiresApproval": {
292
292
  "type": "boolean",
293
293
  "description": "RFC 0151 \u00a7C \u2014 an inverse action can itself be harmful (RFC 0147 R9), so it may be gated behind the same approval surface as a forward effect."
294
+ },
295
+ "waiveRequiresApproval": {
296
+ "type": "boolean",
297
+ "description": "RFC 0151 \u00a7B/\u00a7E (S36, 2026-08-17) \u2014 OPTIONAL. When true, ABANDONING this inverse action (\u00a7E `skip with justification` or `terminate as uncompensated`) requires an RFC 0051 approval to have resolved `approved` first \u2014 if a human must authorize RUNNING an inverse, a human must authorize deciding it will NEVER run. DEFAULT = the obligation's EFFECTIVE `requiresApproval` (after the policy's `approvalScope` escalation, \u00a7B) \u2014 NOT false \u2014 so the field is purely additive: no existing document changes meaning. Stamped onto the obligation at MINT time like `requiresApproval` (a mid-flight redefinition cannot change who had to authorize). Does NOT apply to `substitute` (still an attempt to undo; a substitution is a new planVersion whose own `requiresApproval` governs it). No policy-level counterpart: because the default inherits the effective value, an `approvalScope` escalation escalates waives with it."
294
298
  }
295
299
  }
296
300
  },
@@ -301,11 +305,21 @@
301
305
  },
302
306
  "additionalProperties": false,
303
307
  "if": {
304
- "properties": { "irreversibleEffect": { "const": true } },
305
- "required": ["irreversibleEffect"]
308
+ "properties": {
309
+ "irreversibleEffect": {
310
+ "const": true
311
+ }
312
+ },
313
+ "required": [
314
+ "irreversibleEffect"
315
+ ]
306
316
  },
307
317
  "then": {
308
- "not": { "required": ["compensation"] }
318
+ "not": {
319
+ "required": [
320
+ "compensation"
321
+ ]
322
+ }
309
323
  }
310
324
  },
311
325
  "WorkflowEdge": {
@@ -828,7 +842,7 @@
828
842
  },
829
843
  "compensation": {
830
844
  "$ref": "compensation-policy.schema.json",
831
- "description": "RFC 0151 §B the workflow-level compensation policy (WHEN an unwind starts and HOW it runs). OPTIONAL and capability-gated: a host that advertises `capabilities.compensation` MUST validate it at registration; a host that does not MUST refuse the workflow with `capability_required` rather than accept a policy it will never honour. Node-level `nodes[].compensation` declarations remain valid without it they describe the inverse action; this key requests the unwind."
845
+ "description": "RFC 0151 \u00a7B \u2014 the workflow-level compensation policy (WHEN an unwind starts and HOW it runs). OPTIONAL and capability-gated: a host that advertises `capabilities.compensation` MUST validate it at registration; a host that does not MUST refuse the workflow with `capability_required` rather than accept a policy it will never honour. Node-level `nodes[].compensation` declarations remain valid without it \u2014 they describe the inverse action; this key requests the unwind."
832
846
  }
833
847
  }
834
848
  }
@@ -24,9 +24,16 @@ const SEED_FIXTURE = 'conformance-noop';
24
24
 
25
25
  /** Seeds a run via the basic `conformance-noop` fixture; null (soft-skip)
26
26
  * when the fixture isn't advertised or creation fails. */
27
- export async function seedRun(tenantId: string): Promise<string | null> {
27
+ export async function seedRun(_label: string): Promise<string | null> {
28
28
  if (!isFixtureAdvertised(SEED_FIXTURE)) return null;
29
- const r = await driver.post('/v1/runs', { workflowId: SEED_FIXTURE, tenantId, inputs: {} });
29
+ // S41 (2026-08-17): the argument used to be sent as `tenantId` on POST /v1/runs. It was
30
+ // a fabricated label (`mem-attr-emit`, `feedback-cti`, …), and a host that enforces
31
+ // "tenantId MUST match the principal's accessible workspaces" answers 403 — MyndHyve did,
32
+ // which left every scenario on this helper `blocked` and kept `openwop-memory` from
33
+ // certifying. No caller ever needed a SECOND tenant: every leg reads back its own run.
34
+ // The tenant is the credential's own tenant (the host defaults it from the API key —
35
+ // rest-endpoints.md), which is what "a run the bearer provably owns" means.
36
+ const r = await driver.post('/v1/runs', { workflowId: SEED_FIXTURE, inputs: {} });
30
37
  if (r.status !== 200 && r.status !== 201) return null;
31
38
  return (r.json as { runId?: string } | undefined)?.runId ?? null;
32
39
  }
@@ -24,9 +24,16 @@ const SEED_FIXTURE = 'conformance-noop';
24
24
 
25
25
  /** Seeds a basic run (the host writes a run-summary on completion); null
26
26
  * (soft-skip) when the fixture isn't advertised or creation fails. */
27
- export async function seedRun(tenantId: string): Promise<string | null> {
27
+ export async function seedRun(_label: string): Promise<string | null> {
28
28
  if (!isFixtureAdvertised(SEED_FIXTURE)) return null;
29
- const r = await driver.post('/v1/runs', { workflowId: SEED_FIXTURE, tenantId, inputs: {} });
29
+ // S41 (2026-08-17): the argument used to be sent as `tenantId` on POST /v1/runs. It was
30
+ // a fabricated label (`mem-attr-emit`, `feedback-cti`, …), and a host that enforces
31
+ // "tenantId MUST match the principal's accessible workspaces" answers 403 — MyndHyve did,
32
+ // which left every scenario on this helper `blocked` and kept `openwop-memory` from
33
+ // certifying. No caller ever needed a SECOND tenant: every leg reads back its own run.
34
+ // The tenant is the credential's own tenant (the host defaults it from the API key —
35
+ // rest-endpoints.md), which is what "a run the bearer provably owns" means.
36
+ const r = await driver.post('/v1/runs', { workflowId: SEED_FIXTURE, inputs: {} });
30
37
  if (r.status !== 200 && r.status !== 201) return null;
31
38
  return (r.json as { runId?: string } | undefined)?.runId ?? null;
32
39
  }
@@ -621,6 +621,32 @@ export const PROFILE_FLOOR_SCENARIOS: Readonly<Record<string, ProfileFloor>> = {
621
621
  // (`trigger-bridge-delivery.test.ts`, profile-gated) verify …" plus the shape.
622
622
  'openwop-trigger-bridge': { required: ['trigger-bridge-shape.test.ts', 'trigger-bridge-delivery.test.ts'] },
623
623
 
624
+ // `agent-platform-profile.md` §C — the operational annex's aggregate-evidence
625
+ // rule: the platform claim is BACKED BY the constituent capabilities' runtime
626
+ // scenarios, never asserted on shape alone. S42 (2026-08-17): the profile was
627
+ // pushed onto `claimedProfiles` by the discovery predicate but had NO entry
628
+ // here, so `requirementsFor()` returned null and the verdict was always
629
+ // "NOT certifiable (no floor defined)" — a host on which every `agent-*`
630
+ // scenario passed could never certify. Floor = the §C list; replay is
631
+ // discovery-conditional ("or the host's declared nondeterminism").
632
+ 'openwop-agent-platform': {
633
+ required: [
634
+ 'agent-platform-aggregate-evidence.test.ts',
635
+ 'agent-manifest-runtime.test.ts',
636
+ 'agent-live-runtime-shape.test.ts',
637
+ 'tool-descriptor-shape.test.ts',
638
+ 'tool-hooks-shape.test.ts',
639
+ 'safefetch-behavior.test.ts',
640
+ 'provider-usage.test.ts',
641
+ 'prompt-list-and-fetch.test.ts',
642
+ 'memory-capability-model-shape.test.ts',
643
+ 'feedback-capability-shape.test.ts',
644
+ ],
645
+ conditional: [
646
+ { path: 'replay.modes', includes: 'replay', required: ['replayDeterminism.test.ts'] },
647
+ ],
648
+ },
649
+
624
650
  // `profiles.md` §`openwop-replay-fork`: "This profile gates
625
651
  // `replayDeterminism.test.ts` and `replay-fork.test.ts` … the conformance
626
652
  // scenarios pass on whichever mode the host advertises." Discovery-conditional
@@ -23,16 +23,20 @@
23
23
  import { describe, it, expect } from 'vitest';
24
24
  import { readFileSync } from 'node:fs';
25
25
  import { join } from 'node:path';
26
- import { SCHEMAS_DIR } from '../lib/paths.js';
26
+ import { V1_DIR } from '../lib/paths.js';
27
27
  import { behaviorGate } from '../lib/behavior-gate.js';
28
28
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
29
29
 
30
30
  const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
31
- const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
31
+ // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
32
+ // from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
33
+ // MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
34
+ // repo-layout only: `null` in the published layout and skipped, never thrown.
35
+ const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
32
36
 
33
37
  describe('chain-subchain-unsupported-refused §A: corpus contract (RFC 0133, always-on)', () => {
34
- it('spec corpus pins sub_chain_unsupported as a 422 refusal (never flatten)', () => {
35
- const doc = readFileSync(CHAIN_DOC, 'utf8');
38
+ it.skipIf(CHAIN_DOC === null)('spec corpus pins sub_chain_unsupported as a 422 refusal (never flatten)', () => {
39
+ const doc = readFileSync(CHAIN_DOC as string, 'utf8');
36
40
  expect(doc.includes('sub_chain_unsupported'), cite('§Error codes', 'the code MUST be registered')).toBe(true);
37
41
  // The normative refusal-not-flatten rule MUST appear in the composition section.
38
42
  expect(
@@ -124,6 +124,14 @@ describe('RFC 0151 §B — node compensation declaration', () => {
124
124
  expect(ok, JSON.stringify(validate.errors)).toBe(true);
125
125
  });
126
126
 
127
+ it('waiveRequiresApproval (S36) is an OPTIONAL boolean in the closed §B block — absent is valid (default = effective requiresApproval), non-boolean is refused', () => {
128
+ const decl = { nodeTypeId: 'vendor.shop.release', requiresApproval: true };
129
+ expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: false } }), JSON.stringify(validate.errors)).toBe(true);
130
+ expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: true } }), JSON.stringify(validate.errors)).toBe(true);
131
+ expect(validate({ ...base, compensation: { ...decl } }), 'absent MUST validate — the default is the obligation\'s effective requiresApproval, so no existing document changes meaning').toBe(true);
132
+ expect(validate({ ...base, compensation: { ...decl, waiveRequiresApproval: 'yes' } }), 'compensation.md §B: a plain boolean, not a policy language').toBe(false);
133
+ });
134
+
127
135
  it('a node without compensation stays valid', () => {
128
136
  // The profile is optional. Absent MUST NOT become a validation error, or
129
137
  // every existing workflow in the corpus breaks.
@@ -22,14 +22,18 @@ import { readFileSync } from 'node:fs';
22
22
  import { join } from 'node:path';
23
23
  import Ajv2020 from 'ajv/dist/2020.js';
24
24
  import addFormats from 'ajv-formats';
25
- import { SCHEMAS_DIR } from '../lib/paths.js';
25
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
26
26
  import { behaviorGate } from '../lib/behavior-gate.js';
27
27
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
28
28
 
29
29
  const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
30
30
  const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
31
31
  const MANIFEST = join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json');
32
- const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
32
+ // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
33
+ // from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
34
+ // MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
35
+ // repo-layout only: `null` in the published layout and skipped, never thrown.
36
+ const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
33
37
 
34
38
  function loadSchema(path: string): Record<string, unknown> {
35
39
  return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
@@ -77,8 +81,8 @@ describe('edge-condition-truthy-falsy §A: corpus (RFC 0134, always-on)', () =>
77
81
  expect(validate(pack), cite('§EdgeCondition', `truthy/falsy edges validate: ${ajv.errorsText(validate.errors)}`)).toBe(true);
78
82
  });
79
83
 
80
- it('the spec documents the no-`right` + required-`left` truthy/falsy semantics', () => {
81
- const doc = readFileSync(CHAIN_DOC, 'utf8');
84
+ it.skipIf(CHAIN_DOC === null)('the spec documents the no-`right` + required-`left` truthy/falsy semantics', () => {
85
+ const doc = readFileSync(CHAIN_DOC as string, 'utf8');
82
86
  expect(doc.includes('truthy'), cite('§Edge-condition operators', 'documents truthy')).toBe(true);
83
87
  expect(
84
88
  /truthy[\s\S]{0,400}(no|without).{0,20}`?right`?/i.test(doc) || /(no|without).{0,20}`?right`?[\s\S]{0,400}truthy/i.test(doc),
@@ -40,7 +40,7 @@
40
40
  import { describe, it, expect } from 'vitest';
41
41
  import { readFileSync } from 'node:fs';
42
42
  import { join } from 'node:path';
43
- import { SCHEMAS_DIR } from '../lib/paths.js';
43
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
44
44
  import { driver } from '../lib/driver.js';
45
45
  import { behaviorGate } from '../lib/behavior-gate.js';
46
46
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
@@ -53,7 +53,11 @@ import {
53
53
 
54
54
  const CAPS = join(SCHEMAS_DIR, 'capabilities.schema.json');
55
55
  const MANIFEST = join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json');
56
- const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
56
+ // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
57
+ // from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
58
+ // MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
59
+ // repo-layout only: `null` in the published layout and skipped, never thrown.
60
+ const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
57
61
 
58
62
  /** Spec-cited assertion message. Unlike `driver.describe`, this does NOT load
59
63
  * env, so it is safe in always-on server-free legs (no OPENWOP_BASE_URL). */
@@ -240,8 +244,8 @@ describe('workflow-chain-deferred: schema + spec surface (always-on, server-free
240
244
  ).toBe(true);
241
245
  });
242
246
 
243
- it('the spec pins the error code + the per-run credentialRef (not plaintext) supply shape', () => {
244
- const spec = readFileSync(CHAIN_DOC, 'utf8');
247
+ it.skipIf(CHAIN_DOC === null)('the spec pins the error code + the per-run credentialRef (not plaintext) supply shape', () => {
248
+ const spec = readFileSync(CHAIN_DOC as string, 'utf8');
245
249
  expect(spec.includes('sensitive_param_not_deferrable'), 'error code MUST be documented').toBe(true);
246
250
  expect(
247
251
  spec.includes('credentialRef'),
@@ -20,11 +20,15 @@ import { readFileSync } from 'node:fs';
20
20
  import { join } from 'node:path';
21
21
  import Ajv2020 from 'ajv/dist/2020.js';
22
22
  import addFormats from 'ajv-formats';
23
- import { SCHEMAS_DIR } from '../lib/paths.js';
23
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
24
24
 
25
25
  const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
26
26
  const MANIFEST = join(SCHEMAS_DIR, 'workflow-chain-pack-manifest.schema.json');
27
- const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
27
+ // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
28
+ // from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
29
+ // MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
30
+ // repo-layout only: `null` in the published layout and skipped, never thrown.
31
+ const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
28
32
 
29
33
  function packWith(internal: unknown): Record<string, unknown> {
30
34
  return {
@@ -65,8 +69,8 @@ describe('workflow-chain-internal-flag §A: corpus (RFC 0135, always-on)', () =>
65
69
  expect(validate(packWith('yes')), cite('§WorkflowChain', 'non-boolean internal rejected')).toBe(false);
66
70
  });
67
71
 
68
- it('the spec documents the MUST-omit-from-default-gallery + not-an-authz-boundary rules', () => {
69
- const doc = readFileSync(CHAIN_DOC, 'utf8');
72
+ it.skipIf(CHAIN_DOC === null)('the spec documents the MUST-omit-from-default-gallery + not-an-authz-boundary rules', () => {
73
+ const doc = readFileSync(CHAIN_DOC as string, 'utf8');
70
74
  expect(doc.includes('Chain visibility (RFC 0135)'), cite('§Chain visibility', 'section present')).toBe(true);
71
75
  expect(
72
76
  /internal[\s\S]{0,600}MUST omit/i.test(doc),
@@ -43,7 +43,7 @@ import { readFileSync } from 'node:fs';
43
43
  import { join } from 'node:path';
44
44
  import Ajv2020 from 'ajv/dist/2020.js';
45
45
  import addFormats from 'ajv-formats';
46
- import { SCHEMAS_DIR } from '../lib/paths.js';
46
+ import { SCHEMAS_DIR, V1_DIR } from '../lib/paths.js';
47
47
  import { behaviorGate } from '../lib/behavior-gate.js';
48
48
  import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
49
49
  import { isFixtureAdvertised } from '../lib/fixtures.js';
@@ -51,7 +51,12 @@ import { driver } from '../lib/driver.js';
51
51
 
52
52
  const cite = (section: string, requirement: string): string => `${section} — ${requirement}`;
53
53
  const WORKFLOW_DEF = join(SCHEMAS_DIR, 'workflow-definition.schema.json');
54
- const CHAIN_DOC = join(SCHEMAS_DIR, '..', 'spec', 'v1', 'workflow-chain-packs.md');
54
+ // S38 (2026-08-17): `spec/` is NOT in the published package (`files`), so a path built
55
+ // from SCHEMAS_DIR/../spec ENOENTs for every npm consumer — five always-on legs reddened
56
+ // MyndHyve's bundle for a reason that had nothing to do with the host. Prose legs are
57
+ // repo-layout only: `null` in the published layout and skipped, never thrown.
58
+ const CHAIN_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, 'workflow-chain-packs.md');
59
+ const RFC_DOC: string | null = V1_DIR === null ? null : join(V1_DIR, '..', '..', 'RFCS', '0136-workflow-variable-format.md');
55
60
 
56
61
  function loadSchema(path: string): Record<string, unknown> {
57
62
  return JSON.parse(readFileSync(path, 'utf8')) as Record<string, unknown>;
@@ -115,8 +120,8 @@ describe('workflow-variable-format §A: corpus (RFC 0136, always-on)', () => {
115
120
  ).toBe(true);
116
121
  });
117
122
 
118
- it('A4 — chain-pack spec documents deferred-mode `format` propagation as mode-scoped', () => {
119
- const doc = readFileSync(CHAIN_DOC, 'utf8');
123
+ it.skipIf(CHAIN_DOC === null)('A4 — chain-pack spec documents deferred-mode `format` propagation as mode-scoped', () => {
124
+ const doc = readFileSync(CHAIN_DOC as string, 'utf8');
120
125
  const step1 = doc.slice(doc.indexOf('Materialize parameters as variables'));
121
126
  expect(step1.length > 0, cite('§Deferred-parameter expansion', 'step 1 present')).toBe(true);
122
127
  expect(
@@ -129,8 +134,8 @@ describe('workflow-variable-format §A: corpus (RFC 0136, always-on)', () => {
129
134
  ).toBe(true);
130
135
  });
131
136
 
132
- it('A5 — the RFC forbids `format` participating in a `configurable` validation decision', () => {
133
- const rfc = readFileSync(join(SCHEMAS_DIR, '..', 'RFCS', '0136-workflow-variable-format.md'), 'utf8');
137
+ it.skipIf(RFC_DOC === null)('A5 — the RFC forbids `format` participating in a `configurable` validation decision', () => {
138
+ const rfc = readFileSync(RFC_DOC as string, 'utf8');
134
139
  expect(
135
140
  /configurableSchema/.test(rfc),
136
141
  cite('RFC 0136', 'names the configurableSchema propagation path'),