@cleocode/contracts 2026.5.112 → 2026.5.113

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 (98) hide show
  1. package/dist/__tests__/cli-meta-suggested-next.test.d.ts +21 -0
  2. package/dist/__tests__/cli-meta-suggested-next.test.d.ts.map +1 -0
  3. package/dist/__tests__/cli-meta-suggested-next.test.js +70 -0
  4. package/dist/__tests__/cli-meta-suggested-next.test.js.map +1 -0
  5. package/dist/config/__tests__/manifest.test.d.ts +13 -0
  6. package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
  7. package/dist/config/__tests__/manifest.test.js +143 -0
  8. package/dist/config/__tests__/manifest.test.js.map +1 -0
  9. package/dist/config/manifest.d.ts +158 -0
  10. package/dist/config/manifest.d.ts.map +1 -0
  11. package/dist/config/manifest.js +123 -0
  12. package/dist/config/manifest.js.map +1 -0
  13. package/dist/docs/__tests__/provenance.test.d.ts +14 -0
  14. package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
  15. package/dist/docs/__tests__/provenance.test.js +579 -0
  16. package/dist/docs/__tests__/provenance.test.js.map +1 -0
  17. package/dist/docs/provenance.d.ts +731 -0
  18. package/dist/docs/provenance.d.ts.map +1 -0
  19. package/dist/docs/provenance.js +258 -0
  20. package/dist/docs/provenance.js.map +1 -0
  21. package/dist/docs-taxonomy.d.ts +24 -0
  22. package/dist/docs-taxonomy.d.ts.map +1 -1
  23. package/dist/docs-taxonomy.js +27 -0
  24. package/dist/docs-taxonomy.js.map +1 -1
  25. package/dist/doctor.d.ts +101 -0
  26. package/dist/doctor.d.ts.map +1 -1
  27. package/dist/errors.d.ts +4 -0
  28. package/dist/errors.d.ts.map +1 -1
  29. package/dist/errors.js +4 -0
  30. package/dist/errors.js.map +1 -1
  31. package/dist/exit-codes.d.ts +37 -0
  32. package/dist/exit-codes.d.ts.map +1 -1
  33. package/dist/exit-codes.js +37 -0
  34. package/dist/exit-codes.js.map +1 -1
  35. package/dist/index.d.ts +10 -3
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +7 -2
  38. package/dist/index.js.map +1 -1
  39. package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
  40. package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
  41. package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
  42. package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
  43. package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
  44. package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
  45. package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
  46. package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
  47. package/dist/invariants/adr-056-release.d.ts +42 -0
  48. package/dist/invariants/adr-056-release.d.ts.map +1 -0
  49. package/dist/invariants/adr-056-release.js +143 -0
  50. package/dist/invariants/adr-056-release.js.map +1 -0
  51. package/dist/invariants/adr-070-orchestration.d.ts +73 -0
  52. package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
  53. package/dist/invariants/adr-070-orchestration.js +290 -0
  54. package/dist/invariants/adr-070-orchestration.js.map +1 -0
  55. package/dist/invariants/index.d.ts +4 -1
  56. package/dist/invariants/index.d.ts.map +1 -1
  57. package/dist/invariants/index.js +11 -2
  58. package/dist/invariants/index.js.map +1 -1
  59. package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
  60. package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
  61. package/dist/operations/__tests__/input-contract.test.js +158 -0
  62. package/dist/operations/__tests__/input-contract.test.js.map +1 -0
  63. package/dist/operations/docs.d.ts +16 -0
  64. package/dist/operations/docs.d.ts.map +1 -1
  65. package/dist/operations/input-contract.d.ts +267 -0
  66. package/dist/operations/input-contract.d.ts.map +1 -0
  67. package/dist/operations/input-contract.js +29 -0
  68. package/dist/operations/input-contract.js.map +1 -0
  69. package/dist/templates/__tests__/manifest.test.d.ts +18 -0
  70. package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
  71. package/dist/templates/__tests__/manifest.test.js +123 -0
  72. package/dist/templates/__tests__/manifest.test.js.map +1 -0
  73. package/dist/templates/manifest.d.ts +186 -0
  74. package/dist/templates/manifest.d.ts.map +1 -0
  75. package/dist/templates/manifest.js +165 -0
  76. package/dist/templates/manifest.js.map +1 -0
  77. package/package.json +2 -2
  78. package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
  79. package/src/config/__tests__/manifest.test.ts +176 -0
  80. package/src/config/manifest.ts +218 -0
  81. package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
  82. package/src/docs/__tests__/provenance.test.ts +648 -0
  83. package/src/docs/provenance.ts +601 -0
  84. package/src/docs-taxonomy.ts +55 -0
  85. package/src/doctor.ts +111 -0
  86. package/src/errors.ts +4 -0
  87. package/src/exit-codes.ts +39 -0
  88. package/src/index.ts +90 -0
  89. package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
  90. package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
  91. package/src/invariants/adr-056-release.ts +154 -0
  92. package/src/invariants/adr-070-orchestration.ts +317 -0
  93. package/src/invariants/index.ts +11 -2
  94. package/src/operations/__tests__/input-contract.test.ts +188 -0
  95. package/src/operations/docs.ts +16 -0
  96. package/src/operations/input-contract.ts +301 -0
  97. package/src/templates/__tests__/manifest.test.ts +140 -0
  98. package/src/templates/manifest.ts +208 -0
