@cleocode/contracts 2026.6.12 → 2026.6.13

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 (136) hide show
  1. package/dist/__tests__/operation-def.test.js +2 -1
  2. package/dist/__tests__/operation-def.test.js.map +1 -1
  3. package/dist/__tests__/sealed-credential.test.d.ts +19 -0
  4. package/dist/__tests__/sealed-credential.test.d.ts.map +1 -0
  5. package/dist/__tests__/sealed-credential.test.js +74 -0
  6. package/dist/__tests__/sealed-credential.test.js.map +1 -0
  7. package/dist/__tests__/skill-executor-contract.test.js +3 -0
  8. package/dist/__tests__/skill-executor-contract.test.js.map +1 -1
  9. package/dist/acceptance-gate.d.ts +2 -3
  10. package/dist/acceptance-gate.d.ts.map +1 -1
  11. package/dist/boundary.d.ts.map +1 -1
  12. package/dist/boundary.js +1 -19
  13. package/dist/boundary.js.map +1 -1
  14. package/dist/config.d.ts +102 -0
  15. package/dist/config.d.ts.map +1 -1
  16. package/dist/data-accessor.d.ts +21 -1
  17. package/dist/data-accessor.d.ts.map +1 -1
  18. package/dist/data-accessor.js.map +1 -1
  19. package/dist/dispatch/identity.d.ts +1 -1
  20. package/dist/dispatch/identity.d.ts.map +1 -1
  21. package/dist/dispatch/identity.js +5 -0
  22. package/dist/dispatch/identity.js.map +1 -1
  23. package/dist/dispatch/operations-registry.d.ts.map +1 -1
  24. package/dist/dispatch/operations-registry.js +46 -168
  25. package/dist/dispatch/operations-registry.js.map +1 -1
  26. package/dist/docs/read.d.ts +2 -2
  27. package/dist/index.d.ts +12 -7
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/lease-ipc/__tests__/freeze.test.d.ts +15 -0
  32. package/dist/lease-ipc/__tests__/freeze.test.d.ts.map +1 -0
  33. package/dist/lease-ipc/__tests__/freeze.test.js +221 -0
  34. package/dist/lease-ipc/__tests__/freeze.test.js.map +1 -0
  35. package/dist/lease-ipc/index.d.ts +17 -0
  36. package/dist/lease-ipc/index.d.ts.map +1 -0
  37. package/dist/lease-ipc/index.js +16 -0
  38. package/dist/lease-ipc/index.js.map +1 -0
  39. package/dist/lease-ipc/messages.d.ts +754 -0
  40. package/dist/lease-ipc/messages.d.ts.map +1 -0
  41. package/dist/lease-ipc/messages.js +383 -0
  42. package/dist/lease-ipc/messages.js.map +1 -0
  43. package/dist/lease-ipc/version.d.ts +41 -0
  44. package/dist/lease-ipc/version.d.ts.map +1 -0
  45. package/dist/lease-ipc/version.js +43 -0
  46. package/dist/lease-ipc/version.js.map +1 -0
  47. package/dist/llm/interfaces.d.ts +23 -0
  48. package/dist/llm/interfaces.d.ts.map +1 -1
  49. package/dist/llm/sealed-credential.d.ts +164 -0
  50. package/dist/llm/sealed-credential.d.ts.map +1 -0
  51. package/dist/llm/sealed-credential.js +43 -0
  52. package/dist/llm/sealed-credential.js.map +1 -0
  53. package/dist/llm/system-of-use.d.ts +383 -0
  54. package/dist/llm/system-of-use.d.ts.map +1 -0
  55. package/dist/llm/system-of-use.js +238 -0
  56. package/dist/llm/system-of-use.js.map +1 -0
  57. package/dist/llm/system-resolver.d.ts +45 -11
  58. package/dist/llm/system-resolver.d.ts.map +1 -1
  59. package/dist/llm/system-resolver.js +14 -7
  60. package/dist/llm/system-resolver.js.map +1 -1
  61. package/dist/operations/__tests__/ensures-schema-registry.test.d.ts +17 -0
  62. package/dist/operations/__tests__/ensures-schema-registry.test.d.ts.map +1 -0
  63. package/dist/operations/__tests__/ensures-schema-registry.test.js +113 -0
  64. package/dist/operations/__tests__/ensures-schema-registry.test.js.map +1 -0
  65. package/dist/operations/ensures-schema-registry.d.ts +172 -0
  66. package/dist/operations/ensures-schema-registry.d.ts.map +1 -0
  67. package/dist/operations/ensures-schema-registry.js +226 -0
  68. package/dist/operations/ensures-schema-registry.js.map +1 -0
  69. package/dist/operations/llm.d.ts +101 -9
  70. package/dist/operations/llm.d.ts.map +1 -1
  71. package/dist/operations/validate.d.ts +1 -12
  72. package/dist/operations/validate.d.ts.map +1 -1
  73. package/dist/operations/validate.js +1 -1
  74. package/dist/playbook.d.ts +60 -0
  75. package/dist/playbook.d.ts.map +1 -1
  76. package/dist/session.d.ts +11 -0
  77. package/dist/session.d.ts.map +1 -1
  78. package/dist/session.js.map +1 -1
  79. package/dist/templates/manifest.d.ts +1 -1
  80. package/dist/tools/agent-tools.d.ts +231 -0
  81. package/dist/tools/agent-tools.d.ts.map +1 -0
  82. package/dist/tools/agent-tools.js +25 -0
  83. package/dist/tools/agent-tools.js.map +1 -0
  84. package/dist/tools/atomic.d.ts +19 -0
  85. package/dist/tools/atomic.d.ts.map +1 -1
  86. package/dist/tools/atomic.js +17 -0
  87. package/dist/tools/atomic.js.map +1 -1
  88. package/dist/tools/index.d.ts +2 -0
  89. package/dist/tools/index.d.ts.map +1 -1
  90. package/dist/tools/skill-executor.d.ts +7 -0
  91. package/dist/tools/skill-executor.d.ts.map +1 -1
  92. package/dist/tools/web-tools.d.ts +217 -0
  93. package/dist/tools/web-tools.d.ts.map +1 -0
  94. package/dist/tools/web-tools.js +31 -0
  95. package/dist/tools/web-tools.js.map +1 -0
  96. package/package.json +6 -2
  97. package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
  98. package/src/__tests__/operation-def.test.ts +2 -1
  99. package/src/__tests__/sealed-credential.test.ts +100 -0
  100. package/src/__tests__/skill-executor-contract.test.ts +3 -0
  101. package/src/acceptance-gate.ts +2 -3
  102. package/src/boundary.ts +1 -20
  103. package/src/config.ts +104 -0
  104. package/src/data-accessor.ts +22 -1
  105. package/src/dispatch/identity.ts +5 -0
  106. package/src/dispatch/operations-registry.ts +51 -170
  107. package/src/index.ts +103 -24
  108. package/src/lease-ipc/__tests__/freeze.test.ts +246 -0
  109. package/src/lease-ipc/index.ts +63 -0
  110. package/src/lease-ipc/messages.ts +471 -0
  111. package/src/lease-ipc/version.ts +46 -0
  112. package/src/llm/interfaces.ts +23 -0
  113. package/src/llm/sealed-credential.ts +166 -0
  114. package/src/llm/system-of-use.ts +503 -0
  115. package/src/llm/system-resolver.ts +46 -11
  116. package/src/operations/__tests__/ensures-schema-registry.test.ts +169 -0
  117. package/src/operations/ensures-schema-registry.ts +260 -0
  118. package/src/operations/llm.ts +105 -8
  119. package/src/operations/validate.ts +1 -14
  120. package/src/playbook.ts +61 -0
  121. package/src/session.ts +11 -0
  122. package/src/tools/agent-tools.ts +270 -0
  123. package/src/tools/atomic.ts +21 -0
  124. package/src/tools/index.ts +46 -0
  125. package/src/tools/skill-executor.ts +7 -0
  126. package/src/tools/web-tools.ts +247 -0
  127. package/dist/tessera.d.ts +0 -32
  128. package/dist/tessera.d.ts.map +0 -1
  129. package/dist/tessera.js +0 -11
  130. package/dist/tessera.js.map +0 -1
  131. package/dist/warp-chain.d.ts +0 -129
  132. package/dist/warp-chain.d.ts.map +0 -1
  133. package/dist/warp-chain.js +0 -13
  134. package/dist/warp-chain.js.map +0 -1
  135. package/src/tessera.ts +0 -35
  136. package/src/warp-chain.ts +0 -185
