@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.
- package/dist/__tests__/operation-def.test.js +2 -1
- package/dist/__tests__/operation-def.test.js.map +1 -1
- package/dist/__tests__/sealed-credential.test.d.ts +19 -0
- package/dist/__tests__/sealed-credential.test.d.ts.map +1 -0
- package/dist/__tests__/sealed-credential.test.js +74 -0
- package/dist/__tests__/sealed-credential.test.js.map +1 -0
- package/dist/__tests__/skill-executor-contract.test.js +3 -0
- package/dist/__tests__/skill-executor-contract.test.js.map +1 -1
- package/dist/acceptance-gate.d.ts +2 -3
- package/dist/acceptance-gate.d.ts.map +1 -1
- package/dist/boundary.d.ts.map +1 -1
- package/dist/boundary.js +1 -19
- package/dist/boundary.js.map +1 -1
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/data-accessor.d.ts +21 -1
- package/dist/data-accessor.d.ts.map +1 -1
- package/dist/data-accessor.js.map +1 -1
- package/dist/dispatch/identity.d.ts +1 -1
- package/dist/dispatch/identity.d.ts.map +1 -1
- package/dist/dispatch/identity.js +5 -0
- package/dist/dispatch/identity.js.map +1 -1
- package/dist/dispatch/operations-registry.d.ts.map +1 -1
- package/dist/dispatch/operations-registry.js +46 -168
- package/dist/dispatch/operations-registry.js.map +1 -1
- package/dist/docs/read.d.ts +2 -2
- package/dist/index.d.ts +12 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -1
- package/dist/lease-ipc/__tests__/freeze.test.d.ts +15 -0
- package/dist/lease-ipc/__tests__/freeze.test.d.ts.map +1 -0
- package/dist/lease-ipc/__tests__/freeze.test.js +221 -0
- package/dist/lease-ipc/__tests__/freeze.test.js.map +1 -0
- package/dist/lease-ipc/index.d.ts +17 -0
- package/dist/lease-ipc/index.d.ts.map +1 -0
- package/dist/lease-ipc/index.js +16 -0
- package/dist/lease-ipc/index.js.map +1 -0
- package/dist/lease-ipc/messages.d.ts +754 -0
- package/dist/lease-ipc/messages.d.ts.map +1 -0
- package/dist/lease-ipc/messages.js +383 -0
- package/dist/lease-ipc/messages.js.map +1 -0
- package/dist/lease-ipc/version.d.ts +41 -0
- package/dist/lease-ipc/version.d.ts.map +1 -0
- package/dist/lease-ipc/version.js +43 -0
- package/dist/lease-ipc/version.js.map +1 -0
- package/dist/llm/interfaces.d.ts +23 -0
- package/dist/llm/interfaces.d.ts.map +1 -1
- package/dist/llm/sealed-credential.d.ts +164 -0
- package/dist/llm/sealed-credential.d.ts.map +1 -0
- package/dist/llm/sealed-credential.js +43 -0
- package/dist/llm/sealed-credential.js.map +1 -0
- package/dist/llm/system-of-use.d.ts +383 -0
- package/dist/llm/system-of-use.d.ts.map +1 -0
- package/dist/llm/system-of-use.js +238 -0
- package/dist/llm/system-of-use.js.map +1 -0
- package/dist/llm/system-resolver.d.ts +45 -11
- package/dist/llm/system-resolver.d.ts.map +1 -1
- package/dist/llm/system-resolver.js +14 -7
- package/dist/llm/system-resolver.js.map +1 -1
- package/dist/operations/__tests__/ensures-schema-registry.test.d.ts +17 -0
- package/dist/operations/__tests__/ensures-schema-registry.test.d.ts.map +1 -0
- package/dist/operations/__tests__/ensures-schema-registry.test.js +113 -0
- package/dist/operations/__tests__/ensures-schema-registry.test.js.map +1 -0
- package/dist/operations/ensures-schema-registry.d.ts +172 -0
- package/dist/operations/ensures-schema-registry.d.ts.map +1 -0
- package/dist/operations/ensures-schema-registry.js +226 -0
- package/dist/operations/ensures-schema-registry.js.map +1 -0
- package/dist/operations/llm.d.ts +101 -9
- package/dist/operations/llm.d.ts.map +1 -1
- package/dist/operations/validate.d.ts +1 -12
- package/dist/operations/validate.d.ts.map +1 -1
- package/dist/operations/validate.js +1 -1
- package/dist/playbook.d.ts +60 -0
- package/dist/playbook.d.ts.map +1 -1
- package/dist/session.d.ts +11 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js.map +1 -1
- package/dist/templates/manifest.d.ts +1 -1
- package/dist/tools/agent-tools.d.ts +231 -0
- package/dist/tools/agent-tools.d.ts.map +1 -0
- package/dist/tools/agent-tools.js +25 -0
- package/dist/tools/agent-tools.js.map +1 -0
- package/dist/tools/atomic.d.ts +19 -0
- package/dist/tools/atomic.d.ts.map +1 -1
- package/dist/tools/atomic.js +17 -0
- package/dist/tools/atomic.js.map +1 -1
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/skill-executor.d.ts +7 -0
- package/dist/tools/skill-executor.d.ts.map +1 -1
- package/dist/tools/web-tools.d.ts +217 -0
- package/dist/tools/web-tools.d.ts.map +1 -0
- package/dist/tools/web-tools.js +31 -0
- package/dist/tools/web-tools.js.map +1 -0
- package/package.json +6 -2
- package/src/__tests__/__snapshots__/operations-registry.test.ts.snap +1 -1
- package/src/__tests__/operation-def.test.ts +2 -1
- package/src/__tests__/sealed-credential.test.ts +100 -0
- package/src/__tests__/skill-executor-contract.test.ts +3 -0
- package/src/acceptance-gate.ts +2 -3
- package/src/boundary.ts +1 -20
- package/src/config.ts +104 -0
- package/src/data-accessor.ts +22 -1
- package/src/dispatch/identity.ts +5 -0
- package/src/dispatch/operations-registry.ts +51 -170
- package/src/index.ts +103 -24
- package/src/lease-ipc/__tests__/freeze.test.ts +246 -0
- package/src/lease-ipc/index.ts +63 -0
- package/src/lease-ipc/messages.ts +471 -0
- package/src/lease-ipc/version.ts +46 -0
- package/src/llm/interfaces.ts +23 -0
- package/src/llm/sealed-credential.ts +166 -0
- package/src/llm/system-of-use.ts +503 -0
- package/src/llm/system-resolver.ts +46 -11
- package/src/operations/__tests__/ensures-schema-registry.test.ts +169 -0
- package/src/operations/ensures-schema-registry.ts +260 -0
- package/src/operations/llm.ts +105 -8
- package/src/operations/validate.ts +1 -14
- package/src/playbook.ts +61 -0
- package/src/session.ts +11 -0
- package/src/tools/agent-tools.ts +270 -0
- package/src/tools/atomic.ts +21 -0
- package/src/tools/index.ts +46 -0
- package/src/tools/skill-executor.ts +7 -0
- package/src/tools/web-tools.ts +247 -0
- package/dist/tessera.d.ts +0 -32
- package/dist/tessera.d.ts.map +0 -1
- package/dist/tessera.js +0 -11
- package/dist/tessera.js.map +0 -1
- package/dist/warp-chain.d.ts +0 -129
- package/dist/warp-chain.d.ts.map +0 -1
- package/dist/warp-chain.js +0 -13
- package/dist/warp-chain.js.map +0 -1
- package/src/tessera.ts +0 -35
- package/src/warp-chain.ts +0 -185
|
@@ -1,11 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* `SystemOfUseLabel` contract types for the `resolveLLMForSystem` chokepoint (E9).
|
|
3
3
|
*
|
|
4
|
-
* A "system of use" is the
|
|
5
|
-
*
|
|
6
|
-
* a higher-level, stable identity that
|
|
7
|
-
* config resolution. This indirection
|
|
8
|
-
* vocabulary (roles) and from
|
|
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
|
-
*
|
|
29
|
+
* Flat, ergonomic label for a CLEO background subsystem that requests an LLM
|
|
30
|
+
* client through the E9 resolver.
|
|
22
31
|
*
|
|
23
|
-
* Each `
|
|
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
|
|
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
|
|
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<
|
|
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
|
+
]);
|