@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.
Files changed (118) 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/__tests__/evidence-atom-schema.test.d.ts +16 -0
  6. package/dist/__tests__/evidence-atom-schema.test.d.ts.map +1 -0
  7. package/dist/__tests__/evidence-atom-schema.test.js +394 -0
  8. package/dist/__tests__/evidence-atom-schema.test.js.map +1 -0
  9. package/dist/config/__tests__/manifest.test.d.ts +13 -0
  10. package/dist/config/__tests__/manifest.test.d.ts.map +1 -0
  11. package/dist/config/__tests__/manifest.test.js +143 -0
  12. package/dist/config/__tests__/manifest.test.js.map +1 -0
  13. package/dist/config/manifest.d.ts +158 -0
  14. package/dist/config/manifest.d.ts.map +1 -0
  15. package/dist/config/manifest.js +123 -0
  16. package/dist/config/manifest.js.map +1 -0
  17. package/dist/docs/__tests__/provenance.test.d.ts +14 -0
  18. package/dist/docs/__tests__/provenance.test.d.ts.map +1 -0
  19. package/dist/docs/__tests__/provenance.test.js +579 -0
  20. package/dist/docs/__tests__/provenance.test.js.map +1 -0
  21. package/dist/docs/provenance.d.ts +731 -0
  22. package/dist/docs/provenance.d.ts.map +1 -0
  23. package/dist/docs/provenance.js +258 -0
  24. package/dist/docs/provenance.js.map +1 -0
  25. package/dist/docs-taxonomy.d.ts +24 -0
  26. package/dist/docs-taxonomy.d.ts.map +1 -1
  27. package/dist/docs-taxonomy.js +27 -0
  28. package/dist/docs-taxonomy.js.map +1 -1
  29. package/dist/doctor.d.ts +101 -0
  30. package/dist/doctor.d.ts.map +1 -1
  31. package/dist/errors.d.ts +4 -0
  32. package/dist/errors.d.ts.map +1 -1
  33. package/dist/errors.js +4 -0
  34. package/dist/errors.js.map +1 -1
  35. package/dist/evidence-atom-schema.d.ts +407 -0
  36. package/dist/evidence-atom-schema.d.ts.map +1 -0
  37. package/dist/evidence-atom-schema.js +505 -0
  38. package/dist/evidence-atom-schema.js.map +1 -0
  39. package/dist/exit-codes.d.ts +37 -0
  40. package/dist/exit-codes.d.ts.map +1 -1
  41. package/dist/exit-codes.js +37 -0
  42. package/dist/exit-codes.js.map +1 -1
  43. package/dist/index.d.ts +13 -2
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +8 -1
  46. package/dist/index.js.map +1 -1
  47. package/dist/invariants/__tests__/adr-056-release.test.d.ts +16 -0
  48. package/dist/invariants/__tests__/adr-056-release.test.d.ts.map +1 -0
  49. package/dist/invariants/__tests__/adr-056-release.test.js +88 -0
  50. package/dist/invariants/__tests__/adr-056-release.test.js.map +1 -0
  51. package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts +22 -0
  52. package/dist/invariants/__tests__/adr-070-orchestration.test.d.ts.map +1 -0
  53. package/dist/invariants/__tests__/adr-070-orchestration.test.js +151 -0
  54. package/dist/invariants/__tests__/adr-070-orchestration.test.js.map +1 -0
  55. package/dist/invariants/__tests__/registry.test.d.ts +14 -0
  56. package/dist/invariants/__tests__/registry.test.d.ts.map +1 -0
  57. package/dist/invariants/__tests__/registry.test.js +113 -0
  58. package/dist/invariants/__tests__/registry.test.js.map +1 -0
  59. package/dist/invariants/adr-056-release.d.ts +42 -0
  60. package/dist/invariants/adr-056-release.d.ts.map +1 -0
  61. package/dist/invariants/adr-056-release.js +143 -0
  62. package/dist/invariants/adr-056-release.js.map +1 -0
  63. package/dist/invariants/adr-070-orchestration.d.ts +73 -0
  64. package/dist/invariants/adr-070-orchestration.d.ts.map +1 -0
  65. package/dist/invariants/adr-070-orchestration.js +290 -0
  66. package/dist/invariants/adr-070-orchestration.js.map +1 -0
  67. package/dist/invariants/adr-073-saga.d.ts +32 -0
  68. package/dist/invariants/adr-073-saga.d.ts.map +1 -0
  69. package/dist/invariants/adr-073-saga.js +155 -0
  70. package/dist/invariants/adr-073-saga.js.map +1 -0
  71. package/dist/invariants/index.d.ts +162 -0
  72. package/dist/invariants/index.d.ts.map +1 -0
  73. package/dist/invariants/index.js +118 -0
  74. package/dist/invariants/index.js.map +1 -0
  75. package/dist/operations/__tests__/input-contract.test.d.ts +20 -0
  76. package/dist/operations/__tests__/input-contract.test.d.ts.map +1 -0
  77. package/dist/operations/__tests__/input-contract.test.js +158 -0
  78. package/dist/operations/__tests__/input-contract.test.js.map +1 -0
  79. package/dist/operations/docs.d.ts +16 -0
  80. package/dist/operations/docs.d.ts.map +1 -1
  81. package/dist/operations/input-contract.d.ts +267 -0
  82. package/dist/operations/input-contract.d.ts.map +1 -0
  83. package/dist/operations/input-contract.js +29 -0
  84. package/dist/operations/input-contract.js.map +1 -0
  85. package/dist/templates/__tests__/manifest.test.d.ts +18 -0
  86. package/dist/templates/__tests__/manifest.test.d.ts.map +1 -0
  87. package/dist/templates/__tests__/manifest.test.js +123 -0
  88. package/dist/templates/__tests__/manifest.test.js.map +1 -0
  89. package/dist/templates/manifest.d.ts +186 -0
  90. package/dist/templates/manifest.d.ts.map +1 -0
  91. package/dist/templates/manifest.js +165 -0
  92. package/dist/templates/manifest.js.map +1 -0
  93. package/package.json +2 -2
  94. package/src/__tests__/cli-meta-suggested-next.test.ts +76 -0
  95. package/src/__tests__/evidence-atom-schema.test.ts +499 -0
  96. package/src/config/__tests__/manifest.test.ts +176 -0
  97. package/src/config/manifest.ts +218 -0
  98. package/src/docs/__tests__/__snapshots__/provenance.test.ts.snap +252 -0
  99. package/src/docs/__tests__/provenance.test.ts +648 -0
  100. package/src/docs/provenance.ts +601 -0
  101. package/src/docs-taxonomy.ts +55 -0
  102. package/src/doctor.ts +111 -0
  103. package/src/errors.ts +4 -0
  104. package/src/evidence-atom-schema.ts +628 -0
  105. package/src/exit-codes.ts +39 -0
  106. package/src/index.ts +129 -0
  107. package/src/invariants/__tests__/adr-056-release.test.ts +102 -0
  108. package/src/invariants/__tests__/adr-070-orchestration.test.ts +186 -0
  109. package/src/invariants/__tests__/registry.test.ts +136 -0
  110. package/src/invariants/adr-056-release.ts +154 -0
  111. package/src/invariants/adr-070-orchestration.ts +317 -0
  112. package/src/invariants/adr-073-saga.ts +167 -0
  113. package/src/invariants/index.ts +222 -0
  114. package/src/operations/__tests__/input-contract.test.ts +188 -0
  115. package/src/operations/docs.ts +16 -0
  116. package/src/operations/input-contract.ts +301 -0
  117. package/src/templates/__tests__/manifest.test.ts +140 -0
  118. 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
+ });
@@ -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
  /**