@@ -1,11 +1,18 @@
1
1
  /**
2
- * SystemOfUse contract types for the `resolveLLMForSystem` chokepoint (E9).
2
+ * `SystemOfUseLabel` contract types for the `resolveLLMForSystem` chokepoint (E9).
3
3
  *
4
- * A "system of use" is the semantic label for the subsystem that is requesting
5
- * an LLM client — e.g. `'sentient'`, `'memory'`, `'task-executor'`. It is
6
- * a higher-level, stable identity that CLEO maps to a {@link RoleName} for
7
- * config resolution. This indirection insulates call-sites from the config
8
- * vocabulary (roles) and from model/provider churn.
4
+ * A "system of use label" is the flat, ergonomic vocabulary the E9 resolver
5
+ * accepts for the seven role-mapped background subsystems — e.g. `'sentient'`,
6
+ * `'memory'`, `'task-executor'`. It is a higher-level, stable identity that
7
+ * CLEO maps to a {@link RoleName} for config resolution. This indirection
8
+ * insulates call-sites from the config vocabulary (roles) and from
9
+ * model/provider churn.
10
+ *
11
+ * The richer, structured `{ kind, id }` taxonomy that covers the OTHER routing
12
+ * axes (orchestration tiers, tools, skills, cantbook nodes, spawn units) lives
13
+ * in {@link import('./system-of-use.js').SystemOfUse} (T11747). This label set
14
+ * is the flat resolver-input subset; both are kept in lock-step (the label set
15
+ * mirrors the `aux` axis plus `default`).
9
16
  *
10
17
  * `resolveLLMForSystem` is the single DRY chokepoint that the 4-resolver /
11
18
  * 3-picker sprawl will collapse onto (E9 · T11745).
@@ -16,15 +23,21 @@
16
23
  */
