@cleocode/contracts 2026.5.111 → 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.
- package/dist/__tests__/cli-meta-suggested-next.test.d.ts +21 -0
- package/dist/__tests__/cli-meta-suggested-next.test.d.ts.map +1 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js +70 -0
- package/dist/__tests__/cli-meta-suggested-next.test.js.map +1 -0
- package/dist/__tests__/evidence-atom-schema.test.d.ts +16 -0
- package/dist/__tests__/evidence-atom-schema.test.d.ts.map +1 -0
- package/dist/__tests__/evidence-atom-schema.test.js +394 -0
- package/dist/__tests__/evidence-atom-schema.test.js.map +1 -0
- package/dist/config/__tests__/manifest.test.d.ts +13 -0
- package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/config/__tests__/manifest.test.js +143 -0
- package/dist/config/__tests__/manifest.test.js.map +1 -0
- package/dist/config/manifest.d.ts +158 -0
- package/dist/config/manifest.d.ts.map +1 -0
- package/dist/config/manifest.js +123 -0
- package/dist/config/manifest.js.map +1 -0
- package/dist/docs/__tests__/provenance.test.d.ts +14 -0
- package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
- package/dist/docs/__tests__/provenance.test.js +579 -0
- package/dist/docs/__tests__/provenance.test.js.map +1 -0
- package/dist/docs/provenance.d.ts +731 -0
- package/dist/docs/provenance.d.ts.map +1 -0
- package/dist/docs/provenance.js +258 -0
- package/dist/docs/provenance.js.map +1 -0
- package/dist/docs-taxonomy.d.ts +24 -0
- package/dist/docs-taxonomy.d.ts.map +1 -1
- package/dist/docs-taxonomy.js +27 -0
- package/dist/docs-taxonomy.js.map +1 -1
- package/dist/doctor.d.ts +101 -0
- package/dist/doctor.d.ts.map +1 -1
- package/dist/errors.d.ts +4 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +4 -0
- package/dist/errors.js.map +1 -1
- package/dist/evidence-atom-schema.d.ts +407 -0
- package/dist/evidence-atom-schema.d.ts.map +1 -0
- package/dist/evidence-atom-schema.js +505 -0
- package/dist/evidence-atom-schema.js.map +1 -0
- package/dist/exit-codes.d.ts +37 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +37 -0
- package/dist/exit-codes.js.map +1 -1
- package/dist/index.d.ts +13 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -1
- package/dist/index.js.map +1 -1
- package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
- package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
- package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
- package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
- package/dist/invariants/__tests__/registry.test.d.ts +14 -0
- package/dist/invariants/__tests__/registry.test.d.ts.map +1 -0
- package/dist/invariants/__tests__/registry.test.js +113 -0
- package/dist/invariants/__tests__/registry.test.js.map +1 -0
- package/dist/invariants/adr-056-release.d.ts +42 -0
- package/dist/invariants/adr-056-release.d.ts.map +1 -0
- package/dist/invariants/adr-056-release.js +143 -0
- package/dist/invariants/adr-056-release.js.map +1 -0
- package/dist/invariants/adr-070-orchestration.d.ts +73 -0
- package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
- package/dist/invariants/adr-070-orchestration.js +290 -0
- package/dist/invariants/adr-070-orchestration.js.map +1 -0
- package/dist/invariants/adr-073-saga.d.ts +32 -0
- package/dist/invariants/adr-073-saga.d.ts.map +1 -0
- package/dist/invariants/adr-073-saga.js +155 -0
- package/dist/invariants/adr-073-saga.js.map +1 -0
- package/dist/invariants/index.d.ts +162 -0
- package/dist/invariants/index.d.ts.map +1 -0
- package/dist/invariants/index.js +118 -0
- package/dist/invariants/index.js.map +1 -0
- package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
- package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
- package/dist/operations/__tests__/input-contract.test.js +158 -0
- package/dist/operations/__tests__/input-contract.test.js.map +1 -0
- package/dist/operations/docs.d.ts +16 -0
- package/dist/operations/docs.d.ts.map +1 -1
- package/dist/operations/input-contract.d.ts +267 -0
- package/dist/operations/input-contract.d.ts.map +1 -0
- package/dist/operations/input-contract.js +29 -0
- package/dist/operations/input-contract.js.map +1 -0
- package/dist/templates/__tests__/manifest.test.d.ts +18 -0
- package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
- package/dist/templates/__tests__/manifest.test.js +123 -0
- package/dist/templates/__tests__/manifest.test.js.map +1 -0
- package/dist/templates/manifest.d.ts +186 -0
- package/dist/templates/manifest.d.ts.map +1 -0
- package/dist/templates/manifest.js +165 -0
- package/dist/templates/manifest.js.map +1 -0
- package/package.json +2 -2
- package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
- package/src/__tests__/evidence-atom-schema.test.ts +499 -0
- package/src/config/__tests__/manifest.test.ts +176 -0
- package/src/config/manifest.ts +218 -0
- package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
- package/src/docs/__tests__/provenance.test.ts +648 -0
- package/src/docs/provenance.ts +601 -0
- package/src/docs-taxonomy.ts +55 -0
- package/src/doctor.ts +111 -0
- package/src/errors.ts +4 -0
- package/src/evidence-atom-schema.ts +628 -0
- package/src/exit-codes.ts +39 -0
- package/src/index.ts +129 -0
- package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
- package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
- package/src/invariants/__tests__/registry.test.ts +136 -0
- package/src/invariants/adr-056-release.ts +154 -0
- package/src/invariants/adr-070-orchestration.ts +317 -0
- package/src/invariants/adr-073-saga.ts +167 -0
- package/src/invariants/index.ts +222 -0
- package/src/operations/__tests__/input-contract.test.ts +188 -0
- package/src/operations/docs.ts +16 -0
- package/src/operations/input-contract.ts +301 -0
- package/src/templates/__tests__/manifest.test.ts +140 -0
- package/src/templates/manifest.ts +208 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Central invariants registry — SSoT for system-wide invariants surfaced via
|
|
3
|
+
* the SaGa T10326 SG-SUBSTRATE-RECONCILIATION programme (Epic T10327
|
|
4
|
+
* E-INVARIANT-REGISTRY-SSOT, Task T10335 R1).
|
|
5
|
+
*
|
|
6
|
+
* Every registered invariant carries the metadata needed to:
|
|
7
|
+
* 1. Reference its source ADR (e.g. `ADR-073` §1.2 for I1-I8).
|
|
8
|
+
* 2. Locate the runtime guard that enforces it (when one exists).
|
|
9
|
+
* 3. Bind the CI lint rule that protects it (when one exists).
|
|
10
|
+
* 4. Cross-reference test files that exercise it.
|
|
11
|
+
* 5. Render documentation via the downstream R8 auto-render pipeline.
|
|
12
|
+
*
|
|
13
|
+
* Downstream consumers:
|
|
14
|
+
* - R2 (T10336, SHIPPED): registers ADR-070 ORC-001..ORC-014 codes into
|
|
15
|
+
* this same registry via `./adr-070-orchestration.ts`.
|
|
16
|
+
* - R4 (T10338): CI gate validates that every `severity:'error'` entry has
|
|
17
|
+
* a non-null `runtimeGate` AND a matching error-code in `errors.ts`.
|
|
18
|
+
* - R5 (T10339): refactor `packages/core/src/release/invariants/registry.ts`
|
|
19
|
+
* to consume the central registry instead of carrying its own list.
|
|
20
|
+
* - R6 (T10340): `cleo doctor --audit-invariants` walks the registry to
|
|
21
|
+
* produce a per-invariant audit report.
|
|
22
|
+
* - R8 (T10342): auto-render docs page from the registry.
|
|
23
|
+
*
|
|
24
|
+
* @epic T10327 — E-INVARIANT-REGISTRY-SSOT
|
|
25
|
+
* @saga T10326 — SG-SUBSTRATE-RECONCILIATION
|
|
26
|
+
* @task T10335 — R1: registry + ADR-073 I1-I8
|
|
27
|
+
* @see ADR-073-above-epic-naming.md §1.2
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { ADR_056_INVARIANTS } from './adr-056-release.js';
|
|
31
|
+
import { ADR_070_INVARIANTS } from './adr-070-orchestration.js';
|
|
32
|
+
import { ADR_073_INVARIANTS } from './adr-073-saga.js';
|
|
33
|
+
|
|
34
|
+
// Re-exported so consumers can import either the per-ADR list or the merged
|
|
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';
|
|
38
|
+
export { ADR_073_INVARIANTS } from './adr-073-saga.js';
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Severity tier for a registered invariant.
|
|
42
|
+
*
|
|
43
|
+
* - `'error'` — runtime gate MUST exist; violation surfaces as LAFS error
|
|
44
|
+
* code (cross-checked by R4 CI gate).
|
|
45
|
+
* - `'warning'` — orchestration/process concern; surfaces via doctor audit
|
|
46
|
+
* but does not throw at runtime.
|
|
47
|
+
* - `'info'` — display or storage concern; enforced (if at all) by DB
|
|
48
|
+
* constraints or convention rather than a runtime function.
|
|
49
|
+
*/
|
|
50
|
+
export type InvariantSeverity = 'info' | 'warning' | 'error';
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Pointer to a runtime guard function that enforces an invariant.
|
|
54
|
+
*
|
|
55
|
+
* The `module` field is the package-relative import specifier
|
|
56
|
+
* (e.g. `packages/core/src/sagas/enforcement.ts`). The `functionName` is the
|
|
57
|
+
* exported identifier (e.g. `assertSagaInvariantI3`).
|
|
58
|
+
*
|
|
59
|
+
* R4 CI gate uses this triple to assert the guard actually exists and is
|
|
60
|
+
* exported with the expected name. R6 doctor audit uses it to render
|
|
61
|
+
* per-invariant enforcement provenance.
|
|
62
|
+
*/
|
|
63
|
+
export interface InvariantRuntimeGate {
|
|
64
|
+
readonly module: string;
|
|
65
|
+
readonly functionName: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Pointer to a CI lint script that protects an invariant.
|
|
70
|
+
*
|
|
71
|
+
* The path is repo-rooted (e.g. `scripts/lint-saga-label-anti-pattern.mjs`).
|
|
72
|
+
*/
|
|
73
|
+
export interface InvariantLintRule {
|
|
74
|
+
readonly lintScript: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Pointer to a `cleo doctor` audit that surfaces invariant violations.
|
|
79
|
+
*
|
|
80
|
+
* The lintScript field mirrors the CI gate pointer — many invariants share
|
|
81
|
+
* the same script between CI and doctor (e.g. saga-label anti-pattern).
|
|
82
|
+
*/
|
|
83
|
+
export interface InvariantDoctorAudit {
|
|
84
|
+
readonly lintScript: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* A registered invariant — one entry per `(adr, code)` pair.
|
|
89
|
+
*
|
|
90
|
+
* The `adr` + `code` combination forms the registry key
|
|
91
|
+
* (`${adr}.${code}`, e.g. `'ADR-073.I3'`). Lookups via `getInvariant`
|
|
92
|
+
* and per-ADR enumeration via `getInvariantsByAdr` use this convention.
|
|
93
|
+
*
|
|
94
|
+
* @see ADR-073-above-epic-naming.md §1.2 — original I1-I8 source.
|
|
95
|
+
*/
|
|
96
|
+
export interface RegisteredInvariant {
|
|
97
|
+
/** Source ADR identifier, e.g. `'ADR-073'`. */
|
|
98
|
+
readonly adr: string;
|
|
99
|
+
/** Invariant code within the ADR, e.g. `'I3'` or `'ORC-001'`. */
|
|
100
|
+
readonly code: string;
|
|
101
|
+
/** Short human-readable name, e.g. `'Tier promotion mandatory'`. */
|
|
102
|
+
readonly name: string;
|
|
103
|
+
/** Full description (one or two sentences, no markdown). */
|
|
104
|
+
readonly description: string;
|
|
105
|
+
/** Severity tier — drives R4 CI gate strictness. */
|
|
106
|
+
readonly severity: InvariantSeverity;
|
|
107
|
+
/**
|
|
108
|
+
* Runtime guard function, or `null` when the invariant has no
|
|
109
|
+
* runtime-checkable form (display/storage/process concerns).
|
|
110
|
+
*
|
|
111
|
+
* Every `severity:'error'` invariant MUST set a non-null
|
|
112
|
+
* `runtimeGate` — enforced by the registry test suite (T10335 AC5)
|
|
113
|
+
* and by the R4 CI gate (T10338).
|
|
114
|
+
*/
|
|
115
|
+
readonly runtimeGate: InvariantRuntimeGate | null;
|
|
116
|
+
/**
|
|
117
|
+
* Optional CI lint rule reference. Distinct from `runtimeGate` —
|
|
118
|
+
* `runtimeGate` runs in dispatch code, `lintRule` runs in CI.
|
|
119
|
+
*/
|
|
120
|
+
readonly lintRule?: InvariantLintRule | null;
|
|
121
|
+
/**
|
|
122
|
+
* Optional `cleo doctor` audit reference. R6 consumes this field to
|
|
123
|
+
* decide which invariants surface in `cleo doctor --audit-invariants`.
|
|
124
|
+
*/
|
|
125
|
+
readonly doctorAudit?: InvariantDoctorAudit | null;
|
|
126
|
+
/**
|
|
127
|
+
* Paths of test files that exercise the invariant. Used by R8 to render
|
|
128
|
+
* documentation pages with live test cross-references.
|
|
129
|
+
*/
|
|
130
|
+
readonly tests: readonly string[];
|
|
131
|
+
/**
|
|
132
|
+
* Optional flag — set to `true` when the invariant is superseded but
|
|
133
|
+
* preserved for historical/import-mapping reasons. Defaults to `false`.
|
|
134
|
+
*/
|
|
135
|
+
readonly deprecated?: boolean;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Compute the canonical registry key for an invariant.
|
|
140
|
+
*
|
|
141
|
+
* @internal
|
|
142
|
+
*/
|
|
143
|
+
function buildKey(invariant: RegisteredInvariant): string {
|
|
144
|
+
return `${invariant.adr}.${invariant.code}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Build the merged registry record from all per-ADR invariant modules.
|
|
149
|
+
*
|
|
150
|
+
* Future ADR modules (ADR-070 ORC codes via R2 T10336, etc.) append to
|
|
151
|
+
* the spread list below. Each module exports a `readonly RegisteredInvariant[]`
|
|
152
|
+
* — this index assembles them into a keyed record at module load time.
|
|
153
|
+
*
|
|
154
|
+
* @internal
|
|
155
|
+
*/
|
|
156
|
+
function buildRegistry(): Readonly<Record<string, RegisteredInvariant>> {
|
|
157
|
+
const entries: RegisteredInvariant[] = [
|
|
158
|
+
...ADR_073_INVARIANTS,
|
|
159
|
+
...ADR_056_INVARIANTS,
|
|
160
|
+
...ADR_070_INVARIANTS,
|
|
161
|
+
];
|
|
162
|
+
const record: Record<string, RegisteredInvariant> = {};
|
|
163
|
+
for (const entry of entries) {
|
|
164
|
+
const key = buildKey(entry);
|
|
165
|
+
if (record[key] !== undefined) {
|
|
166
|
+
throw new Error(`Duplicate invariant key registered: ${key}`);
|
|
167
|
+
}
|
|
168
|
+
record[key] = entry;
|
|
169
|
+
}
|
|
170
|
+
return Object.freeze(record);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Central invariants registry — keyed by `${adr}.${code}`.
|
|
175
|
+
*
|
|
176
|
+
* Treat as read-only at runtime. Mutating this record is a contract
|
|
177
|
+
* violation; downstream consumers (R5 release-registry refactor, R6
|
|
178
|
+
* doctor audit, R8 docs renderer) assume immutability.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* import { INVARIANTS_REGISTRY } from '@cleocode/contracts';
|
|
183
|
+
* const i3 = INVARIANTS_REGISTRY['ADR-073.I3'];
|
|
184
|
+
* if (i3?.runtimeGate) {
|
|
185
|
+
* console.log(`Guard: ${i3.runtimeGate.functionName}`);
|
|
186
|
+
* }
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export const INVARIANTS_REGISTRY: Readonly<Record<string, RegisteredInvariant>> = buildRegistry();
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Look up a single invariant by its `${adr}.${code}` key.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```ts
|
|
196
|
+
* const i3 = getInvariant('ADR-073.I3');
|
|
197
|
+
* // → RegisteredInvariant for ADR-073 invariant I3
|
|
198
|
+
* ```
|
|
199
|
+
*/
|
|
200
|
+
export function getInvariant(adrCode: string): RegisteredInvariant | undefined {
|
|
201
|
+
return INVARIANTS_REGISTRY[adrCode];
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Return every invariant registered against a given ADR, in declaration order.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* ```ts
|
|
209
|
+
* const adr073 = getInvariantsByAdr('ADR-073');
|
|
210
|
+
* // → array of I1..I8 entries
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
export function getInvariantsByAdr(adr: string): RegisteredInvariant[] {
|
|
214
|
+
const result: RegisteredInvariant[] = [];
|
|
215
|
+
for (const key of Object.keys(INVARIANTS_REGISTRY)) {
|
|
216
|
+
const entry = INVARIANTS_REGISTRY[key];
|
|
217
|
+
if (entry !== undefined && entry.adr === adr) {
|
|
218
|
+
result.push(entry);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
@@ -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
|
+
});
|
package/src/operations/docs.ts
CHANGED
|
@@ -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
|
/**
|