@cleocode/contracts 2026.5.112 → 2026.5.114
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/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/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 +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- 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/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/index.d.ts +4 -1
- package/dist/invariants/index.d.ts.map +1 -1
- package/dist/invariants/index.js +11 -2
- package/dist/invariants/index.js.map +1 -1
- 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/__tests__/tasks-input-contracts.test.d.ts +18 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.d.ts.map +1 -0
- package/dist/operations/__tests__/tasks-input-contracts.test.js +80 -0
- package/dist/operations/__tests__/tasks-input-contracts.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/operations/tasks.d.ts +96 -0
- package/dist/operations/tasks.d.ts.map +1 -1
- package/dist/operations/tasks.js +237 -1
- package/dist/operations/tasks.js.map +1 -1
- 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/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/exit-codes.ts +39 -0
- package/src/index.ts +102 -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/adr-056-release.ts +154 -0
- package/src/invariants/adr-070-orchestration.ts +317 -0
- package/src/invariants/index.ts +11 -2
- package/src/operations/__tests__/input-contract.test.ts +188 -0
- package/src/operations/__tests__/tasks-input-contracts.test.ts +106 -0
- package/src/operations/docs.ts +16 -0
- package/src/operations/input-contract.ts +301 -0
- package/src/operations/tasks.ts +308 -0
- package/src/templates/__tests__/manifest.test.ts +140 -0
- package/src/templates/manifest.ts +208 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TemplateManifest contract — SSoT for every template CLEO ships, installs,
|
|
3
|
+
* or rewrites during `cleo init` / `cleo upgrade` / `cleo scaffold-*`.
|
|
4
|
+
*
|
|
5
|
+
* A {@link TemplateManifestEntry} declares one template file: where its
|
|
6
|
+
* source lives in the monorepo, where it installs into a consumer project,
|
|
7
|
+
* how its placeholders are substituted, which placeholders it accepts, and
|
|
8
|
+
* how it MUST be reconciled when the shipped version moves ahead of the
|
|
9
|
+
* deployed copy.
|
|
10
|
+
*
|
|
11
|
+
* This contract is the leaf type. The CORE-side template registry
|
|
12
|
+
* (T9877) consumes it; the CLI verbs (`cleo init --workflows`,
|
|
13
|
+
* `cleo upgrade`, the scaffold sweeper) read the manifest at runtime to
|
|
14
|
+
* decide what to write, what to skip, and what to diff-prompt the user on.
|
|
15
|
+
*
|
|
16
|
+
* @example minimal workflow template entry
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { TemplateManifestEntrySchema, type TemplateManifestEntry } from '@cleocode/contracts';
|
|
19
|
+
*
|
|
20
|
+
* const entry: TemplateManifestEntry = TemplateManifestEntrySchema.parse({
|
|
21
|
+
* id: 'ci.yml',
|
|
22
|
+
* kind: 'workflow',
|
|
23
|
+
* sourcePath: 'packages/core/templates/workflows/ci.yml.tmpl',
|
|
24
|
+
* installPath: '.github/workflows/ci.yml',
|
|
25
|
+
* substitution: 'regex-tmpl',
|
|
26
|
+
* placeholders: [
|
|
27
|
+
* {
|
|
28
|
+
* name: 'NODE_VERSION',
|
|
29
|
+
* source: 'project-context',
|
|
30
|
+
* sourcePath: 'engines.node',
|
|
31
|
+
* defaultValue: 24,
|
|
32
|
+
* },
|
|
33
|
+
* ],
|
|
34
|
+
* updateStrategy: 'overwrite-on-bump',
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*
|
|
38
|
+
* @task T9875
|
|
39
|
+
* @epic T9874
|
|
40
|
+
* @saga T9855
|
|
41
|
+
* @see ADR-076 (canonical doc routing — same boundary discipline applies here)
|
|
42
|
+
*/
|
|
43
|
+
import { z } from 'zod';
|
|
44
|
+
// ─── TemplateKind ─────────────────────────────────────────────────────────────
|
|
45
|
+
/**
|
|
46
|
+
* Allowed categories of template the manifest can describe.
|
|
47
|
+
*
|
|
48
|
+
* - `workflow` — GitHub Actions / CI workflow files.
|
|
49
|
+
* - `config` — Configuration files (e.g. `.editorconfig`, `tsconfig.json`).
|
|
50
|
+
* - `agent` — Agent definition files shipped to `.claude/agents/` etc.
|
|
51
|
+
* - `skill` — Skill markdown shipped to `.claude/skills/` etc.
|
|
52
|
+
* - `provider` — Provider-specific bootstrap files (Anthropic, OpenAI, …).
|
|
53
|
+
* - `doc` — Documentation scaffolds (e.g. README.md, AGENTS.md).
|
|
54
|
+
*/
|
|
55
|
+
export const TEMPLATE_KINDS = ['workflow', 'config', 'agent', 'skill', 'provider', 'doc'];
|
|
56
|
+
// ─── TemplateSubstitution ─────────────────────────────────────────────────────
|
|
57
|
+
/**
|
|
58
|
+
* Allowed substitution strategies a template can request.
|
|
59
|
+
*
|
|
60
|
+
* - `regex-tmpl` — `{{KEY}}` placeholder substitution via regex.
|
|
61
|
+
* - `static` — No substitution; the file is copied byte-for-byte.
|
|
62
|
+
* - `json-merge` — The template is parsed as JSON and merged into any
|
|
63
|
+
* existing JSON file at `installPath`; placeholders inside
|
|
64
|
+
* string values still go through `regex-tmpl`.
|
|
65
|
+
*/
|
|
66
|
+
export const TEMPLATE_SUBSTITUTIONS = ['regex-tmpl', 'static', 'json-merge'];
|
|
67
|
+
// ─── UpdateStrategy ───────────────────────────────────────────────────────────
|
|
68
|
+
/**
|
|
69
|
+
* How an installed template is reconciled with the shipped copy on
|
|
70
|
+
* `cleo upgrade` / `cleo init --force`.
|
|
71
|
+
*
|
|
72
|
+
* - `overwrite-on-bump` — Always overwrite when the manifest version is
|
|
73
|
+
* newer than the deployed copy.
|
|
74
|
+
* - `diff-prompt` — Show a diff and ask the user before overwriting.
|
|
75
|
+
* - `immutable` — Never overwrite once installed.
|
|
76
|
+
* - `manifest-merge` — Apply structural merge (e.g. JSON deep-merge,
|
|
77
|
+
* respecting existing user keys).
|
|
78
|
+
*/
|
|
79
|
+
export const TEMPLATE_UPDATE_STRATEGIES = [
|
|
80
|
+
'overwrite-on-bump',
|
|
81
|
+
'diff-prompt',
|
|
82
|
+
'immutable',
|
|
83
|
+
'manifest-merge',
|
|
84
|
+
];
|
|
85
|
+
// ─── PlaceholderSpec ──────────────────────────────────────────────────────────
|
|
86
|
+
/**
|
|
87
|
+
* Allowed resolver sources a {@link PlaceholderSpec} can reference.
|
|
88
|
+
*
|
|
89
|
+
* - `project-context` — `.cleo/project-context.json` (detected ecosystem hints).
|
|
90
|
+
* - `project-info` — `.cleo/project-info.json` (per-project metadata).
|
|
91
|
+
* - `.cleo/config` — Per-project config (`<projectRoot>/.cleo/config.json`).
|
|
92
|
+
* - `~/.cleo/config` — User-global config (`$XDG_CONFIG_HOME/cleo/config.json`).
|
|
93
|
+
* - `tool-resolver` — Resolved via the per-language tool resolver
|
|
94
|
+
* (e.g. `pnpm` vs `npm` chosen by `primaryType`).
|
|
95
|
+
* - `literal` — Hard-coded value baked into the manifest entry.
|
|
96
|
+
*/
|
|
97
|
+
export const PLACEHOLDER_SOURCES = [
|
|
98
|
+
'project-context',
|
|
99
|
+
'project-info',
|
|
100
|
+
'.cleo/config',
|
|
101
|
+
'~/.cleo/config',
|
|
102
|
+
'tool-resolver',
|
|
103
|
+
'literal',
|
|
104
|
+
];
|
|
105
|
+
/**
|
|
106
|
+
* Zod schema for a single placeholder declaration on a template entry.
|
|
107
|
+
*
|
|
108
|
+
* Each placeholder is resolved at install time by reading `sourcePath` from
|
|
109
|
+
* the named `source`. If resolution returns `undefined`, the installer
|
|
110
|
+
* falls back to `defaultValue`; if neither yields a value, installation
|
|
111
|
+
* fails with `E_PLACEHOLDER_UNRESOLVED`.
|
|
112
|
+
*/
|
|
113
|
+
export const PlaceholderSpecSchema = z.object({
|
|
114
|
+
/**
|
|
115
|
+
* Placeholder identifier as it appears in the template body
|
|
116
|
+
* (e.g. `NODE_VERSION` matches `{{NODE_VERSION}}`).
|
|
117
|
+
*/
|
|
118
|
+
name: z.string().min(1, 'placeholder name must be non-empty'),
|
|
119
|
+
/** Resolver source the installer consults for this placeholder. */
|
|
120
|
+
source: z.enum(PLACEHOLDER_SOURCES),
|
|
121
|
+
/**
|
|
122
|
+
* Path expression evaluated against `source` (e.g. `engines.node` against
|
|
123
|
+
* `project-context`, `defaults.branchModel` against `.cleo/config`).
|
|
124
|
+
* For `literal` source, this MAY be the literal value's identifier.
|
|
125
|
+
*/
|
|
126
|
+
sourcePath: z.string().min(1, 'placeholder sourcePath must be non-empty'),
|
|
127
|
+
/**
|
|
128
|
+
* Fallback value used when `source[sourcePath]` resolves to `undefined`.
|
|
129
|
+
* `null` is permitted to explicitly mark "no default — failure required".
|
|
130
|
+
*/
|
|
131
|
+
defaultValue: z.union([z.string(), z.number(), z.boolean(), z.null()]).optional(),
|
|
132
|
+
});
|
|
133
|
+
// ─── TemplateManifestEntry ────────────────────────────────────────────────────
|
|
134
|
+
/**
|
|
135
|
+
* Zod schema for a single template manifest entry.
|
|
136
|
+
*
|
|
137
|
+
* Validates the full descriptor a template registry consumer needs to
|
|
138
|
+
* install a template file:
|
|
139
|
+
*
|
|
140
|
+
* - `id` — Stable identifier (kebab-case, matches install basename
|
|
141
|
+
* or a domain-unique slug).
|
|
142
|
+
* - `kind` — One of {@link TEMPLATE_KINDS}.
|
|
143
|
+
* - `sourcePath` — Repo-relative path of the template source.
|
|
144
|
+
* - `installPath` — Project-relative path where the rendered file lands.
|
|
145
|
+
* - `substitution` — One of {@link TEMPLATE_SUBSTITUTIONS}.
|
|
146
|
+
* - `placeholders` — Declared placeholders the installer must resolve.
|
|
147
|
+
* - `updateStrategy` — Reconciliation policy on upgrade.
|
|
148
|
+
*/
|
|
149
|
+
export const TemplateManifestEntrySchema = z.object({
|
|
150
|
+
/** Stable identifier for this template entry. */
|
|
151
|
+
id: z.string().min(1, 'id must be non-empty'),
|
|
152
|
+
/** Category of file this template represents. */
|
|
153
|
+
kind: z.enum(TEMPLATE_KINDS),
|
|
154
|
+
/** Repo-relative path of the template source file. */
|
|
155
|
+
sourcePath: z.string().min(1, 'sourcePath must be non-empty'),
|
|
156
|
+
/** Project-relative path where the rendered template installs. */
|
|
157
|
+
installPath: z.string().min(1, 'installPath must be non-empty'),
|
|
158
|
+
/** Substitution strategy the installer applies to `sourcePath`. */
|
|
159
|
+
substitution: z.enum(TEMPLATE_SUBSTITUTIONS),
|
|
160
|
+
/** Declared placeholders this template requires. May be empty. */
|
|
161
|
+
placeholders: z.array(PlaceholderSpecSchema),
|
|
162
|
+
/** Reconciliation policy on upgrade. */
|
|
163
|
+
updateStrategy: z.enum(TEMPLATE_UPDATE_STRATEGIES),
|
|
164
|
+
});
|
|
165
|
+
//# sourceMappingURL=manifest.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"manifest.js","sourceRoot":"","sources":["../../src/templates/manifest.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,iFAAiF;AAEjF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,UAAU,EAAE,KAAK,CAAU,CAAC;AAKnG,iFAAiF;AAEjF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,YAAY,EAAE,QAAQ,EAAE,YAAY,CAAU,CAAC;AAKtF,iFAAiF;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;IACxC,mBAAmB;IACnB,aAAa;IACb,WAAW;IACX,gBAAgB;CACR,CAAC;AAKX,iFAAiF;AAEjF;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,iBAAiB;IACjB,cAAc;IACd,cAAc;IACd,gBAAgB;IAChB,eAAe;IACf,SAAS;CACD,CAAC;AAKX;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,CAAC,MAAM,CAAC;IAC5C;;;OAGG;IACH,IAAI,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,oCAAoC,CAAC;IAC7D,mEAAmE;IACnE,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,mBAAmB,CAAC;IACnC;;;;OAIG;IACH,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,0CAA0C,CAAC;IACzE;;;OAGG;IACH,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE;CAClF,CAAC,CAAC;AAYH,iFAAiF;AAEjF;;;;;;;;;;;;;;GAcG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,iDAAiD;IACjD,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,sBAAsB,CAAC;IAC7C,iDAAiD;IACjD,IAAI,EAAE,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC;IAC5B,sDAAsD;IACtD,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,8BAA8B,CAAC;IAC7D,kEAAkE;IAClE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,+BAA+B,CAAC;IAC/D,mEAAmE;IACnE,YAAY,EAAE,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC;IAC5C,kEAAkE;IAClE,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,qBAAqB,CAAC;IAC5C,wCAAwC;IACxC,cAAc,EAAE,CAAC,CAAC,IAAI,CAAC,0BAA0B,CAAC;CACnD,CAAC,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cleocode/contracts",
|
|
3
|
-
"version": "2026.5.
|
|
3
|
+
"version": "2026.5.114",
|
|
4
4
|
"description": "Domain types, interfaces, and contracts for the CLEO ecosystem",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -138,7 +138,7 @@
|
|
|
138
138
|
"dependencies": {
|
|
139
139
|
"zod": "^4.3.6",
|
|
140
140
|
"zod-to-json-schema": "^3.25.2",
|
|
141
|
-
"@cleocode/lafs": "2026.5.
|
|
141
|
+
"@cleocode/lafs": "2026.5.114"
|
|
142
142
|
},
|
|
143
143
|
"scripts": {
|
|
144
144
|
"build": "tsc -b --force && node scripts/copy-assets.mjs && node scripts/emit-schemas.mjs",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Type-shape contract test for the envelope-wide `meta.suggestedNext`
|
|
3
|
+
* field promoted in T9920 (Saga T9855 / E8.1).
|
|
4
|
+
*
|
|
5
|
+
* `CliMeta.suggestedNext` is the canonical LLM-next-action hint
|
|
6
|
+
* surfaced on every CLI envelope. This test asserts the type allows
|
|
7
|
+
* the optional `ReadonlyArray<string>` shape and never widens to
|
|
8
|
+
* `string | null` or to nested objects (which would re-introduce the
|
|
9
|
+
* pre-T9920 nexus-internal-only structured form at the envelope tier).
|
|
10
|
+
*
|
|
11
|
+
* The runtime `attachSuggestedNext` helper lives in
|
|
12
|
+
* `packages/core/src/dispatch/suggested-next.ts` and is exercised by
|
|
13
|
+
* `packages/core/src/dispatch/__tests__/suggested-next.test.ts` —
|
|
14
|
+
* contracts cannot import core without breaking the layering contract.
|
|
15
|
+
*
|
|
16
|
+
* @epic T9919
|
|
17
|
+
* @task T9920
|
|
18
|
+
* @saga T9855
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import type { CliEnvelope, CliMeta } from '@cleocode/lafs';
|
|
22
|
+
import { describe, expect, expectTypeOf, it } from 'vitest';
|
|
23
|
+
|
|
24
|
+
describe('CliMeta.suggestedNext type shape (T9920)', () => {
|
|
25
|
+
it('allows ReadonlyArray<string>', () => {
|
|
26
|
+
const meta: CliMeta = {
|
|
27
|
+
operation: 'tasks.show',
|
|
28
|
+
requestId: '00000000-0000-0000-0000-000000000000',
|
|
29
|
+
duration_ms: 0,
|
|
30
|
+
timestamp: '2026-05-24T00:00:00.000Z',
|
|
31
|
+
suggestedNext: ['cleo focus T1234', 'cleo verify T1234'],
|
|
32
|
+
};
|
|
33
|
+
expect(meta.suggestedNext).toEqual(['cleo focus T1234', 'cleo verify T1234']);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it('is optional — omitting it leaves a valid CliMeta', () => {
|
|
37
|
+
const meta: CliMeta = {
|
|
38
|
+
operation: 'tasks.list',
|
|
39
|
+
requestId: '00000000-0000-0000-0000-000000000001',
|
|
40
|
+
duration_ms: 0,
|
|
41
|
+
timestamp: '2026-05-24T00:00:00.000Z',
|
|
42
|
+
};
|
|
43
|
+
expect(meta.suggestedNext).toBeUndefined();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('accepts an empty array (explicit "no follow-up suggested")', () => {
|
|
47
|
+
const meta: CliMeta = {
|
|
48
|
+
operation: 'tasks.show',
|
|
49
|
+
requestId: '00000000-0000-0000-0000-000000000002',
|
|
50
|
+
duration_ms: 0,
|
|
51
|
+
timestamp: '2026-05-24T00:00:00.000Z',
|
|
52
|
+
suggestedNext: [],
|
|
53
|
+
};
|
|
54
|
+
expect(meta.suggestedNext).toEqual([]);
|
|
55
|
+
expect(Array.isArray(meta.suggestedNext)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('typechecks as ReadonlyArray<string> — never `null`, never nested objects', () => {
|
|
59
|
+
expectTypeOf<CliMeta['suggestedNext']>().toEqualTypeOf<ReadonlyArray<string> | undefined>();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('flows through CliEnvelope<T>.meta verbatim', () => {
|
|
63
|
+
const envelope: CliEnvelope<{ taskId: string }> = {
|
|
64
|
+
success: true,
|
|
65
|
+
data: { taskId: 'T1234' },
|
|
66
|
+
meta: {
|
|
67
|
+
operation: 'tasks.show',
|
|
68
|
+
requestId: '00000000-0000-0000-0000-000000000003',
|
|
69
|
+
duration_ms: 1,
|
|
70
|
+
timestamp: '2026-05-24T00:00:00.000Z',
|
|
71
|
+
suggestedNext: ['cleo focus T1234'],
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
expect(envelope.meta.suggestedNext).toEqual(['cleo focus T1234']);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the ConfigManifest contract (T9876 / Saga T9855 / ADR-076).
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* - Schema round-trip for each of the 4 built-in entries.
|
|
6
|
+
* - Merge-precedence invariant (project > global > defaults).
|
|
7
|
+
* - Metadata-scope isolation (NOT in the merge chain).
|
|
8
|
+
* - {@link DriftDetection} exhaustiveness via a `never` fallback switch.
|
|
9
|
+
*
|
|
10
|
+
* @task T9876
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from 'vitest';
|
|
14
|
+
import {
|
|
15
|
+
CLEO_CONFIG_MANIFEST,
|
|
16
|
+
CONFIG_MANIFEST_ENTRIES,
|
|
17
|
+
type ConfigManifestEntry,
|
|
18
|
+
type ConfigScope,
|
|
19
|
+
configManifestEntrySchema,
|
|
20
|
+
type DriftDetection,
|
|
21
|
+
GLOBAL_CLEO_CONFIG_MANIFEST,
|
|
22
|
+
PROJECT_CONTEXT_MANIFEST,
|
|
23
|
+
PROJECT_INFO_MANIFEST,
|
|
24
|
+
} from '../manifest.js';
|
|
25
|
+
|
|
26
|
+
describe('ConfigManifest built-in entries', () => {
|
|
27
|
+
const builtins: ReadonlyArray<readonly [string, ConfigManifestEntry]> = [
|
|
28
|
+
['PROJECT_INFO_MANIFEST', PROJECT_INFO_MANIFEST],
|
|
29
|
+
['PROJECT_CONTEXT_MANIFEST', PROJECT_CONTEXT_MANIFEST],
|
|
30
|
+
['CLEO_CONFIG_MANIFEST', CLEO_CONFIG_MANIFEST],
|
|
31
|
+
['GLOBAL_CLEO_CONFIG_MANIFEST', GLOBAL_CLEO_CONFIG_MANIFEST],
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const [name, entry] of builtins) {
|
|
35
|
+
it(`${name} validates against configManifestEntrySchema`, () => {
|
|
36
|
+
const parsed = configManifestEntrySchema.parse(entry);
|
|
37
|
+
expect(parsed.id).toBe(entry.id);
|
|
38
|
+
expect(parsed.scope).toBe(entry.scope);
|
|
39
|
+
expect(parsed.path).toBe(entry.path);
|
|
40
|
+
expect(parsed.mergePrecedence).toBe(entry.mergePrecedence);
|
|
41
|
+
expect(parsed.driftDetection).toBe(entry.driftDetection);
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
it('CONFIG_MANIFEST_ENTRIES contains all 4 built-in entries', () => {
|
|
46
|
+
const ids = CONFIG_MANIFEST_ENTRIES.map((e) => e.id).sort();
|
|
47
|
+
expect(ids).toEqual([
|
|
48
|
+
'cleo-config-global',
|
|
49
|
+
'cleo-config-project',
|
|
50
|
+
'project-context',
|
|
51
|
+
'project-info',
|
|
52
|
+
]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('CONFIG_MANIFEST_ENTRIES is frozen', () => {
|
|
56
|
+
expect(Object.isFrozen(CONFIG_MANIFEST_ENTRIES)).toBe(true);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe('Merge-precedence invariant (project > global > defaults)', () => {
|
|
61
|
+
it('project precedence > global precedence (higher wins)', () => {
|
|
62
|
+
expect(CLEO_CONFIG_MANIFEST.mergePrecedence).toBeGreaterThan(
|
|
63
|
+
GLOBAL_CLEO_CONFIG_MANIFEST.mergePrecedence,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('canonical precedence values: project=20, global=10, metadata=0', () => {
|
|
68
|
+
expect(CLEO_CONFIG_MANIFEST.mergePrecedence).toBe(20);
|
|
69
|
+
expect(GLOBAL_CLEO_CONFIG_MANIFEST.mergePrecedence).toBe(10);
|
|
70
|
+
expect(PROJECT_INFO_MANIFEST.mergePrecedence).toBe(0);
|
|
71
|
+
expect(PROJECT_CONTEXT_MANIFEST.mergePrecedence).toBe(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('cascade-resolver sort order yields global then project (project wins)', () => {
|
|
75
|
+
const cascadeEntries = CONFIG_MANIFEST_ENTRIES.filter(
|
|
76
|
+
(e): e is ConfigManifestEntry & { scope: ConfigScope } =>
|
|
77
|
+
e.scope === 'global' || e.scope === 'project',
|
|
78
|
+
)
|
|
79
|
+
.slice() // copy before sort — array is frozen
|
|
80
|
+
.sort((a, b) => a.mergePrecedence - b.mergePrecedence);
|
|
81
|
+
|
|
82
|
+
expect(cascadeEntries.map((e) => e.scope)).toEqual(['global', 'project']);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('Metadata-scope isolation', () => {
|
|
87
|
+
it('metadata entries are NOT in the cascade merge chain', () => {
|
|
88
|
+
const cascadeScopes = CONFIG_MANIFEST_ENTRIES.filter((e) => e.scope !== 'metadata').map(
|
|
89
|
+
(e) => e.scope,
|
|
90
|
+
);
|
|
91
|
+
expect(cascadeScopes).toEqual(expect.arrayContaining(['global', 'project']));
|
|
92
|
+
expect(cascadeScopes).not.toContain('metadata');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('all metadata entries have mergePrecedence 0 (separate channel)', () => {
|
|
96
|
+
const metadataEntries = CONFIG_MANIFEST_ENTRIES.filter((e) => e.scope === 'metadata');
|
|
97
|
+
expect(metadataEntries.length).toBeGreaterThan(0);
|
|
98
|
+
for (const e of metadataEntries) {
|
|
99
|
+
expect(e.mergePrecedence).toBe(0);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('DriftDetection exhaustiveness (type-level)', () => {
|
|
105
|
+
it('switch over DriftDetection covers all variants with never fallback', () => {
|
|
106
|
+
const describe_ = (d: DriftDetection): string => {
|
|
107
|
+
switch (d) {
|
|
108
|
+
case 'schema-validate':
|
|
109
|
+
return 'schema-validate';
|
|
110
|
+
case 'staleness-gate':
|
|
111
|
+
return 'staleness-gate';
|
|
112
|
+
case 'value-diff':
|
|
113
|
+
return 'value-diff';
|
|
114
|
+
case 'none':
|
|
115
|
+
return 'none';
|
|
116
|
+
default: {
|
|
117
|
+
// If a new variant is added without updating this switch, TS will
|
|
118
|
+
// refuse to assign it to `never` at compile time.
|
|
119
|
+
const _exhaustive: never = d;
|
|
120
|
+
return _exhaustive;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
expect(describe_('schema-validate')).toBe('schema-validate');
|
|
126
|
+
expect(describe_('staleness-gate')).toBe('staleness-gate');
|
|
127
|
+
expect(describe_('value-diff')).toBe('value-diff');
|
|
128
|
+
expect(describe_('none')).toBe('none');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe('configManifestEntrySchema rejects invalid input', () => {
|
|
133
|
+
it('rejects empty id', () => {
|
|
134
|
+
const result = configManifestEntrySchema.safeParse({
|
|
135
|
+
id: '',
|
|
136
|
+
scope: 'project',
|
|
137
|
+
path: '.cleo/config.json',
|
|
138
|
+
mergePrecedence: 20,
|
|
139
|
+
driftDetection: 'schema-validate',
|
|
140
|
+
});
|
|
141
|
+
expect(result.success).toBe(false);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('rejects unknown scope', () => {
|
|
145
|
+
const result = configManifestEntrySchema.safeParse({
|
|
146
|
+
id: 'x',
|
|
147
|
+
scope: 'workspace',
|
|
148
|
+
path: '.cleo/x.json',
|
|
149
|
+
mergePrecedence: 0,
|
|
150
|
+
driftDetection: 'none',
|
|
151
|
+
});
|
|
152
|
+
expect(result.success).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it('rejects negative mergePrecedence', () => {
|
|
156
|
+
const result = configManifestEntrySchema.safeParse({
|
|
157
|
+
id: 'x',
|
|
158
|
+
scope: 'project',
|
|
159
|
+
path: '.cleo/x.json',
|
|
160
|
+
mergePrecedence: -1,
|
|
161
|
+
driftDetection: 'none',
|
|
162
|
+
});
|
|
163
|
+
expect(result.success).toBe(false);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('rejects unknown driftDetection variant', () => {
|
|
167
|
+
const result = configManifestEntrySchema.safeParse({
|
|
168
|
+
id: 'x',
|
|
169
|
+
scope: 'project',
|
|
170
|
+
path: '.cleo/x.json',
|
|
171
|
+
mergePrecedence: 0,
|
|
172
|
+
driftDetection: 'sha-compare',
|
|
173
|
+
});
|
|
174
|
+
expect(result.success).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ConfigManifest contract — declarative registry of every config-like file CLEO
|
|
3
|
+
* reads, writes, or merges, with its scope, schema, merge precedence, and the
|
|
4
|
+
* drift-detection strategy applied to it.
|
|
5
|
+
*
|
|
6
|
+
* Foundation for the CORE SSoT config registry (T9878) and the `resolveCleoConfig`
|
|
7
|
+
* cascade resolver. Built-in entries cover the four canonical CLEO files:
|
|
8
|
+
*
|
|
9
|
+
* - `~/.cleo/config.json` (`global`, precedence 10)
|
|
10
|
+
* - `<project>/.cleo/config.json` (`project`, precedence 20)
|
|
11
|
+
* - `<project>/.cleo/project-info.json` (`metadata`, separate channel)
|
|
12
|
+
* - `<project>/.cleo/project-context.json` (`metadata`, separate channel)
|
|
13
|
+
*
|
|
14
|
+
* Merge semantics: higher `mergePrecedence` wins. The `metadata` scope is
|
|
15
|
+
* intentionally OUTSIDE the project ↔ global merge chain — those files are
|
|
16
|
+
* read-only consumer state and must NEVER be merged into resolved CleoConfig.
|
|
17
|
+
*
|
|
18
|
+
* @task T9876
|
|
19
|
+
* @saga T9855
|
|
20
|
+
* @adr 076
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { type ZodTypeAny, z } from 'zod';
|
|
24
|
+
|
|
25
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
26
|
+
// Scope + drift unions
|
|
27
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Cascade-participating scopes for the project ↔ global merge chain.
|
|
31
|
+
*
|
|
32
|
+
* - `'global'` — `~/.cleo/config.json` (precedence 10)
|
|
33
|
+
* - `'project'` — `<project>/.cleo/config.json` (precedence 20, higher wins)
|
|
34
|
+
*
|
|
35
|
+
* Note: `'metadata'` is NOT part of this union — metadata-scoped files are
|
|
36
|
+
* read-only consumer state and live on a separate channel. See
|
|
37
|
+
* {@link ConfigManifestScope} for the full union including metadata.
|
|
38
|
+
*
|
|
39
|
+
* @task T9876
|
|
40
|
+
*/
|
|
41
|
+
export type ConfigScope = 'global' | 'project';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Full scope union covering cascade scopes AND the read-only `metadata`
|
|
45
|
+
* channel used by `project-info.json` and `project-context.json`.
|
|
46
|
+
*
|
|
47
|
+
* Metadata-scoped entries are NEVER merged into the resolved CleoConfig —
|
|
48
|
+
* they are surfaced separately as consumer state and validated only for
|
|
49
|
+
* schema conformance / staleness.
|
|
50
|
+
*
|
|
51
|
+
* @task T9876
|
|
52
|
+
*/
|
|
53
|
+
export type ConfigManifestScope = ConfigScope | 'metadata';
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Drift-detection strategy applied to a manifest entry.
|
|
57
|
+
*
|
|
58
|
+
* - `'schema-validate'` — Re-parse the file's contents against {@link ConfigManifestEntry.schema}
|
|
59
|
+
* on read. Any parse error is reported as drift.
|
|
60
|
+
* - `'staleness-gate'` — Compare the file's `detectedAt` (or equivalent freshness key)
|
|
61
|
+
* against a freshness threshold. Stale files are flagged but not rejected.
|
|
62
|
+
* - `'value-diff'` — Diff the file's resolved values against the manifest's
|
|
63
|
+
* `defaults` baseline and report changed keys. Useful for invariant tracking.
|
|
64
|
+
* - `'none'` — No drift detection. Entry is informational-only.
|
|
65
|
+
*
|
|
66
|
+
* @task T9876
|
|
67
|
+
*/
|
|
68
|
+
export type DriftDetection = 'schema-validate' | 'staleness-gate' | 'value-diff' | 'none';
|
|
69
|
+
|
|
70
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
71
|
+
// Manifest entry shape
|
|
72
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* A single registry entry describing one config-like file CLEO is aware of.
|
|
76
|
+
*
|
|
77
|
+
* `mergePrecedence` orders entries within the SAME scope channel. Across
|
|
78
|
+
* channels, the cascade-resolver consumes entries with `scope` in
|
|
79
|
+
* {@link ConfigScope} (global/project) and merges by ascending precedence
|
|
80
|
+
* (higher wins). Metadata entries are surfaced separately.
|
|
81
|
+
*
|
|
82
|
+
* @task T9876
|
|
83
|
+
*/
|
|
84
|
+
export interface ConfigManifestEntry {
|
|
85
|
+
/** Stable identifier for the entry (e.g. `'cleo-config-project'`). */
|
|
86
|
+
id: string;
|
|
87
|
+
/** Where the file lives in the precedence hierarchy. */
|
|
88
|
+
scope: ConfigManifestScope;
|
|
89
|
+
/** Absolute or `~`-rooted path to the file on disk. */
|
|
90
|
+
path: string;
|
|
91
|
+
/**
|
|
92
|
+
* Optional Zod schema used by the `'schema-validate'` drift detector to
|
|
93
|
+
* re-parse the file's contents. `null` disables schema validation even when
|
|
94
|
+
* `driftDetection === 'schema-validate'` (advisory-only mode).
|
|
95
|
+
*/
|
|
96
|
+
schema?: ZodTypeAny | null;
|
|
97
|
+
/**
|
|
98
|
+
* Numeric merge precedence within the cascade. Higher wins.
|
|
99
|
+
*
|
|
100
|
+
* Canonical values:
|
|
101
|
+
* - `0` — defaults / metadata (NOT part of the merge chain)
|
|
102
|
+
* - `10` — global
|
|
103
|
+
* - `20` — project
|
|
104
|
+
*/
|
|
105
|
+
mergePrecedence: number;
|
|
106
|
+
/** Drift-detection strategy to apply to this entry. */
|
|
107
|
+
driftDetection: DriftDetection;
|
|
108
|
+
/** Optional default values used as the baseline for `'value-diff'` drift detection. */
|
|
109
|
+
defaults?: Record<string, unknown>;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
113
|
+
// Built-in manifest entries (4 canonical CLEO files)
|
|
114
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Manifest entry for `<project>/.cleo/project-info.json`.
|
|
118
|
+
*
|
|
119
|
+
* Metadata channel (NOT part of the merge cascade). Schema-validated on
|
|
120
|
+
* read because the file is consumer state with a stable shape.
|
|
121
|
+
*
|
|
122
|
+
* @task T9876
|
|
123
|
+
*/
|
|
124
|
+
export const PROJECT_INFO_MANIFEST: ConfigManifestEntry = {
|
|
125
|
+
id: 'project-info',
|
|
126
|
+
scope: 'metadata',
|
|
127
|
+
path: '.cleo/project-info.json',
|
|
128
|
+
mergePrecedence: 0,
|
|
129
|
+
driftDetection: 'schema-validate',
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Manifest entry for `<project>/.cleo/project-context.json`.
|
|
134
|
+
*
|
|
135
|
+
* Metadata channel. Staleness-gated because the file is regenerated by
|
|
136
|
+
* project-detection scans and consumers care about freshness, not exact shape.
|
|
137
|
+
*
|
|
138
|
+
* @task T9876
|
|
139
|
+
*/
|
|
140
|
+
export const PROJECT_CONTEXT_MANIFEST: ConfigManifestEntry = {
|
|
141
|
+
id: 'project-context',
|
|
142
|
+
scope: 'metadata',
|
|
143
|
+
path: '.cleo/project-context.json',
|
|
144
|
+
mergePrecedence: 0,
|
|
145
|
+
driftDetection: 'staleness-gate',
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Manifest entry for `<project>/.cleo/config.json` (project-scoped CleoConfig).
|
|
150
|
+
*
|
|
151
|
+
* Project scope; precedence 20 — wins over global. Schema-validated.
|
|
152
|
+
*
|
|
153
|
+
* @task T9876
|
|
154
|
+
*/
|
|
155
|
+
export const CLEO_CONFIG_MANIFEST: ConfigManifestEntry = {
|
|
156
|
+
id: 'cleo-config-project',
|
|
157
|
+
scope: 'project',
|
|
158
|
+
path: '.cleo/config.json',
|
|
159
|
+
mergePrecedence: 20,
|
|
160
|
+
driftDetection: 'schema-validate',
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Manifest entry for `~/.cleo/config.json` (global CleoConfig).
|
|
165
|
+
*
|
|
166
|
+
* Global scope; precedence 10 — loses to project. Schema-validated.
|
|
167
|
+
*
|
|
168
|
+
* @task T9876
|
|
169
|
+
*/
|
|
170
|
+
export const GLOBAL_CLEO_CONFIG_MANIFEST: ConfigManifestEntry = {
|
|
171
|
+
id: 'cleo-config-global',
|
|
172
|
+
scope: 'global',
|
|
173
|
+
path: '~/.cleo/config.json',
|
|
174
|
+
mergePrecedence: 10,
|
|
175
|
+
driftDetection: 'schema-validate',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Frozen registry of all built-in manifest entries shipped with the contracts
|
|
180
|
+
* package. Order is informational only — consumers MUST sort by
|
|
181
|
+
* `mergePrecedence` when applying the cascade.
|
|
182
|
+
*
|
|
183
|
+
* @task T9876
|
|
184
|
+
*/
|
|
185
|
+
export const CONFIG_MANIFEST_ENTRIES: readonly ConfigManifestEntry[] = Object.freeze([
|
|
186
|
+
PROJECT_INFO_MANIFEST,
|
|
187
|
+
PROJECT_CONTEXT_MANIFEST,
|
|
188
|
+
GLOBAL_CLEO_CONFIG_MANIFEST,
|
|
189
|
+
CLEO_CONFIG_MANIFEST,
|
|
190
|
+
]);
|
|
191
|
+
|
|
192
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
193
|
+
// Runtime validation schema
|
|
194
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Zod schema for {@link ConfigManifestEntry}.
|
|
198
|
+
*
|
|
199
|
+
* Treats `schema` as `unknown` because `ZodTypeAny` instances cannot be
|
|
200
|
+
* validated through Zod itself without infinite recursion — schema-shape
|
|
201
|
+
* checking is the responsibility of the consumer that constructs the entry.
|
|
202
|
+
*
|
|
203
|
+
* @task T9876
|
|
204
|
+
*/
|
|
205
|
+
export const configManifestEntrySchema = z.object({
|
|
206
|
+
id: z.string().min(1),
|
|
207
|
+
scope: z.union([z.literal('global'), z.literal('project'), z.literal('metadata')]),
|
|
208
|
+
path: z.string().min(1),
|
|
209
|
+
schema: z.unknown().optional(),
|
|
210
|
+
mergePrecedence: z.number().int().nonnegative(),
|
|
211
|
+
driftDetection: z.union([
|
|
212
|
+
z.literal('schema-validate'),
|
|
213
|
+
z.literal('staleness-gate'),
|
|
214
|
+
z.literal('value-diff'),
|
|
215
|
+
z.literal('none'),
|
|
216
|
+
]),
|
|
217
|
+
defaults: z.record(z.string(), z.unknown()).optional(),
|
|
218
|
+
});
|