17
24
 
18
25
  import type { RoleName } from '../config.js';
26
+ import type { RoleSystem } from './system-of-use.js';
19
27
 
20
28
  /**
21
- * Semantic label for a CLEO subsystem that requests an LLM client.
29
+ * Flat, ergonomic label for a CLEO background subsystem that requests an LLM
30
+ * client through the E9 resolver.
22
31
  *
23
- * Each `SystemOfUse` value maps to a canonical {@link RoleName} used by
32
+ * Each `SystemOfUseLabel` value maps to a canonical {@link RoleName} used by
24
33
  * `resolveLLMForSystem` for config and credential resolution. Subsystems
25
34
  * MUST declare their identity so the resolver can apply the correct
26
35
  * per-system overrides and audit trails.
27
36
  *
37
+ * For the full structured taxonomy (tools/skills/cantbook nodes/spawn units/
38
+ * orchestration tiers), see the discriminated
39
+ * {@link import('./system-of-use.js').SystemOfUse} descriptor (T11747).
40
+ *
28
41
  * ### Role mapping (static default; overridable via config `systemRoles`)
29
42
  *
30
43
  * | System | Default role |
@@ -40,7 +53,7 @@ import type { RoleName } from '../config.js';
40
53
  *
41
54
  * @task T11749
42
55
  */
43
- export type SystemOfUse =
56
+ export type SystemOfUseLabel =
44
57
  | 'sentient'
45
58
  | 'memory'
46
59
  | 'task-executor'
@@ -51,13 +64,13 @@ export type SystemOfUse =
51
64
  | 'default';
52
65
 
53
66
  /**
54
- * Static mapping from {@link SystemOfUse} to the {@link RoleName} used for
67
+ * Static mapping from {@link SystemOfUseLabel} to the {@link RoleName} used for
55
68
  * config resolution. This is the canonical default; callers may override
56
69
  * individual systems via `config.llm.systemRoles[system]`.
57
70
  *
58
71
  * @task T11749
59
72
  */