@@ -11,7 +11,8 @@
11
11
  * 5. Render documentation via the downstream R8 auto-render pipeline.
12
12
  *
13
13
  * Downstream consumers:
14
- * - R2 (T10336): register ADR-070 ORC codes into this same registry.
14
+ * - R2 (T10336, SHIPPED): registers ADR-070 ORC-001..ORC-014 codes into
15
+ * this same registry via `./adr-070-orchestration.ts`.
15
16
  * - R4 (T10338): CI gate validates that every `severity:'error'` entry has
16
17
  * a non-null `runtimeGate` AND a matching error-code in `errors.ts`.
17
18
  * - R5 (T10339): refactor `packages/core/src/release/invariants/registry.ts`
@@ -26,10 +27,14 @@
26
27
  * @see ADR-073-above-epic-naming.md §1.2
27
28
  */
28
29
 
30
+ import { ADR_056_INVARIANTS } from './adr-056-release.js';
31
+ import { ADR_070_INVARIANTS } from './adr-070-orchestration.js';
29
32
  import { ADR_073_INVARIANTS } from './adr-073-saga.js';
30
33
 
31
34
  // Re-exported so consumers can import either the per-ADR list or the merged
32
35
  // registry from one place.
36
+ export { ADR_056_INVARIANTS } from './adr-056-release.js';
37
+ export { ADR_070_INVARIANTS } from './adr-070-orchestration.js';
33
38
  export { ADR_073_INVARIANTS } from './adr-073-saga.js';
34
39
 
35
40
  /**
@@ -149,7 +154,11 @@ function buildKey(invariant: RegisteredInvariant): string {
149
154
  * @internal
150
155
  */
151
156
  function buildRegistry(): Readonly<Record<string, RegisteredInvariant>> {
152
- const entries: RegisteredInvariant[] = [...ADR_073_INVARIANTS];
157
+ const entries: RegisteredInvariant[] = [
158
+ ...ADR_073_INVARIANTS,
159
+ ...ADR_056_INVARIANTS,
160
+ ...ADR_070_INVARIANTS,
161
+ ];
153
162
  const record: Record<string, RegisteredInvariant> = {};
154
163
  for (const entry of entries) {
155
164
  const key = buildKey(entry);
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Type + construction tests for OperationInputContract, ValidationResult,
3
+ * and ValidationError.
4
+ *
5
+ * These tests are intentionally minimal — the contracts package is a leaf
6
+ * with zero runtime dependencies, so there is no validator to exercise
7
+ * here. Instead, we assert:
8
+ *
9
+ * 1. ValidationResult narrows correctly via the `ok` discriminator.
10
+ * 2. A typed OperationInputContract<T> can be constructed and its
11
+ * examples[0].value satisfies the generic.
12
+ * 3. ValidationError can be constructed with all 6 required fields.
13
+ *
14
+ * The runtime validator that consumes these types ships under T9915.
15
+ *
16
+ * @epic T9855
17
+ * @task T9914
18
+ */
19
+
20
+ import { describe, expect, it } from 'vitest';
21
+ import type {
22
+ JsonSchema,
23
+ OperationInputContract,
24
+ OperationInputContractRegistry,
25
+ ValidationError,
26
+ ValidationResult,
27
+ } from '../input-contract.js';
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // ValidationResult discriminated-union narrowing
31
+ // ---------------------------------------------------------------------------
32
+
33
+ describe('ValidationResult<T>', () => {
34
+ it('narrows to the success branch when ok is true', () => {
35
+ // Constructor return type is widened so TS keeps both branches reachable
36
+ // at the call site — that's the whole point of the discriminated union.
37
+ const makeResult = (): ValidationResult<{ title: string }> => ({
38
+ ok: true,
39
+ value: { title: 'Ship E7' },
40
+ });
41
+ const result = makeResult();
42
+
43
+ if (result.ok) {
44
+ // Type-level: TypeScript narrows result.value to { title: string }.
45
+ // Runtime: the value is exactly what we constructed.
46
+ expect(result.value.title).toBe('Ship E7');
47
+ } else {
48
+ // Type-level: result.errors is ValidationError[].
49
+ expect(result.errors).toEqual([]);
50
+ }
51
+ });
52
+
53
+ it('narrows to the failure branch when ok is false', () => {
54
+ const makeResult = (): ValidationResult<{ title: string }> => ({
55
+ ok: false,
56
+ errors: [
57
+ {
58
+ path: '/title',
59
+ expected: 'string with minLength 1',
60
+ received: 'empty string',
61
+ fix: 'Provide a non-empty title.',
62
+ errorCode: 'E_INPUT_MIN_LENGTH',
63
+ schemaPath: '#/properties/title/minLength',
64
+ },
65
+ ],
66
+ });
67
+ const result = makeResult();
68
+
69
+ if (result.ok) {
70
+ expect(result.value).toBeDefined();
71
+ } else {
72
+ expect(result.errors).toHaveLength(1);
73
+ expect(result.errors[0]?.errorCode).toBe('E_INPUT_MIN_LENGTH');
74
+ }
75
+ });
76
+ });
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // OperationInputContract<T> construction
80
+ // ---------------------------------------------------------------------------
81
+
82
+ describe('OperationInputContract<T>', () => {
83
+ it('builds a typed contract whose examples match the generic', () => {
84
+ interface CreateTaskInput {
85
+ title: string;
86
+ acceptance: string;
87
+ }
88
+
89
+ const schema: JsonSchema = {
90
+ type: 'object',
91
+ required: ['title', 'acceptance'],
92
+ properties: {
93
+ title: { type: 'string', minLength: 1, maxLength: 200 },
94
+ acceptance: { type: 'string', minLength: 1 },
95
+ },
96
+ additionalProperties: false,
97
+ };
98
+
99
+ const contract: OperationInputContract<CreateTaskInput> = {
100
+ operation: 'tasks.create',
101
+ schema,
102
+ examples: [
103
+ {
104
+ name: 'minimal',
105
+ value: { title: 'Ship E7', acceptance: 'PR merged' },
106
+ description: 'Smallest valid payload.',
107
+ },
108
+ ],
109
+ };
110
+
111
+ expect(contract.operation).toBe('tasks.create');
112
+ expect(contract.schema).toBe(schema);
113
+ expect(contract.examples).toHaveLength(1);
114
+
115
+ const first = contract.examples[0];
116
+ expect(first).toBeDefined();
117
+ // Type-level: first.value is CreateTaskInput. Runtime sanity:
118
+ expect(first?.value.title).toBe('Ship E7');
119
+ expect(first?.value.acceptance).toBe('PR merged');
120
+ expect(first?.name).toBe('minimal');
121
+ expect(first?.description).toBe('Smallest valid payload.');
122
+ });
123
+
124
+ it('accepts a contract with an example that omits the optional description', () => {
125
+ const contract: OperationInputContract<{ id: string }> = {
126
+ operation: 'docs.fetch',
127
+ schema: { type: 'object', required: ['id'], properties: { id: { type: 'string' } } },
128
+ examples: [{ name: 'minimal', value: { id: 'adr-076' } }],
129
+ };
130
+
131
+ expect(contract.examples[0]?.description).toBeUndefined();
132
+ });
133
+ });
134
+
135
+ // ---------------------------------------------------------------------------
136
+ // ValidationError shape
137
+ // ---------------------------------------------------------------------------
138
+
139
+ describe('ValidationError', () => {
140
+ it('constructs with all 6 required fields', () => {
141
+ const err: ValidationError = {
142
+ path: '/owners/0/email',
143
+ expected: 'string matching email format',
144
+ received: 'string',
145
+ fix: 'Use a valid email address (e.g. user@example.com).',
146
+ errorCode: 'E_INPUT_FORMAT_EMAIL',
147
+ schemaPath: '#/properties/owners/items/properties/email/format',
148
+ };
149
+
150
+ expect(err.path).toBe('/owners/0/email');
151
+ expect(err.expected).toBe('string matching email format');
152
+ expect(err.received).toBe('string');
153
+ expect(err.fix).toBe('Use a valid email address (e.g. user@example.com).');
154
+ expect(err.errorCode).toBe('E_INPUT_FORMAT_EMAIL');
155
+ expect(err.schemaPath).toBe('#/properties/owners/items/properties/email/format');
156
+
157
+ // All 6 fields present — no extras.
158
+ expect(Object.keys(err).sort()).toEqual([
159
+ 'errorCode',
160
+ 'expected',
161
+ 'fix',
162
+ 'path',
163
+ 'received',
164
+ 'schemaPath',
165
+ ]);
166
+ });
167
+ });
168
+
169
+ // ---------------------------------------------------------------------------
170
+ // Registry shape
171
+ // ---------------------------------------------------------------------------
172
+
173
+ describe('OperationInputContractRegistry', () => {
174
+ it('keys contracts by their operation identifier', () => {
175
+ const createTask: OperationInputContract<{ title: string }> = {
176
+ operation: 'tasks.create',
177
+ schema: { type: 'object', required: ['title'], properties: { title: { type: 'string' } } },
178
+ examples: [{ name: 'minimal', value: { title: 'a' } }],
179
+ };
180
+
181
+ const registry: OperationInputContractRegistry = {
182
+ 'tasks.create': createTask as OperationInputContract<unknown>,
183
+ };
184
+
185
+ expect(registry['tasks.create']?.operation).toBe('tasks.create');
186
+ expect(registry['unknown.op']).toBeUndefined();
187
+ });
188
+ });
@@ -405,6 +405,22 @@ export interface DocsAddParams {
405
405
  * @task T9637 (T-DOCS-SLUG-2)
406
406
  */
407
407
  type?: DocsType;
408
+ /**
409
+ * When `true`, body-schema validation against
410
+ * {@link DocKindMetadata.requiredSections} is enforced — a missing
411
+ * required H2 section returns `E_DOC_SCHEMA_MISMATCH` with the
412
+ * missing-sections list in `details.missing`. When omitted or `false`,
413
+ * the validator runs in advisory mode: the write proceeds and any
414
+ * missing sections surface as a warning in the envelope's `meta`.
415
+ *
416
+ * Only meaningful when `file` is set; URL attachments skip body
417
+ * validation (no local bytes to scan).
418
+ *
419
+ * @task T10160 (E12.C3 · absorbs T10154)
420
+ * @epic T10157
421
+ * @saga T9855
422
+ */
423
+ strict?: boolean;
408
424
  }
409
425
 
410
426
  /**
@@ -0,0 +1,301 @@
1
+ /**
2
+ * OperationInputContract — schema-first input contract for CLEO operations.
3
+ *
4
+ * This module is the SINGLE SOURCE OF TRUTH for the schema-first input
5
+ * validation surface that powers the `mutate(operation, input)` DX
6
+ * (Saga T9855 / E7). Every CLEO operation that accepts structured input
7
+ * MUST be describable as an {@link OperationInputContract}: a JSON Schema
8
+ * draft-07 document, a stable operation identifier, and at least one
9
+ * worked example payload.
10
+ *
11
+ * Validators consume an {@link OperationInputContract} and produce a
12
+ * {@link ValidationResult} discriminated union — either the typed value
13
+ * (`ok: true`) or an array of {@link ValidationError} entries with
14
+ * actionable, JSON-Pointer-anchored remediation hints (`ok: false`).
15
+ *
16
+ * ZERO runtime dependencies: `contracts` is a leaf package. JSON Schema
17
+ * documents are modeled as the loose alias {@link JsonSchema} so this
18
+ * module does not pull in a JSON Schema typings package.
19
+ *
20
+ * @packageDocumentation
21
+ * @module @cleocode/contracts/operations/input-contract
22
+ *
23
+ * @see {@link https://json-schema.org/draft-07/schema}
24
+ *
25
+ * @epic T9855
26
+ * @task T9914
27
+ */
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // JSON Schema
31
+ // ---------------------------------------------------------------------------
32
+
33
+ /**
34
+ * Loose alias for a JSON Schema draft-07 document.
35
+ *
36
+ * Intentionally typed as `Record<string, unknown>` to keep the contracts
37
+ * package free of any JSON Schema typings runtime/type dependency. The
38
+ * actual draft-07 shape is enforced by the validator implementation
39
+ * (T9915) at the point of use, not by the type system here.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * const titleSchema: JsonSchema = {
44
+ * type: 'object',
45
+ * required: ['title'],
46
+ * properties: {
47
+ * title: { type: 'string', minLength: 1 },
48
+ * },
49
+ * };
50
+ * ```
51
+ */
52
+ export type JsonSchema = Record<string, unknown>;
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Examples
56
+ // ---------------------------------------------------------------------------
57
+
58
+ /**
59
+ * A single worked example payload for an {@link OperationInputContract}.
60
+ *
61
+ * Examples are MANDATORY (the contract requires at least one) — they
62
+ * double as documentation for human readers AND as conformance fixtures
63
+ * for the validator self-test that ships with every CLEO release.
64
+ *
65
+ * @typeParam T - The validated, typed input shape produced by the contract.
66
+ *
67
+ * @example
68
+ * ```ts
69
+ * const example: OperationInputExample<{ title: string }> = {
70
+ * name: 'minimal',
71
+ * value: { title: 'Ship saga T9855' },
72
+ * description: 'Smallest valid input — title is the only required field.',
73
+ * };
74
+ * ```
75
+ */
76
+ export interface OperationInputExample<T> {
77
+ /**
78
+ * Short, human-readable identifier for the example. Conventionally
79
+ * kebab-case (`'minimal'`, `'with-all-fields'`, `'edge-empty-array'`).
80
+ */
81
+ name: string;
82
+
83
+ /**
84
+ * A concrete value that validates successfully against the contract's
85
+ * {@link OperationInputContract.schema | schema}. Strongly typed via
86
+ * the `T` generic so example tables stay in sync with the operation
87
+ * payload shape at compile time.
88
+ */
89
+ value: T;
90
+
91
+ /**
92
+ * Optional prose explaining why this example matters — what edge case
93
+ * it covers, what default it exercises, etc. Surfaced in generated docs.
94
+ */
95
+ description?: string;
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // OperationInputContract
100
+ // ---------------------------------------------------------------------------
101
+
102
+ /**
103
+ * Schema-first input contract for a single CLEO operation.
104
+ *
105
+ * Pairs a stable operation identifier with the JSON Schema draft-07
106
+ * document that describes the operation's accepted input shape, plus a
107
+ * non-empty (by convention) catalogue of worked examples. The validator
108
+ * implementation (T9915) ingests an {@link OperationInputContract} and a
109
+ * raw payload, returning a {@link ValidationResult}.
110
+ *
111
+ * Together with the {@link OperationInputContractRegistry}, this type is
112
+ * the foundation of the schema-first `mutate(operation, input)` DX
113
+ * surface (Saga T9855 / E7).
114
+ *
115
+ * @typeParam T - The validated, typed input shape produced when the
116
+ * raw payload passes validation. The generic propagates through
117
+ * {@link ValidationResult} so callers receive a strongly-typed `value`
118
+ * in the success branch.
119
+ *
120
+ * @example
121
+ * ```ts
122
+ * interface CreateTaskInput {
123
+ * title: string;
124
+ * acceptance: string;
125
+ * }
126
+ *
127
+ * const createTaskContract: OperationInputContract<CreateTaskInput> = {
128
+ * operation: 'tasks.create',
129
+ * schema: {
130
+ * type: 'object',
131
+ * required: ['title', 'acceptance'],
132
+ * properties: {
133
+ * title: { type: 'string', minLength: 1, maxLength: 200 },
134
+ * acceptance: { type: 'string', minLength: 1 },
135
+ * },
136
+ * additionalProperties: false,
137
+ * },
138
+ * examples: [
139
+ * {
140
+ * name: 'minimal',
141
+ * value: { title: 'Ship E7', acceptance: 'PR merged' },
142
+ * },
143
+ * ],
144
+ * };
145
+ * ```
146
+ */
147
+ export interface OperationInputContract<T> {
148
+ /**
149
+ * Stable, fully-qualified operation identifier in
150
+ * `<domain>.<verb>` form (e.g. `'tasks.create'`, `'docs.publish'`).
151
+ * Used as the registry key in {@link OperationInputContractRegistry}.
152
+ */
153
+ operation: string;
154
+
155
+ /**
156
+ * JSON Schema draft-07 document describing the accepted input shape.
157
+ * The validator MUST treat absent properties as not-present (it does
158
+ * NOT inject `default` values) so callers can distinguish "user
159
+ * omitted" from "user supplied default".
160
+ */
161
+ schema: JsonSchema;
162
+
163
+ /**
164
+ * Catalogue of worked example payloads. Conventionally non-empty so
165
+ * generated docs always have at least one runnable sample. Each
166
+ * example's {@link OperationInputExample.value | value} is statically
167
+ * typed to the contract's `T` parameter.
168
+ */
169
+ examples: ReadonlyArray<OperationInputExample<T>>;
170
+ }
171
+
172
+ // ---------------------------------------------------------------------------
173
+ // ValidationError
174
+ // ---------------------------------------------------------------------------
175
+
176
+ /**
177
+ * A single validation failure produced by the input validator.
178
+ *
179
+ * Every field is REQUIRED — there are no optional remediation hints.
180
+ * This shape is the wire format the `mutate` DX surface returns to
181
+ * callers, so it MUST be stable, machine-parseable, AND human-readable.
182
+ *
183
+ * @example
184
+ * ```ts
185
+ * const err: ValidationError = {
186
+ * path: '/title',
187
+ * expected: 'string with minLength 1',
188
+ * received: 'empty string',
189
+ * fix: 'Provide a non-empty title (1-200 chars).',
190
+ * errorCode: 'E_INPUT_MIN_LENGTH',
191
+ * schemaPath: '#/properties/title/minLength',
192
+ * };
193
+ * ```
194
+ */
195
+ export interface ValidationError {
196
+ /**
197
+ * RFC 6901 JSON Pointer to the failing input location, anchored at
198
+ * the root of the payload (e.g. `'/title'`, `'/owners/0/email'`).
199
+ * An empty string `''` denotes the root document itself.
200
+ */
201
+ path: string;
202
+
203
+ /**
204
+ * Human-readable description of what the validator expected at
205
+ * {@link ValidationError.path | path} (e.g. `'string'`,
206
+ * `'integer >= 0'`, `'one of: pending | active | done'`).
207
+ */
208
+ expected: string;
209
+
210
+ /**
211
+ * Human-readable description of what was actually received at
212
+ * {@link ValidationError.path | path}. NEVER includes the raw value
213
+ * itself — describes type/cardinality only — so logs of errors do
214
+ * not accidentally leak user payloads.
215
+ */
216
+ received: string;
217
+
218
+ /**
219
+ * Actionable, imperative remediation hint targeted at the caller
220
+ * (e.g. `'Provide a non-empty title (1-200 chars).'`). Surfaced
221
+ * directly in CLI output and validator return envelopes.
222
+ */
223
+ fix: string;
224
+
225
+ /**
226
+ * Stable, screaming-snake-case error code (e.g.
227
+ * `'E_INPUT_MIN_LENGTH'`, `'E_INPUT_REQUIRED'`). Stable across
228
+ * releases so callers can branch on it programmatically.
229
+ */
230
+ errorCode: string;
231
+
232
+ /**
233
+ * RFC 6901 JSON Pointer into the contract's
234
+ * {@link OperationInputContract.schema | schema} that produced the
235
+ * failure (e.g. `'#/properties/title/minLength'`). Lets tooling jump
236
+ * straight to the offending schema clause.
237
+ */
238
+ schemaPath: string;
239
+ }
240
+
241
+ // ---------------------------------------------------------------------------
242
+ // ValidationResult
243
+ // ---------------------------------------------------------------------------
244
+
245
+ /**
246
+ * Discriminated union returned by the input validator.
247
+ *
248
+ * Narrows on the `ok` boolean: when `ok === true`, the strongly-typed
249
+ * `value` is present; when `ok === false`, a non-empty `errors` array
250
+ * lists every failure (validators are encouraged to surface ALL errors
251
+ * in one pass, not bail on the first).
252
+ *
253
+ * @typeParam T - The validated, typed input shape produced on success.
254
+ *
255
+ * @example
256
+ * ```ts
257
+ * function handle(result: ValidationResult<{ title: string }>) {
258
+ * if (result.ok) {
259
+ * // result.value is { title: string }
260
+ * return createTask(result.value);
261
+ * }
262
+ * // result.errors is ValidationError[]
263
+ * for (const e of result.errors) {
264
+ * console.error(`${e.path}: ${e.fix}`);
265
+ * }
266
+ * }
267
+ * ```
268
+ */
269
+ export type ValidationResult<T> =
270
+ | { readonly ok: true; readonly value: T }
271
+ | { readonly ok: false; readonly errors: ValidationError[] };
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // Registry
275
+ // ---------------------------------------------------------------------------
276
+
277
+ /**
278
+ * Registry of every {@link OperationInputContract} known to the CLEO
279
+ * runtime, keyed by the contract's `operation` identifier (e.g.
280
+ * `'tasks.create'`).
281
+ *
282
+ * The value type uses `unknown` for the contract's generic so registry
283
+ * lookups stay assignable across operations with different input
284
+ * shapes. Callers that need the concrete `T` MUST narrow via an
285
+ * operation-specific accessor — direct `registry[op]` reads are
286
+ * intentionally not type-narrowed by operation name.
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * const registry: OperationInputContractRegistry = {
291
+ * 'tasks.create': createTaskContract,
292
+ * 'docs.publish': publishDocsContract,
293
+ * };
294
+ *
295
+ * const contract = registry['tasks.create'];
296
+ * if (contract) {
297
+ * // validate(contract, payload) → ValidationResult<unknown>
298
+ * }
299
+ * ```
300
+ */
301
+ export type OperationInputContractRegistry = Record<string, OperationInputContract<unknown>>;
@@ -0,0 +1,140 @@
1
+ /**
2
+ * Contract tests for the TemplateManifest schema.
3
+ *
4
+ * Asserts:
5
+ *
6
+ * 1. A valid entry for every {@link TemplateKind} parses successfully.
7
+ * 2. An entry with an unknown `kind` is rejected.
8
+ * 3. An entry missing `installPath` is rejected.
9
+ * 4. The {@link PlaceholderSpec} `source` field is exhaustively typed —
10
+ * every member of {@link PLACEHOLDER_SOURCES} parses, and a bogus
11
+ * source value is rejected.
12
+ *
13
+ * @task T9875
14
+ * @epic T9874
15
+ * @saga T9855
16
+ */
17
+
18
+ import { describe, expect, it } from 'vitest';
19
+ import {
20
+ PLACEHOLDER_SOURCES,
21
+ PlaceholderSpecSchema,
22
+ TEMPLATE_KINDS,
23
+ type TemplateKind,
24
+ type TemplateManifestEntry,
25
+ TemplateManifestEntrySchema,
26
+ } from '../manifest.js';
27
+
28
+ /**
29
+ * Build a valid baseline entry parameterized by `kind` so each
30
+ * assertion below stays minimal.
31
+ */
32
+ function baseEntry(kind: TemplateKind): TemplateManifestEntry {
33
+ return {
34
+ id: `sample-${kind}`,
35
+ kind,
36
+ sourcePath: `packages/core/templates/${kind}s/sample.tmpl`,
37
+ installPath: `.cleo/${kind}s/sample`,
38
+ substitution: 'regex-tmpl',
39
+ placeholders: [
40
+ {
41
+ name: 'NODE_VERSION',
42
+ source: 'project-context',
43
+ sourcePath: 'engines.node',
44
+ defaultValue: 24,
45
+ },
46
+ ],
47
+ updateStrategy: 'overwrite-on-bump',
48
+ };
49
+ }
50
+
51
+ describe('TemplateManifestEntrySchema (T9875)', () => {
52
+ it('accepts a valid entry for every TemplateKind', () => {
53
+ for (const kind of TEMPLATE_KINDS) {
54
+ const entry = baseEntry(kind);
55
+ const parsed = TemplateManifestEntrySchema.parse(entry);
56
+ expect(parsed.kind, `kind '${kind}' must round-trip`).toBe(kind);
57
+ expect(parsed.installPath).toBe(entry.installPath);
58
+ }
59
+ });
60
+
61
+ it('rejects an entry with an unknown kind', () => {
62
+ const bogus = {
63
+ ...baseEntry('workflow'),
64
+ kind: 'not-a-real-kind',
65
+ };
66
+ const result = TemplateManifestEntrySchema.safeParse(bogus);
67
+ expect(result.success).toBe(false);
68
+ });
69
+
70
+ it('rejects an entry missing installPath', () => {
71
+ const { installPath: _drop, ...rest } = baseEntry('config');
72
+ const result = TemplateManifestEntrySchema.safeParse(rest);
73
+ expect(result.success).toBe(false);
74
+ });
75
+
76
+ it('accepts an entry whose placeholders array is empty', () => {
77
+ const entry = { ...baseEntry('doc'), placeholders: [] };
78
+ const result = TemplateManifestEntrySchema.safeParse(entry);
79
+ expect(result.success).toBe(true);
80
+ });
81
+
82
+ it('accepts a defaultValue of every supported literal type and null', () => {
83
+ const types = [
84
+ { defaultValue: 'string-default' },
85
+ { defaultValue: 42 },
86
+ { defaultValue: true },
87
+ { defaultValue: null },
88
+ {},
89
+ ] as const;
90
+ for (const extra of types) {
91
+ const result = PlaceholderSpecSchema.safeParse({
92
+ name: 'X',
93
+ source: 'literal',
94
+ sourcePath: 'X',
95
+ ...extra,
96
+ });
97
+ expect(result.success, `defaultValue case ${JSON.stringify(extra)} must parse`).toBe(true);
98
+ }
99
+ });
100
+ });
101
+
102
+ describe('PlaceholderSpecSchema source enum (T9875)', () => {
103
+ it('exhaustively accepts every member of PLACEHOLDER_SOURCES', () => {
104
+ for (const source of PLACEHOLDER_SOURCES) {
105
+ const result = PlaceholderSpecSchema.safeParse({
106
+ name: 'PLACEHOLDER',
107
+ source,
108
+ sourcePath: 'some.path',
109
+ });
110
+ expect(result.success, `source '${source}' must parse`).toBe(true);
111
+ }
112
+ });
113
+
114
+ it('rejects an unknown source value', () => {
115
+ const result = PlaceholderSpecSchema.safeParse({
116
+ name: 'PLACEHOLDER',
117
+ source: 'not-a-source',
118
+ sourcePath: 'some.path',
119
+ });
120
+ expect(result.success).toBe(false);
121
+ });
122
+
123
+ it('rejects an empty name', () => {
124
+ const result = PlaceholderSpecSchema.safeParse({
125
+ name: '',
126
+ source: 'project-context',
127
+ sourcePath: 'some.path',
128
+ });
129
+ expect(result.success).toBe(false);
130
+ });
131
+
132
+ it('rejects an empty sourcePath', () => {
133
+ const result = PlaceholderSpecSchema.safeParse({
134
+ name: 'X',
135
+ source: 'project-context',
136
+ sourcePath: '',
137
+ });
138
+ expect(result.success).toBe(false);
139
+ });
140
+ });