60
- export const SYSTEM_ROLE_MAP: Readonly<Record<SystemOfUse, RoleName | null>> = {
73
+ export const SYSTEM_ROLE_MAP: Readonly<Record<SystemOfUseLabel, RoleName | null>> = {
61
74
  sentient: 'consolidation',
62
75
  memory: 'extraction',
63
76
  'task-executor': 'judgement',
@@ -69,6 +82,28 @@ export const SYSTEM_ROLE_MAP: Readonly<Record<SystemOfUse, RoleName | null>> = {
69
82
  default: null,
70
83
  } as const;
71
84
 
85
+ /**
86
+ * Input accepted by `resolveLLMForSystem()`.
87
+ *
88
+ * The chokepoint accepts EITHER:
89
+ *
90
+ * - the flat, ergonomic {@link SystemOfUseLabel} for the role-mapped background
91
+ * subsystems (`'sentient'`, `'memory'`, … — the original locked vocabulary), OR
92
+ * - a structured {@link RoleSystem} descriptor (`{ kind: 'role', id: RoleName }`)
93
+ * for a *direct* role resolution.
94
+ *
95
+ * The descriptor form makes the E9 equivalence
96
+ * `resolveLLMForRole(role) ≡ resolveLLMForSystem({ kind: 'role', id: role })`
97
+ * (T11750 · AC1) directly expressible at the chokepoint, so role callers can
98
+ * collapse onto the same single resolution path with ZERO duplicate resolution
99
+ * logic. Both forms funnel through the SAME `resolveLLMForRole` core inside
100
+ * `@cleocode/core`.
101
+ *
102
+ * @task T11750
103
+ * @epic T11745
104
+ */
105
+ export type SystemResolverInput = SystemOfUseLabel | RoleSystem;
106
+
72
107
  /**
73
108
  * Options accepted by `resolveLLMForSystem()`.
74
109
  *
@@ -0,0 +1,169 @@
1
+ /**
2
+ * Tests for the ensures-schema Zod registry (T11762 ST-1 · T11900).
3
+ *
4
+ * Behavior-parity guard: every input the deleted bespoke validators
5
+ * (`validateDecompositionTaskTree` / `validateIvtrEvidenceOutput`,
6
+ * `packages/playbooks/src/runtime.ts:794-902`) REJECTED must still be rejected
7
+ * here, with the FIRST Zod issue message equal to the SUFFIX of the original
8
+ * violation string (the part after the `ensures.schema[<name>] on <nodeId>: `
9
+ * prefix that ST-2's runtime wrapper re-applies). Every input they ACCEPTED must
10
+ * still parse. The ACs call out: empty array, missing title, empty acceptance,
11
+ * `{}` evidence, numeric evidence.
12
+ *
13
+ * @task T11900
14
+ * @epic T11762
15
+ */
16
+
17
+ import { describe, expect, it } from 'vitest';
18
+ import {
19
+ ENSURES_SCHEMA_REGISTRY,
20
+ type EnsuresSchemaSpec,
21
+ evidenceSchema,
22
+ LEGACY_PASSTHROUGH_SCHEMA_NAMES,
23
+ passthroughSchema,
24
+ taskTreeSchema,
25
+ } from '../ensures-schema-registry.js';
26
+
27
+ /** First Zod issue message for a failing parse (or `undefined` if it parsed). */
28
+ function firstIssueMessage(schema: typeof taskTreeSchema, value: unknown): string | undefined {
29
+ const result = schema.safeParse(value);
30
+ return result.success ? undefined : result.error.issues[0]?.message;
31
+ }
32
+
33
+ describe('ensures-schema-registry — registry DATA', () => {
34
+ it('registers the two strict schemas plus every legacy passthrough name', () => {
35
+ // T11762 ST-2 (R1): the two strict ports (`task_tree`, `evidence`) PLUS the
36
+ // legacy starter `.cantbook` schema names — registered with passthrough so
37
+ // the fail-closed flip does not regress the shipped playbooks.
38
+ expect([...ENSURES_SCHEMA_REGISTRY.keys()].sort()).toEqual(
39
+ ['task_tree', 'evidence', ...LEGACY_PASSTHROUGH_SCHEMA_NAMES].sort(),
40
+ );
41
+ });
42
+
43
+ it('each strict spec carries name, contextKey (= name by default), and a schema', () => {
44
+ for (const name of ['task_tree', 'evidence'] as const) {
45
+ const spec = ENSURES_SCHEMA_REGISTRY.get(name) as EnsuresSchemaSpec;
46
+ expect(spec).toBeDefined();
47
+ expect(spec.name).toBe(name);
48
+ expect(spec.contextKey).toBe(name);
49
+ expect(typeof spec.schema.safeParse).toBe('function');
50
+ }
51
+ });
52
+
53
+ it('every legacy passthrough name is registered with the passthrough schema', () => {
54
+ // R1: passthrough accepts ANY value (including `undefined`) so the historical
55
+ // "silently skipped" behavior is preserved EXACTLY.
56
+ for (const name of LEGACY_PASSTHROUGH_SCHEMA_NAMES) {
57
+ const spec = ENSURES_SCHEMA_REGISTRY.get(name) as EnsuresSchemaSpec;
58
+ expect(spec).toBeDefined();
59
+ expect(spec.name).toBe(name);
60
+ expect(spec.schema).toBe(passthroughSchema);
61
+ expect(spec.schema.safeParse(undefined).success).toBe(true);
62
+ expect(spec.schema.safeParse({ anything: 1 }).success).toBe(true);
63
+ expect(spec.schema.safeParse(42).success).toBe(true);
64
+ }
65
+ });
66
+ });
67
+
68
+ describe('ensures-schema-registry — task_tree (ports validateDecompositionTaskTree)', () => {
69
+ it('accepts a minimal valid task tree', () => {
70
+ expect(taskTreeSchema.safeParse([{ title: 'Build X', acceptance: ['does X'] }]).success).toBe(
71
+ true,
72
+ );
73
+ });
74
+
75
+ it('accepts a tree with optional id/parentId/depends populated', () => {
76
+ expect(
77
+ taskTreeSchema.safeParse([
78
+ { title: 'Root', acceptance: ['ac1'], id: 'T1' },
79
+ { title: 'Child', acceptance: ['ac2', 'ac3'], id: 'T2', parentId: 'T1', depends: ['T1'] },
80
+ ]).success,
81
+ ).toBe(true);
82
+ });
83
+
84
+ it('rejects a non-array (parity: "task_tree must be a non-empty array")', () => {
85
+ expect(firstIssueMessage(taskTreeSchema, 'not-an-array')).toBe(
86
+ 'task_tree must be a non-empty array',
87
+ );
88
+ });
89
+
90
+ it('rejects an EMPTY ARRAY (parity: empty-array message)', () => {
91
+ expect(firstIssueMessage(taskTreeSchema, [])).toBe(
92
+ 'task_tree is an empty array — decomposition produced no tasks',
93
+ );
94
+ });
95
+
96
+ it('rejects a MISSING TITLE (parity: "title must be a non-empty string")', () => {
97
+ expect(firstIssueMessage(taskTreeSchema, [{ acceptance: ['ac1'] }])).toBe(
98
+ 'title must be a non-empty string',
99
+ );
100
+ });
101
+
102
+ it('rejects a whitespace-only title (parity: same non-empty-title message)', () => {
103
+ expect(firstIssueMessage(taskTreeSchema, [{ title: ' ', acceptance: ['ac1'] }])).toBe(
104
+ 'title must be a non-empty string',
105
+ );
106
+ });
107
+
108
+ it('rejects an EMPTY ACCEPTANCE array (parity: non-empty-acceptance message)', () => {
109
+ expect(firstIssueMessage(taskTreeSchema, [{ title: 'X', acceptance: [] }])).toBe(
110
+ 'must have a non-empty acceptance array',
111
+ );
112
+ });
113
+
114
+ it('rejects an acceptance array with no non-empty strings (parity message)', () => {
115
+ expect(firstIssueMessage(taskTreeSchema, [{ title: 'X', acceptance: [' ', ''] }])).toBe(
116
+ 'acceptance array contains no non-empty strings',
117
+ );
118
+ });
119
+ });
120
+
121
+ describe('ensures-schema-registry — evidence (ports validateIvtrEvidenceOutput)', () => {
122
+ it('accepts a non-empty string', () => {
123
+ expect(evidenceSchema.safeParse('commit:abc123').success).toBe(true);
124
+ });
125
+
126
+ it('accepts a non-empty array', () => {
127
+ expect(evidenceSchema.safeParse(['atom']).success).toBe(true);
128
+ });
129
+
130
+ it('accepts an object with at least one key', () => {
131
+ expect(evidenceSchema.safeParse({ commit: 'abc' }).success).toBe(true);
132
+ });
133
+
134
+ it('rejects null/undefined (parity: must-be-present message)', () => {
135
+ expect(firstIssueMessage(evidenceSchema, null)).toBe(
136
+ 'evidence must be present (non-null, non-undefined)',
137
+ );
138
+ expect(firstIssueMessage(evidenceSchema, undefined)).toBe(
139
+ 'evidence must be present (non-null, non-undefined)',
140
+ );
141
+ });
142
+
143
+ it('rejects an empty / whitespace-only string (parity message)', () => {
144
+ expect(firstIssueMessage(evidenceSchema, '')).toBe('evidence string must not be empty');
145
+ expect(firstIssueMessage(evidenceSchema, ' ')).toBe('evidence string must not be empty');
146
+ });
147
+
148
+ it('rejects an empty array (parity message)', () => {
149
+ expect(firstIssueMessage(evidenceSchema, [])).toBe('evidence array must not be empty');
150
+ });
151
+
152
+ it('rejects an EMPTY OBJECT {} (parity: at-least-one-key message)', () => {
153
+ expect(firstIssueMessage(evidenceSchema, {})).toBe(
154
+ 'evidence object must have at least one key (got {})',
155
+ );
156
+ });
157
+
158
+ it('rejects NUMERIC evidence (parity: wrong-type message with typeof)', () => {
159
+ expect(firstIssueMessage(evidenceSchema, 42)).toBe(
160
+ 'evidence must be a string, array, or object (got number)',
161
+ );
162
+ });
163
+
164
+ it('rejects boolean evidence (parity: wrong-type message with typeof)', () => {
165
+ expect(firstIssueMessage(evidenceSchema, true)).toBe(
166
+ 'evidence must be a string, array, or object (got boolean)',
167
+ );
168
+ });
169
+ });
@@ -0,0 +1,260 @@
1
+ /**
2
+ * Ensures-schema Zod registry — the cantbook `ensures.schema` name → Zod
3
+ * validator SSoT (T11762 ST-1 · Lane A).
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * The playbook runtime (`packages/playbooks/src/runtime.ts`) validates a node's
8
+ * post-merge `context` against a named shape declared as `ensures.schema:` in a
9
+ * `.cantbook`. Historically the runtime HARDCODED two bespoke validators
10
+ * (`validateDecompositionTaskTree` / `validateIvtrEvidenceOutput`) in an
11
+ * `if (schema === 'task_tree') … else if (=== 'evidence')` block, and silently
12
+ * SKIPPED any other schema name. T11762 replaces that hardcode with a
13
+ * registry-driven lookup so:
14
+ *
15
+ * - `ensures.schema` names resolve to a registered Zod validator (AC1).
16
+ * - the runtime no longer hardcodes `'task_tree'` / `'evidence'` (AC2).
17
+ * - an unknown schema name FAILS CLOSED instead of silently passing.
18
+ *
19
+ * This module holds ONLY the Gate-10 (`lint-no-runtime-in-contracts`)-ALLOWED
20
+ * kinds: the Zod schema **const values**, the {@link EnsuresSchemaSpec} **type**,
21
+ * and the registry **data** (a `ReadonlyMap` value). The bodied accessors
22
+ * (`getEnsuresSchema` / `listEnsuresSchemaNames` / `defineEnsuresSchema`) live in
23
+ * `@cleocode/core` (`packages/core/src/dispatch/contracts/ensures-schema.ts`,
24
+ * ST-1b) because a `Map.get` helper is a net-new runtime function that the
25
+ * contracts-purity gate forbids in this leaf package.
26
+ *
27
+ * ## Message parity (behavior preservation)
28
+ *
29
+ * The Zod schemas are crafted so that — for every input the deleted bespoke
30
+ * validators rejected — the FIRST Zod issue message equals the SUFFIX of the
31
+ * original human-readable violation string (the part after the
32
+ * `ensures.schema[<name>] on <nodeId>: ` prefix). ST-2's runtime wrapper
33
+ * re-applies that prefix, so the end-to-end violation strings are unchanged. The
34
+ * ST-1 parity test asserts on these issue messages directly.
35
+ *
36
+ * ## Genkit-forward naming
37
+ *
38
+ * The registry surface is deliberately Genkit-shaped so a future Genkit
39
+ * `ai.defineSchema(name, zodSchema)` bridge (T11768) is a thin adapter over this
40
+ * SSoT, never a competing source.
41
+ *
42
+ * @packageDocumentation
43
+ * @module @cleocode/contracts/operations/ensures-schema-registry
44
+ *
45
+ * @epic T11762 — E-OUTPUT-SCHEMA-ENFORCEMENT
46
+ * @task T11900 — ST-1: ensures-schema Zod registry DATA + schemas
47
+ * @see validateDecompositionTaskTree — the ported bespoke task_tree validator
48
+ * @see validateIvtrEvidenceOutput — the ported bespoke evidence validator
49
+ */
50
+
51
+ import { type ZodType, z } from 'zod';
52
+
53
+ /**
54
+ * One registered `ensures.schema` entry: the Zod validator plus which playbook
55
+ * `context` key holds the value to validate.
56
+ *
57
+ * @public
58
+ */
59
+ export interface EnsuresSchemaSpec {
60
+ /**
61
+ * The schema NAME used in `ensures.schema:` (e.g. `'task_tree'`, `'evidence'`).
62
+ */
63
+ readonly name: string;
64
+ /**
65
+ * The playbook `context` key whose value is validated. Defaults to {@link name}
66
+ * (matching today's `context['task_tree']` / `context['evidence']` convention).
67
+ */
68
+ readonly contextKey: string;
69
+ /**
70
+ * The Zod validator applied to `context[contextKey]`.
71
+ */
72
+ readonly schema: ZodType;
73
+ }
74
+
75
+ // ─── task_tree — ports validateDecompositionTaskTree (runtime.ts:794-855) ──────
76
+
77
+ /**
78
+ * Zod schema for a single `task_tree` entry.
79
+ *
80
+ * Parity rules ported from `validateDecompositionTaskTree`:
81
+ * - `title` must be a non-empty string (after trim).
82
+ * - `acceptance` must be a non-empty array containing at least one non-empty
83
+ * (after-trim) string.
84
+ * - `id` / `parentId` / `depends` are optional and not hard-validated (the
85
+ * bespoke validator was intentionally lenient on these).
86
+ *
87
+ * @remarks
88
+ * The `title` check is split so the message echoes the bespoke
89
+ * `entry[i].title must be a non-empty string` regardless of whether the field is
90
+ * absent, the wrong type, or whitespace-only.
91
+ */
92
+ export const taskTreeEntrySchema: ZodType = z.object({
93
+ title: z
94
+ .string({ message: 'title must be a non-empty string' })
95
+ .refine((s) => s.trim().length > 0, { message: 'title must be a non-empty string' }),
96
+ acceptance: z
97
+ .array(z.unknown(), { message: 'must have a non-empty acceptance array' })
98
+ .min(1, { message: 'must have a non-empty acceptance array' })
99
+ .refine((arr) => arr.some((s) => typeof s === 'string' && s.trim().length > 0), {
100
+ message: 'acceptance array contains no non-empty strings',
101
+ }),
102
+ id: z.string().optional(),
103
+ parentId: z.string().optional(),
104
+ depends: z.array(z.string()).optional(),
105
+ });
106
+
107
+ /**
108
+ * Zod schema for the `task_tree` context value emitted by a RCASD decomposition
109
+ * node — a non-empty array of {@link taskTreeEntrySchema} entries.
110
+ *
111
+ * Parity: an empty array is rejected with `task_tree is an empty array …`; a
112
+ * non-array is rejected by Zod's base array type error.
113
+ *
114
+ * @public
115
+ */
116
+ export const taskTreeSchema: ZodType = z
117
+ .array(taskTreeEntrySchema, { message: 'task_tree must be a non-empty array' })
118
+ .min(1, { message: 'task_tree is an empty array — decomposition produced no tasks' });
119
+
120
+ // ─── evidence — ports validateIvtrEvidenceOutput (runtime.ts:873-902) ──────────
121
+
122
+ /**
123
+ * Zod schema for the `evidence` context value emitted by an IVTR validation
124
+ * node.
125
+ *
126
+ * Parity rules ported from `validateIvtrEvidenceOutput`. Valid evidence is one
127
+ * of:
128
+ * - a non-empty (after-trim) string,
129
+ * - a non-empty array,
130
+ * - an object with at least one key.
131
+ *
132
+ * `null` / `undefined`, the empty string, the empty array, the empty object, and
133
+ * any other primitive (number, boolean) are rejected — matching the bespoke
134
+ * validator exactly.
135
+ *
136
+ * @remarks
137
+ * A `z.union` of the three valid shapes would reject the SAME set of inputs, but
138
+ * collapses every rejection to a generic `"Invalid input"` union error. To
139
+ * preserve the bespoke validator's branch-specific messages (`evidence must be
140
+ * present …`, `evidence string must not be empty`, `evidence array must not be
141
+ * empty`, `evidence object must have at least one key (got {})`, `evidence must
142
+ * be a string, array, or object (got <type>)`), this schema replicates the
143
+ * original branch order in a single `superRefine`. The `superRefine` callback is
144
+ * an inline argument — NOT an exported bodied function/arrow — so it does not
145
+ * trip the Gate-10 contracts-purity lint.
146
+ *
147
+ * @public
148
+ */
149
+ export const evidenceSchema: ZodType = z.unknown().superRefine((value, ctx) => {
150
+ if (value === null || value === undefined) {
151
+ ctx.addIssue({
152
+ code: 'custom',
153
+ message: 'evidence must be present (non-null, non-undefined)',
154
+ });
155
+ return;
156
+ }
157
+ if (typeof value === 'string') {
158
+ if (value.trim().length === 0) {
159
+ ctx.addIssue({ code: 'custom', message: 'evidence string must not be empty' });
160
+ }
161
+ return;
162
+ }
163
+ if (Array.isArray(value)) {
164
+ if (value.length === 0) {
165
+ ctx.addIssue({ code: 'custom', message: 'evidence array must not be empty' });
166
+ }
167
+ return;
168
+ }
169
+ if (typeof value === 'object') {
170
+ if (Object.keys(value).length === 0) {
171
+ ctx.addIssue({
172
+ code: 'custom',
173
+ message: 'evidence object must have at least one key (got {})',
174
+ });
175
+ }
176
+ return;
177
+ }
178
+ // Numbers, booleans, symbols, bigints, functions — not valid evidence shapes.
179
+ ctx.addIssue({
180
+ code: 'custom',
181
+ message: `evidence must be a string, array, or object (got ${typeof value})`,
182
+ });
183
+ });
184
+
185
+ // ─── passthrough — legacy starter `ensures.schema` names (R1 fail-closed) ──────
186
+
187
+ /**
188
+ * Passthrough validator that accepts ANY value (including `undefined`).
189
+ *
190
+ * Used to register the legacy `ensures.schema` names already declared in the
191
+ * shipped starter `.cantbook` files ({@link LEGACY_PASSTHROUGH_SCHEMA_NAMES}).
192
+ *
193
+ * @remarks
194
+ * Before T11762, the runtime hardcoded ONLY `task_tree` / `evidence` and
195
+ * SILENTLY SKIPPED every other `ensures.schema` name. ST-2 flips that to
196
+ * fail-closed: an UNREGISTERED name is now a contract violation. To avoid
197
+ * regressing the in-repo starter playbooks (`release` / `rcasd` / `ivtr`),
198
+ * which declare a dozen descriptive schema names that were never actually
199
+ * shape-checked, each such name is registered here with this passthrough
200
+ * schema — preserving their historical "always pass" behavior EXACTLY while
201
+ * still failing-closed for genuinely-unknown names (Risk R1). Tightening any of
202
+ * these into a real shape is a follow-up; the name is the SSoT anchor.
203
+ *
204
+ * @public
205
+ */
206
+ export const passthroughSchema: ZodType = z.unknown();
207
+
208
+ /**
209
+ * The legacy `ensures.schema` names declared in the shipped starter
210
+ * `.cantbook` files that were SILENTLY SKIPPED pre-T11762. Registered with
211
+ * {@link passthroughSchema} so the fail-closed flip (ST-2) does not regress
212
+ * them. Kept as exported DATA so the parity test can assert the exact set.
213
+ *
214
+ * @public
215
+ */
216
+ export const LEGACY_PASSTHROUGH_SCHEMA_NAMES: readonly string[] = [
217
+ // release.cantbook
218
+ 'version_bump_report',
219
+ 'changelog_entry',
220
+ 'publish_report',
221
+ // rcasd.cantbook
222
+ 'research_summary',
223
+ 'consensus_decision',
224
+ 'architecture_plan',
225
+ 'specification_document',
226
+ // ivtr.cantbook
227
+ 'implementation_diff',
228
+ 'validation_report',
229
+ 'audit_report',
230
+ 'test_report',
231
+ 'release_confirmation',
232
+ ];
233
+
234
+ /**
235
+ * The ensures-schema registry DATA — a `ReadonlyMap` from `ensures.schema` name
236
+ * to its {@link EnsuresSchemaSpec}.
237
+ *
238
+ * This is a VALUE (data), not a bodied function, so it is Gate-10-safe in the
239
+ * `@cleocode/contracts` leaf package. The bodied accessor layer in
240
+ * `@cleocode/core` (ST-1b) reads this map.
241
+ *
242
+ * Two strict entries (`task_tree`, `evidence`) port the deleted bespoke
243
+ * validators; the remaining entries are the legacy starter names
244
+ * ({@link LEGACY_PASSTHROUGH_SCHEMA_NAMES}) registered with
245
+ * {@link passthroughSchema} so the ST-2 fail-closed flip does not regress the
246
+ * shipped playbooks (Risk R1).
247
+ *
248
+ * @public
249
+ */
250
+ export const ENSURES_SCHEMA_REGISTRY: ReadonlyMap<string, EnsuresSchemaSpec> = new Map<
251
+ string,
252
+ EnsuresSchemaSpec
253
+ >([
254
+ ['task_tree', { name: 'task_tree', contextKey: 'task_tree', schema: taskTreeSchema }],
255
+ ['evidence', { name: 'evidence', contextKey: 'evidence', schema: evidenceSchema }],
256
+ ...LEGACY_PASSTHROUGH_SCHEMA_NAMES.map((name): [string, EnsuresSchemaSpec] => [
257
+ name,
258
+ { name, contextKey: name, schema: passthroughSchema },
259
+ ]),
260
+ ]);