cclaw-cli 1.0.0 → 3.0.0
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/artifact-linter/brainstorm.js +15 -1
- package/dist/artifact-linter/design.js +14 -0
- package/dist/artifact-linter/scope.js +14 -0
- package/dist/artifact-linter/shared.d.ts +1 -0
- package/dist/artifact-linter/shared.js +32 -0
- package/dist/artifact-linter.js +13 -5
- package/dist/cli.js +2 -9
- package/dist/config.d.ts +11 -67
- package/dist/config.js +59 -649
- package/dist/content/hook-events.js +1 -5
- package/dist/content/hook-manifest.d.ts +6 -4
- package/dist/content/hook-manifest.js +16 -65
- package/dist/content/hooks.js +54 -14
- package/dist/content/meta-skill.js +4 -3
- package/dist/content/node-hooks.d.ts +0 -26
- package/dist/content/node-hooks.js +459 -157
- package/dist/content/observe.js +5 -4
- package/dist/content/opencode-plugin.js +1 -78
- package/dist/content/skills-elicitation.d.ts +1 -0
- package/dist/content/skills-elicitation.js +123 -0
- package/dist/content/skills.js +6 -4
- package/dist/content/stages/brainstorm.js +7 -3
- package/dist/content/stages/design.js +6 -2
- package/dist/content/stages/plan.js +2 -2
- package/dist/content/stages/scope.js +9 -5
- package/dist/content/stages/tdd.js +11 -11
- package/dist/content/start-command.js +4 -4
- package/dist/content/templates.js +21 -0
- package/dist/flow-state.d.ts +7 -0
- package/dist/flow-state.js +1 -0
- package/dist/gate-evidence.js +1 -5
- package/dist/hook-schema.js +3 -0
- package/dist/hook-schemas/claude-hooks.v1.json +2 -5
- package/dist/hook-schemas/codex-hooks.v1.json +1 -4
- package/dist/hook-schemas/cursor-hooks.v1.json +1 -3
- package/dist/install.d.ts +2 -7
- package/dist/install.js +32 -123
- package/dist/internal/advance-stage/advance.js +22 -1
- package/dist/internal/advance-stage/parsers.d.ts +1 -0
- package/dist/internal/advance-stage/parsers.js +6 -0
- package/dist/internal/compound-readiness.js +1 -16
- package/dist/internal/early-loop-status.js +1 -3
- package/dist/internal/runtime-integrity.js +0 -20
- package/dist/policy.js +6 -9
- package/dist/run-persistence.d.ts +1 -1
- package/dist/run-persistence.js +29 -2
- package/dist/runtime/run-hook.mjs +459 -265
- package/dist/tdd-verification-evidence.js +6 -18
- package/dist/track-heuristics.d.ts +7 -1
- package/dist/track-heuristics.js +12 -0
- package/dist/types.d.ts +0 -56
- package/package.json +1 -1
package/dist/config.js
CHANGED
|
@@ -2,219 +2,67 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { parse, stringify } from "yaml";
|
|
4
4
|
import { CCLAW_VERSION, DEFAULT_HARNESSES, FLOW_VERSION, RUNTIME_ROOT } from "./constants.js";
|
|
5
|
-
import { isIronLawId, normalizeStrictLawIds } from "./content/iron-laws.js";
|
|
6
5
|
import { exists, writeFileSafe } from "./fs-utils.js";
|
|
7
|
-
import {
|
|
6
|
+
import { HARNESS_IDS } from "./types.js";
|
|
8
7
|
const CONFIG_PATH = `${RUNTIME_ROOT}/config.yaml`;
|
|
9
8
|
const HARNESS_ID_SET = new Set(HARNESS_IDS);
|
|
10
|
-
const
|
|
11
|
-
const LANGUAGE_RULE_PACK_SET = new Set(LANGUAGE_RULE_PACKS);
|
|
9
|
+
const ALLOWED_CONFIG_KEYS = new Set(["version", "flowVersion", "harnesses"]);
|
|
12
10
|
const SUPPORTED_HARNESSES_TEXT = HARNESS_IDS.join(", ");
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"harnesses",
|
|
19
|
-
"vcs",
|
|
20
|
-
"strictness",
|
|
21
|
-
"tddTestGlobs",
|
|
22
|
-
"tdd",
|
|
23
|
-
"compound",
|
|
24
|
-
"earlyLoop",
|
|
25
|
-
"early_loop",
|
|
26
|
-
"gitHookGuards",
|
|
27
|
-
"defaultTrack",
|
|
28
|
-
"languageRulePacks",
|
|
29
|
-
"trackHeuristics",
|
|
30
|
-
"sliceReview",
|
|
31
|
-
"ironLaws",
|
|
32
|
-
"optInAudits",
|
|
33
|
-
"reviewLoop"
|
|
34
|
-
]);
|
|
35
|
-
/**
|
|
36
|
-
* Config keys removed in the advisory-by-default consolidation. Kept here so
|
|
37
|
-
* the parser can emit a helpful migration error pointing users at the new
|
|
38
|
-
* single `strictness` knob instead of a generic "unknown key" message.
|
|
39
|
-
*/
|
|
40
|
-
const RETIRED_GUARD_CONFIG_KEYS = new Set([
|
|
41
|
-
"promptGuardMode",
|
|
42
|
-
"tddEnforcement",
|
|
43
|
-
"workflowGuardMode"
|
|
44
|
-
]);
|
|
45
|
-
/**
|
|
46
|
-
* Config keys always present in the minimal init template. Everything else
|
|
47
|
-
* is "advanced" — parsed when present, but not pre-populated by `cclaw init`.
|
|
48
|
-
*
|
|
49
|
-
* Deliberately small: a first-time user should only see knobs they might
|
|
50
|
-
* actually flip. Power users override by adding more keys by hand; the
|
|
51
|
-
* reference lives in `docs/config.md`.
|
|
52
|
-
*/
|
|
53
|
-
const MINIMAL_CONFIG_KEYS = [
|
|
54
|
-
"version",
|
|
55
|
-
"flowVersion",
|
|
56
|
-
"harnesses",
|
|
57
|
-
"vcs",
|
|
58
|
-
"strictness",
|
|
59
|
-
"gitHookGuards"
|
|
11
|
+
// Kept for runtime modules that use these defaults directly.
|
|
12
|
+
export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
|
|
13
|
+
"**/*.test.*",
|
|
14
|
+
"**/tests/**",
|
|
15
|
+
"**/__tests__/**"
|
|
60
16
|
];
|
|
61
|
-
const
|
|
62
|
-
const
|
|
17
|
+
export const DEFAULT_TDD_TEST_GLOBS = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
|
|
18
|
+
export const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = [];
|
|
19
|
+
export const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
|
|
20
|
+
export const DEFAULT_EARLY_LOOP_MAX_ITERATIONS = 3;
|
|
63
21
|
export function createConfigWarningState() {
|
|
64
22
|
return { emitted: new Set() };
|
|
65
23
|
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
24
|
+
export class InvalidConfigError extends Error {
|
|
25
|
+
constructor(message) {
|
|
26
|
+
super(message);
|
|
27
|
+
this.name = "InvalidConfigError";
|
|
70
28
|
}
|
|
71
|
-
warningState.emitted.add(key);
|
|
72
|
-
process.emitWarning(message, { code });
|
|
73
|
-
}
|
|
74
|
-
function sameStringArray(a, b) {
|
|
75
|
-
if (!a || !b)
|
|
76
|
-
return false;
|
|
77
|
-
if (a.length !== b.length)
|
|
78
|
-
return false;
|
|
79
|
-
return a.every((value, index) => value === b[index]);
|
|
80
29
|
}
|
|
81
30
|
function configFixExample() {
|
|
82
31
|
return `harnesses:
|
|
83
32
|
- claude
|
|
84
33
|
- cursor`;
|
|
85
34
|
}
|
|
86
|
-
export class InvalidConfigError extends Error {
|
|
87
|
-
constructor(message) {
|
|
88
|
-
super(message);
|
|
89
|
-
this.name = "InvalidConfigError";
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
35
|
function configValidationError(configFilePath, reason) {
|
|
93
36
|
return new InvalidConfigError(`Invalid cclaw config at ${configFilePath}: ${reason}\n` +
|
|
94
37
|
`Supported harnesses: ${SUPPORTED_HARNESSES_TEXT}\n` +
|
|
95
|
-
`Supported tracks: ${SUPPORTED_TRACKS_TEXT}\n` +
|
|
96
|
-
`Supported languageRulePacks: ${SUPPORTED_LANGUAGE_RULE_PACKS_TEXT}\n` +
|
|
97
38
|
`Example config:\n${configFixExample()}\n` +
|
|
98
39
|
`After fixing, run: cclaw sync`);
|
|
99
40
|
}
|
|
100
41
|
function isRecord(value) {
|
|
101
42
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
102
43
|
}
|
|
103
|
-
function validateStringArray(value, fieldName, configFilePath) {
|
|
104
|
-
if (value === undefined)
|
|
105
|
-
return undefined;
|
|
106
|
-
if (!Array.isArray(value)) {
|
|
107
|
-
throw configValidationError(configFilePath, `"${fieldName}" must be an array of strings`);
|
|
108
|
-
}
|
|
109
|
-
const invalid = value.filter((item) => typeof item !== "string");
|
|
110
|
-
if (invalid.length > 0) {
|
|
111
|
-
throw configValidationError(configFilePath, `"${fieldName}" must contain only strings`);
|
|
112
|
-
}
|
|
113
|
-
return value;
|
|
114
|
-
}
|
|
115
44
|
export function configPath(projectRoot) {
|
|
116
45
|
return path.join(projectRoot, CONFIG_PATH);
|
|
117
46
|
}
|
|
118
|
-
|
|
119
|
-
* Default test-path patterns used by the workflow-guard hook to classify TDD writes.
|
|
120
|
-
*
|
|
121
|
-
* Scope is intentionally narrow and language-agnostic; users can extend this
|
|
122
|
-
* list in config when their repository uses different conventions.
|
|
123
|
-
*/
|
|
124
|
-
export const DEFAULT_TDD_TEST_PATH_PATTERNS = [
|
|
125
|
-
"**/*.test.*",
|
|
126
|
-
"**/tests/**",
|
|
127
|
-
"**/__tests__/**"
|
|
128
|
-
];
|
|
129
|
-
/**
|
|
130
|
-
* Legacy alias kept for backwards compatibility with `tddTestGlobs`.
|
|
131
|
-
* Prefer `tdd.testPathPatterns` in new configurations.
|
|
132
|
-
*/
|
|
133
|
-
export const DEFAULT_TDD_TEST_GLOBS = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
|
|
134
|
-
export const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = [];
|
|
135
|
-
export const DEFAULT_COMPOUND_RECURRENCE_THRESHOLD = 3;
|
|
136
|
-
export const DEFAULT_EARLY_LOOP_MAX_ITERATIONS = 3;
|
|
137
|
-
/**
|
|
138
|
-
* Populated runtime view of config values that downstream callers (install,
|
|
139
|
-
* observe, sync/runtime checks) consume. Always has the derived guard modes populated,
|
|
140
|
-
* regardless of whether the user wrote `strictness`, the legacy keys, both,
|
|
141
|
-
* or neither.
|
|
142
|
-
*/
|
|
143
|
-
export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, defaultTrack = "standard") {
|
|
144
|
-
const tddTestPathPatterns = [...DEFAULT_TDD_TEST_PATH_PATTERNS];
|
|
145
|
-
const tddProductionPathPatterns = [...DEFAULT_TDD_PRODUCTION_PATH_PATTERNS];
|
|
47
|
+
export function createDefaultConfig(harnesses = DEFAULT_HARNESSES, _defaultTrack = "standard") {
|
|
146
48
|
return {
|
|
147
49
|
version: CCLAW_VERSION,
|
|
148
50
|
flowVersion: FLOW_VERSION,
|
|
149
|
-
harnesses
|
|
150
|
-
vcs: "git-local-only",
|
|
151
|
-
strictness: "advisory",
|
|
152
|
-
tddTestGlobs: [...tddTestPathPatterns],
|
|
153
|
-
tdd: {
|
|
154
|
-
testPathPatterns: tddTestPathPatterns,
|
|
155
|
-
productionPathPatterns: tddProductionPathPatterns,
|
|
156
|
-
verificationRef: "auto"
|
|
157
|
-
},
|
|
158
|
-
compound: {
|
|
159
|
-
recurrenceThreshold: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD
|
|
160
|
-
},
|
|
161
|
-
earlyLoop: {
|
|
162
|
-
enabled: true,
|
|
163
|
-
maxIterations: DEFAULT_EARLY_LOOP_MAX_ITERATIONS
|
|
164
|
-
},
|
|
165
|
-
gitHookGuards: false,
|
|
166
|
-
defaultTrack,
|
|
167
|
-
languageRulePacks: [],
|
|
168
|
-
ironLaws: {
|
|
169
|
-
strictLaws: []
|
|
170
|
-
},
|
|
171
|
-
optInAudits: {
|
|
172
|
-
scopePreAudit: false,
|
|
173
|
-
staleDiagramAudit: true
|
|
174
|
-
}
|
|
51
|
+
harnesses: [...new Set(harnesses)]
|
|
175
52
|
};
|
|
176
53
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const detected = [];
|
|
188
|
-
const pkgPath = path.join(projectRoot, "package.json");
|
|
189
|
-
if (await exists(pkgPath)) {
|
|
190
|
-
try {
|
|
191
|
-
const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
|
|
192
|
-
const deps = {
|
|
193
|
-
...pkg.dependencies,
|
|
194
|
-
...pkg.devDependencies
|
|
195
|
-
};
|
|
196
|
-
if ("typescript" in deps || typeof pkg.types === "string") {
|
|
197
|
-
detected.push("typescript");
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
catch {
|
|
201
|
-
// Malformed package.json — skip; user can set the pack manually later.
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
const pythonMarkers = ["pyproject.toml", "requirements.txt", "setup.py", "Pipfile"];
|
|
205
|
-
for (const marker of pythonMarkers) {
|
|
206
|
-
if (await exists(path.join(projectRoot, marker))) {
|
|
207
|
-
detected.push("python");
|
|
208
|
-
break;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (await exists(path.join(projectRoot, "go.mod"))) {
|
|
212
|
-
detected.push("go");
|
|
213
|
-
}
|
|
214
|
-
return [...new Set(detected)];
|
|
54
|
+
function assertOnlySupportedKeys(parsed, fullPath) {
|
|
55
|
+
const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
|
|
56
|
+
if (unknownKeys.length === 0)
|
|
57
|
+
return;
|
|
58
|
+
const keyList = unknownKeys.join(", ");
|
|
59
|
+
throw configValidationError(fullPath, `key(s) ${keyList} are no longer supported in cclaw 3.0.0; see CHANGELOG.md`);
|
|
60
|
+
}
|
|
61
|
+
export async function detectLanguageRulePacks(_projectRoot) {
|
|
62
|
+
// Wave 21: harness-only config. Language packs are no longer configurable.
|
|
63
|
+
return [];
|
|
215
64
|
}
|
|
216
|
-
export async function readConfig(projectRoot,
|
|
217
|
-
const warningState = options.warningState ?? createConfigWarningState();
|
|
65
|
+
export async function readConfig(projectRoot, _options = {}) {
|
|
218
66
|
const fullPath = configPath(projectRoot);
|
|
219
67
|
if (!(await exists(fullPath))) {
|
|
220
68
|
return createDefaultConfig();
|
|
@@ -227,486 +75,48 @@ export async function readConfig(projectRoot, options = {}) {
|
|
|
227
75
|
const reason = error instanceof Error ? error.message : "unknown parse error";
|
|
228
76
|
throw configValidationError(fullPath, `failed to parse YAML (${reason})`);
|
|
229
77
|
}
|
|
230
|
-
if (parsedUnknown !== null && parsedUnknown !== undefined &&
|
|
78
|
+
if (parsedUnknown !== null && parsedUnknown !== undefined && !isRecord(parsedUnknown)) {
|
|
231
79
|
throw configValidationError(fullPath, "top-level config must be a YAML mapping/object");
|
|
232
80
|
}
|
|
233
|
-
const parsed = (parsedUnknown
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (retiredGuardKeys.length > 0) {
|
|
238
|
-
throw configValidationError(fullPath, `config key(s) ${retiredGuardKeys.join(", ")} were removed; ` +
|
|
239
|
-
`use the single \`strictness: advisory|strict\` knob instead ` +
|
|
240
|
-
`(advisory is the default). See docs/config.md#strictness for migration.`);
|
|
241
|
-
}
|
|
242
|
-
const unknownKeys = Object.keys(parsed).filter((key) => !ALLOWED_CONFIG_KEYS.has(key));
|
|
243
|
-
if (unknownKeys.length > 0) {
|
|
244
|
-
throw configValidationError(fullPath, `unknown top-level key(s): ${unknownKeys.join(", ")}`);
|
|
245
|
-
}
|
|
246
|
-
const hasHarnessesField = Object.prototype.hasOwnProperty.call(parsed, "harnesses");
|
|
247
|
-
if (hasHarnessesField && !Array.isArray(parsed.harnesses)) {
|
|
81
|
+
const parsed = (isRecord(parsedUnknown) ? parsedUnknown : {});
|
|
82
|
+
assertOnlySupportedKeys(parsed, fullPath);
|
|
83
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "harnesses") &&
|
|
84
|
+
!Array.isArray(parsed.harnesses)) {
|
|
248
85
|
throw configValidationError(fullPath, `"harnesses" must be an array`);
|
|
249
86
|
}
|
|
250
|
-
const
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
.join(", ");
|
|
256
|
-
throw configValidationError(fullPath, `unknown harness id(s): ${formatted}`);
|
|
257
|
-
}
|
|
258
|
-
const validatedHarnesses = configuredHarnesses;
|
|
259
|
-
if (hasHarnessesField && validatedHarnesses.length === 0) {
|
|
260
|
-
throw configValidationError(fullPath, `"harnesses" must include at least one harness`);
|
|
261
|
-
}
|
|
262
|
-
const harnesses = hasHarnessesField
|
|
263
|
-
? [...new Set(validatedHarnesses)]
|
|
264
|
-
: DEFAULT_HARNESSES;
|
|
265
|
-
const vcsRaw = parsed.vcs;
|
|
266
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "vcs") &&
|
|
267
|
-
vcsRaw !== "git-with-remote" &&
|
|
268
|
-
vcsRaw !== "git-local-only" &&
|
|
269
|
-
vcsRaw !== "none") {
|
|
270
|
-
throw configValidationError(fullPath, `"vcs" must be one of: git-with-remote, git-local-only, none`);
|
|
271
|
-
}
|
|
272
|
-
const vcs = vcsRaw === "git-with-remote" || vcsRaw === "git-local-only" || vcsRaw === "none"
|
|
273
|
-
? vcsRaw
|
|
274
|
-
: "git-local-only";
|
|
275
|
-
const strictnessRaw = parsed.strictness;
|
|
276
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "strictness") &&
|
|
277
|
-
strictnessRaw !== "advisory" &&
|
|
278
|
-
strictnessRaw !== "strict") {
|
|
279
|
-
throw configValidationError(fullPath, `"strictness" must be "advisory" or "strict"`);
|
|
280
|
-
}
|
|
281
|
-
const strictness = strictnessRaw === "strict" ? "strict" : "advisory";
|
|
282
|
-
const tddTestGlobsRaw = parsed.tddTestGlobs;
|
|
283
|
-
const tddTestGlobs = validateStringArray(tddTestGlobsRaw, "tddTestGlobs", fullPath)
|
|
284
|
-
?? [...DEFAULT_TDD_TEST_GLOBS];
|
|
285
|
-
const hasTddField = Object.prototype.hasOwnProperty.call(parsed, "tdd");
|
|
286
|
-
const tddRaw = parsed.tdd;
|
|
287
|
-
let explicitTddTestPathPatterns;
|
|
288
|
-
let explicitTddProductionPathPatterns;
|
|
289
|
-
let explicitTddVerificationRef;
|
|
290
|
-
if (hasTddField) {
|
|
291
|
-
if (!isRecord(tddRaw)) {
|
|
292
|
-
throw configValidationError(fullPath, `"tdd" must be an object`);
|
|
293
|
-
}
|
|
294
|
-
const unknownTddKeys = Object.keys(tddRaw).filter((key) => key !== "testPathPatterns" && key !== "productionPathPatterns" && key !== "verificationRef");
|
|
295
|
-
if (unknownTddKeys.length > 0) {
|
|
296
|
-
throw configValidationError(fullPath, `"tdd" has unknown key(s): ${unknownTddKeys.join(", ")}`);
|
|
87
|
+
const rawHarnesses = Array.isArray(parsed.harnesses) ? parsed.harnesses : DEFAULT_HARNESSES;
|
|
88
|
+
const normalizedHarnesses = [];
|
|
89
|
+
for (const harness of rawHarnesses) {
|
|
90
|
+
if (typeof harness !== "string" || !HARNESS_ID_SET.has(harness)) {
|
|
91
|
+
throw configValidationError(fullPath, `unknown harness id "${String(harness)}"`);
|
|
297
92
|
}
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
if (tddRaw.verificationRef !== undefined &&
|
|
301
|
-
tddRaw.verificationRef !== "auto" &&
|
|
302
|
-
tddRaw.verificationRef !== "required" &&
|
|
303
|
-
tddRaw.verificationRef !== "disabled") {
|
|
304
|
-
throw configValidationError(fullPath, '"tdd.verificationRef" must be one of: auto, required, disabled');
|
|
93
|
+
if (!normalizedHarnesses.includes(harness)) {
|
|
94
|
+
normalizedHarnesses.push(harness);
|
|
305
95
|
}
|
|
306
|
-
explicitTddVerificationRef = tddRaw.verificationRef;
|
|
307
|
-
}
|
|
308
|
-
if (tddTestGlobsRaw !== undefined &&
|
|
309
|
-
explicitTddTestPathPatterns !== undefined &&
|
|
310
|
-
!sameStringArray(tddTestGlobs, explicitTddTestPathPatterns)) {
|
|
311
|
-
emitConfigWarningOnce(warningState, "CCLAW_CONFIG_DEPRECATED_TDD_TEST_GLOBS", `[cclaw] Both "tddTestGlobs" (deprecated) and "tdd.testPathPatterns" are set in ${fullPath}. ` +
|
|
312
|
-
`Using "tdd.testPathPatterns".`);
|
|
313
96
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
];
|
|
317
|
-
const resolvedTddProductionPathPatterns = [
|
|
318
|
-
...(explicitTddProductionPathPatterns ?? DEFAULT_TDD_PRODUCTION_PATH_PATTERNS)
|
|
319
|
-
];
|
|
320
|
-
const hasCompoundField = Object.prototype.hasOwnProperty.call(parsed, "compound");
|
|
321
|
-
const compoundRaw = parsed.compound;
|
|
322
|
-
let compoundRecurrenceThreshold = DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
323
|
-
if (hasCompoundField) {
|
|
324
|
-
if (!isRecord(compoundRaw)) {
|
|
325
|
-
throw configValidationError(fullPath, `"compound" must be an object`);
|
|
326
|
-
}
|
|
327
|
-
const unknownCompoundKeys = Object.keys(compoundRaw).filter((key) => key !== "recurrenceThreshold");
|
|
328
|
-
if (unknownCompoundKeys.length > 0) {
|
|
329
|
-
throw configValidationError(fullPath, `"compound" has unknown key(s): ${unknownCompoundKeys.join(", ")}`);
|
|
330
|
-
}
|
|
331
|
-
if (compoundRaw.recurrenceThreshold !== undefined &&
|
|
332
|
-
(typeof compoundRaw.recurrenceThreshold !== "number" ||
|
|
333
|
-
!Number.isInteger(compoundRaw.recurrenceThreshold) ||
|
|
334
|
-
compoundRaw.recurrenceThreshold < 1)) {
|
|
335
|
-
throw configValidationError(fullPath, `"compound.recurrenceThreshold" must be a positive integer`);
|
|
336
|
-
}
|
|
337
|
-
if (typeof compoundRaw.recurrenceThreshold === "number") {
|
|
338
|
-
compoundRecurrenceThreshold = compoundRaw.recurrenceThreshold;
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
const hasEarlyLoopField = Object.prototype.hasOwnProperty.call(parsed, "earlyLoop");
|
|
342
|
-
const hasLegacyEarlyLoopField = Object.prototype.hasOwnProperty.call(parsed, "early_loop");
|
|
343
|
-
if (hasEarlyLoopField && hasLegacyEarlyLoopField) {
|
|
344
|
-
emitConfigWarningOnce(warningState, "CCLAW_CONFIG_EARLY_LOOP_ALIAS", `[cclaw] Both "earlyLoop" and legacy "early_loop" are set in ${fullPath}. Using "earlyLoop".`);
|
|
345
|
-
}
|
|
346
|
-
const earlyLoopRaw = hasEarlyLoopField
|
|
347
|
-
? parsed.earlyLoop
|
|
348
|
-
: parsed.early_loop;
|
|
349
|
-
let earlyLoopEnabled = true;
|
|
350
|
-
let earlyLoopMaxIterations = DEFAULT_EARLY_LOOP_MAX_ITERATIONS;
|
|
351
|
-
if (hasEarlyLoopField || hasLegacyEarlyLoopField) {
|
|
352
|
-
if (!isRecord(earlyLoopRaw)) {
|
|
353
|
-
throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}" must be an object`);
|
|
354
|
-
}
|
|
355
|
-
const unknownEarlyLoopKeys = Object.keys(earlyLoopRaw).filter((key) => key !== "enabled" && key !== "maxIterations" && key !== "max_iterations");
|
|
356
|
-
if (unknownEarlyLoopKeys.length > 0) {
|
|
357
|
-
throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}" has unknown key(s): ${unknownEarlyLoopKeys.join(", ")}`);
|
|
358
|
-
}
|
|
359
|
-
if (earlyLoopRaw.enabled !== undefined && typeof earlyLoopRaw.enabled !== "boolean") {
|
|
360
|
-
throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}.enabled" must be a boolean`);
|
|
361
|
-
}
|
|
362
|
-
if (earlyLoopRaw.maxIterations !== undefined &&
|
|
363
|
-
earlyLoopRaw.max_iterations !== undefined &&
|
|
364
|
-
earlyLoopRaw.maxIterations !== earlyLoopRaw.max_iterations) {
|
|
365
|
-
emitConfigWarningOnce(warningState, "CCLAW_CONFIG_EARLY_LOOP_MAX_ITERATIONS_ALIAS", `[cclaw] Both "${hasEarlyLoopField ? "earlyLoop.maxIterations" : "early_loop.maxIterations"}" and "${hasEarlyLoopField ? "earlyLoop.max_iterations" : "early_loop.max_iterations"}" are set in ${fullPath}. Using "maxIterations".`);
|
|
366
|
-
}
|
|
367
|
-
const rawMaxIterations = earlyLoopRaw.maxIterations ?? earlyLoopRaw.max_iterations;
|
|
368
|
-
if (rawMaxIterations !== undefined &&
|
|
369
|
-
(typeof rawMaxIterations !== "number" ||
|
|
370
|
-
!Number.isInteger(rawMaxIterations) ||
|
|
371
|
-
rawMaxIterations < 1)) {
|
|
372
|
-
throw configValidationError(fullPath, `"${hasEarlyLoopField ? "earlyLoop" : "early_loop"}.maxIterations" must be a positive integer`);
|
|
373
|
-
}
|
|
374
|
-
if (typeof earlyLoopRaw.enabled === "boolean") {
|
|
375
|
-
earlyLoopEnabled = earlyLoopRaw.enabled;
|
|
376
|
-
}
|
|
377
|
-
if (typeof rawMaxIterations === "number") {
|
|
378
|
-
earlyLoopMaxIterations = rawMaxIterations;
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
const gitHookGuardsRaw = parsed.gitHookGuards;
|
|
382
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "gitHookGuards") &&
|
|
383
|
-
typeof gitHookGuardsRaw !== "boolean") {
|
|
384
|
-
throw configValidationError(fullPath, `"gitHookGuards" must be a boolean`);
|
|
385
|
-
}
|
|
386
|
-
const gitHookGuards = typeof gitHookGuardsRaw === "boolean" ? gitHookGuardsRaw : false;
|
|
387
|
-
const defaultTrackRaw = parsed.defaultTrack;
|
|
388
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "defaultTrack") &&
|
|
389
|
-
(typeof defaultTrackRaw !== "string" || !FLOW_TRACK_SET.has(defaultTrackRaw))) {
|
|
390
|
-
throw configValidationError(fullPath, `"defaultTrack" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
|
|
391
|
-
}
|
|
392
|
-
const defaultTrack = typeof defaultTrackRaw === "string" && FLOW_TRACK_SET.has(defaultTrackRaw)
|
|
393
|
-
? defaultTrackRaw
|
|
394
|
-
: "standard";
|
|
395
|
-
const languageRulePacksRaw = parsed.languageRulePacks;
|
|
396
|
-
const hasLanguageRulePacksField = Object.prototype.hasOwnProperty.call(parsed, "languageRulePacks");
|
|
397
|
-
if (hasLanguageRulePacksField && !Array.isArray(languageRulePacksRaw)) {
|
|
398
|
-
throw configValidationError(fullPath, `"languageRulePacks" must be an array`);
|
|
399
|
-
}
|
|
400
|
-
const rawPacks = (languageRulePacksRaw ?? []);
|
|
401
|
-
const invalidPacks = rawPacks.filter((pack) => typeof pack !== "string" || !LANGUAGE_RULE_PACK_SET.has(pack));
|
|
402
|
-
if (invalidPacks.length > 0) {
|
|
403
|
-
const formatted = invalidPacks
|
|
404
|
-
.map((item) => (typeof item === "string" ? item : JSON.stringify(item)))
|
|
405
|
-
.join(", ");
|
|
406
|
-
throw configValidationError(fullPath, `unknown languageRulePacks id(s): ${formatted}`);
|
|
407
|
-
}
|
|
408
|
-
const languageRulePacks = [...new Set(rawPacks)];
|
|
409
|
-
const trackHeuristicsRaw = parsed.trackHeuristics;
|
|
410
|
-
let trackHeuristics = undefined;
|
|
411
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "trackHeuristics")) {
|
|
412
|
-
if (!isRecord(trackHeuristicsRaw)) {
|
|
413
|
-
throw configValidationError(fullPath, `"trackHeuristics" must be an object`);
|
|
414
|
-
}
|
|
415
|
-
const fallbackRaw = trackHeuristicsRaw.fallback;
|
|
416
|
-
if (fallbackRaw !== undefined && (typeof fallbackRaw !== "string" || !FLOW_TRACK_SET.has(fallbackRaw))) {
|
|
417
|
-
throw configValidationError(fullPath, `"trackHeuristics.fallback" must be one of: ${SUPPORTED_TRACKS_TEXT}`);
|
|
418
|
-
}
|
|
419
|
-
if (Object.prototype.hasOwnProperty.call(trackHeuristicsRaw, "priority")) {
|
|
420
|
-
throw configValidationError(fullPath, `"trackHeuristics.priority" is no longer supported (removed in v0.38.0). Track evaluation order is always standard -> medium -> quick. Remove the field to upgrade.`);
|
|
421
|
-
}
|
|
422
|
-
const tracksRaw = trackHeuristicsRaw.tracks;
|
|
423
|
-
let tracks = undefined;
|
|
424
|
-
if (tracksRaw !== undefined) {
|
|
425
|
-
if (!isRecord(tracksRaw)) {
|
|
426
|
-
throw configValidationError(fullPath, `"trackHeuristics.tracks" must be an object`);
|
|
427
|
-
}
|
|
428
|
-
tracks = {};
|
|
429
|
-
for (const [trackName, ruleRaw] of Object.entries(tracksRaw)) {
|
|
430
|
-
if (!FLOW_TRACK_SET.has(trackName)) {
|
|
431
|
-
throw configValidationError(fullPath, `"trackHeuristics.tracks" contains unknown track "${trackName}". Supported: ${SUPPORTED_TRACKS_TEXT}`);
|
|
432
|
-
}
|
|
433
|
-
if (!isRecord(ruleRaw)) {
|
|
434
|
-
throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}" must be an object`);
|
|
435
|
-
}
|
|
436
|
-
if (Object.prototype.hasOwnProperty.call(ruleRaw, "patterns")) {
|
|
437
|
-
throw configValidationError(fullPath, `"trackHeuristics.tracks.${trackName}.patterns" is no longer supported (removed in v0.38.0). Regex patterns were never wired into runtime routing. Move the intent into "triggers" (substrings) or "veto".`);
|
|
438
|
-
}
|
|
439
|
-
const triggers = validateStringArray(ruleRaw.triggers, `trackHeuristics.tracks.${trackName}.triggers`, fullPath);
|
|
440
|
-
const veto = validateStringArray(ruleRaw.veto, `trackHeuristics.tracks.${trackName}.veto`, fullPath);
|
|
441
|
-
tracks[trackName] = {
|
|
442
|
-
triggers,
|
|
443
|
-
veto
|
|
444
|
-
};
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
trackHeuristics = {
|
|
448
|
-
fallback: fallbackRaw,
|
|
449
|
-
tracks
|
|
450
|
-
};
|
|
451
|
-
}
|
|
452
|
-
const sliceReviewRaw = parsed.sliceReview;
|
|
453
|
-
let sliceReview = undefined;
|
|
454
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "sliceReview")) {
|
|
455
|
-
if (!isRecord(sliceReviewRaw)) {
|
|
456
|
-
throw configValidationError(fullPath, `"sliceReview" must be an object`);
|
|
457
|
-
}
|
|
458
|
-
const enabledRaw = sliceReviewRaw.enabled;
|
|
459
|
-
if (enabledRaw !== undefined && typeof enabledRaw !== "boolean") {
|
|
460
|
-
throw configValidationError(fullPath, `"sliceReview.enabled" must be a boolean`);
|
|
461
|
-
}
|
|
462
|
-
const thresholdRaw = sliceReviewRaw.filesChangedThreshold;
|
|
463
|
-
if (thresholdRaw !== undefined &&
|
|
464
|
-
(typeof thresholdRaw !== "number" || !Number.isInteger(thresholdRaw) || thresholdRaw < 1)) {
|
|
465
|
-
throw configValidationError(fullPath, `"sliceReview.filesChangedThreshold" must be a positive integer`);
|
|
466
|
-
}
|
|
467
|
-
const touchTriggers = validateStringArray(sliceReviewRaw.touchTriggers, "sliceReview.touchTriggers", fullPath);
|
|
468
|
-
const enforceRaw = sliceReviewRaw.enforceOnTracks;
|
|
469
|
-
let enforceOnTracks;
|
|
470
|
-
if (enforceRaw !== undefined) {
|
|
471
|
-
if (!Array.isArray(enforceRaw)) {
|
|
472
|
-
throw configValidationError(fullPath, `"sliceReview.enforceOnTracks" must be an array`);
|
|
473
|
-
}
|
|
474
|
-
const invalidTracks = enforceRaw.filter((value) => typeof value !== "string" || !FLOW_TRACK_SET.has(value));
|
|
475
|
-
if (invalidTracks.length > 0) {
|
|
476
|
-
throw configValidationError(fullPath, `"sliceReview.enforceOnTracks" must contain only: ${SUPPORTED_TRACKS_TEXT}`);
|
|
477
|
-
}
|
|
478
|
-
enforceOnTracks = [...new Set(enforceRaw)];
|
|
479
|
-
}
|
|
480
|
-
sliceReview = {
|
|
481
|
-
enabled: typeof enabledRaw === "boolean" ? enabledRaw : false,
|
|
482
|
-
filesChangedThreshold: typeof thresholdRaw === "number" ? thresholdRaw : DEFAULT_SLICE_REVIEW_THRESHOLD,
|
|
483
|
-
touchTriggers: touchTriggers ?? [],
|
|
484
|
-
enforceOnTracks: enforceOnTracks ?? DEFAULT_SLICE_REVIEW_TRACKS
|
|
485
|
-
};
|
|
486
|
-
}
|
|
487
|
-
const ironLawsRaw = parsed.ironLaws;
|
|
488
|
-
let ironLaws = undefined;
|
|
489
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "ironLaws")) {
|
|
490
|
-
if (!isRecord(ironLawsRaw)) {
|
|
491
|
-
throw configValidationError(fullPath, `"ironLaws" must be an object`);
|
|
492
|
-
}
|
|
493
|
-
if (Object.prototype.hasOwnProperty.call(ironLawsRaw, "mode")) {
|
|
494
|
-
throw configValidationError(fullPath, `"ironLaws.mode" was removed; the project-wide \`strictness\` knob now ` +
|
|
495
|
-
`controls iron-law enforcement. Use \`ironLaws.strictLaws\` for per-law overrides.`);
|
|
496
|
-
}
|
|
497
|
-
const unknownIronLawKeys = Object.keys(ironLawsRaw).filter((key) => key !== "strictLaws");
|
|
498
|
-
if (unknownIronLawKeys.length > 0) {
|
|
499
|
-
throw configValidationError(fullPath, `"ironLaws" has unknown key(s): ${unknownIronLawKeys.join(", ")}`);
|
|
500
|
-
}
|
|
501
|
-
const strictLawIdsRaw = validateStringArray(ironLawsRaw.strictLaws, "ironLaws.strictLaws", fullPath) ?? [];
|
|
502
|
-
const unknownStrictLawIds = strictLawIdsRaw.filter((id) => !isIronLawId(id));
|
|
503
|
-
if (unknownStrictLawIds.length > 0) {
|
|
504
|
-
throw configValidationError(fullPath, `"ironLaws.strictLaws" contains unknown law id(s): ${unknownStrictLawIds.join(", ")}`);
|
|
505
|
-
}
|
|
506
|
-
ironLaws = {
|
|
507
|
-
strictLaws: normalizeStrictLawIds(strictLawIdsRaw)
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
else {
|
|
511
|
-
ironLaws = { strictLaws: [] };
|
|
512
|
-
}
|
|
513
|
-
const optInAuditsRaw = parsed.optInAudits;
|
|
514
|
-
let optInAudits = undefined;
|
|
515
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "optInAudits")) {
|
|
516
|
-
if (!isRecord(optInAuditsRaw)) {
|
|
517
|
-
throw configValidationError(fullPath, `"optInAudits" must be an object`);
|
|
518
|
-
}
|
|
519
|
-
const unknownOptInAuditKeys = Object.keys(optInAuditsRaw).filter((key) => key !== "scopePreAudit" && key !== "staleDiagramAudit");
|
|
520
|
-
if (unknownOptInAuditKeys.length > 0) {
|
|
521
|
-
throw configValidationError(fullPath, `"optInAudits" has unknown key(s): ${unknownOptInAuditKeys.join(", ")}`);
|
|
522
|
-
}
|
|
523
|
-
if (optInAuditsRaw.scopePreAudit !== undefined &&
|
|
524
|
-
typeof optInAuditsRaw.scopePreAudit !== "boolean") {
|
|
525
|
-
throw configValidationError(fullPath, `"optInAudits.scopePreAudit" must be a boolean`);
|
|
526
|
-
}
|
|
527
|
-
if (optInAuditsRaw.staleDiagramAudit !== undefined &&
|
|
528
|
-
typeof optInAuditsRaw.staleDiagramAudit !== "boolean") {
|
|
529
|
-
throw configValidationError(fullPath, `"optInAudits.staleDiagramAudit" must be a boolean`);
|
|
530
|
-
}
|
|
531
|
-
optInAudits = {
|
|
532
|
-
scopePreAudit: typeof optInAuditsRaw.scopePreAudit === "boolean"
|
|
533
|
-
? optInAuditsRaw.scopePreAudit
|
|
534
|
-
: false,
|
|
535
|
-
staleDiagramAudit: typeof optInAuditsRaw.staleDiagramAudit === "boolean"
|
|
536
|
-
? optInAuditsRaw.staleDiagramAudit
|
|
537
|
-
: true
|
|
538
|
-
};
|
|
539
|
-
}
|
|
540
|
-
if (!optInAudits) {
|
|
541
|
-
optInAudits = {
|
|
542
|
-
scopePreAudit: false,
|
|
543
|
-
staleDiagramAudit: true
|
|
544
|
-
};
|
|
545
|
-
}
|
|
546
|
-
const reviewLoopRaw = parsed.reviewLoop;
|
|
547
|
-
let reviewLoop = undefined;
|
|
548
|
-
if (Object.prototype.hasOwnProperty.call(parsed, "reviewLoop")) {
|
|
549
|
-
if (!isRecord(reviewLoopRaw)) {
|
|
550
|
-
throw configValidationError(fullPath, `"reviewLoop" must be an object`);
|
|
551
|
-
}
|
|
552
|
-
const unknownReviewLoopKeys = Object.keys(reviewLoopRaw).filter((key) => key !== "externalSecondOpinion");
|
|
553
|
-
if (unknownReviewLoopKeys.length > 0) {
|
|
554
|
-
throw configValidationError(fullPath, `"reviewLoop" has unknown key(s): ${unknownReviewLoopKeys.join(", ")}`);
|
|
555
|
-
}
|
|
556
|
-
const externalRaw = reviewLoopRaw.externalSecondOpinion;
|
|
557
|
-
let externalSecondOpinion = undefined;
|
|
558
|
-
if (externalRaw !== undefined) {
|
|
559
|
-
if (!isRecord(externalRaw)) {
|
|
560
|
-
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" must be an object`);
|
|
561
|
-
}
|
|
562
|
-
const unknownExternalKeys = Object.keys(externalRaw).filter((key) => key !== "enabled" && key !== "model" && key !== "scoreDeltaThreshold");
|
|
563
|
-
if (unknownExternalKeys.length > 0) {
|
|
564
|
-
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion" has unknown key(s): ${unknownExternalKeys.join(", ")}`);
|
|
565
|
-
}
|
|
566
|
-
if (externalRaw.enabled !== undefined && typeof externalRaw.enabled !== "boolean") {
|
|
567
|
-
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.enabled" must be a boolean`);
|
|
568
|
-
}
|
|
569
|
-
if (externalRaw.model !== undefined && typeof externalRaw.model !== "string") {
|
|
570
|
-
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.model" must be a string`);
|
|
571
|
-
}
|
|
572
|
-
if (externalRaw.scoreDeltaThreshold !== undefined &&
|
|
573
|
-
(typeof externalRaw.scoreDeltaThreshold !== "number" ||
|
|
574
|
-
Number.isNaN(externalRaw.scoreDeltaThreshold) ||
|
|
575
|
-
externalRaw.scoreDeltaThreshold < 0 ||
|
|
576
|
-
externalRaw.scoreDeltaThreshold > 1)) {
|
|
577
|
-
throw configValidationError(fullPath, `"reviewLoop.externalSecondOpinion.scoreDeltaThreshold" must be a number between 0 and 1`);
|
|
578
|
-
}
|
|
579
|
-
externalSecondOpinion = {
|
|
580
|
-
enabled: externalRaw.enabled === true,
|
|
581
|
-
model: typeof externalRaw.model === "string" ? externalRaw.model : undefined,
|
|
582
|
-
scoreDeltaThreshold: typeof externalRaw.scoreDeltaThreshold === "number"
|
|
583
|
-
? externalRaw.scoreDeltaThreshold
|
|
584
|
-
: 0.2
|
|
585
|
-
};
|
|
586
|
-
}
|
|
587
|
-
reviewLoop = { externalSecondOpinion };
|
|
97
|
+
if (normalizedHarnesses.length === 0) {
|
|
98
|
+
throw configValidationError(fullPath, `"harnesses" must include at least one harness`);
|
|
588
99
|
}
|
|
100
|
+
const version = typeof parsed.version === "string" && parsed.version.trim().length > 0
|
|
101
|
+
? parsed.version
|
|
102
|
+
: CCLAW_VERSION;
|
|
103
|
+
const flowVersion = typeof parsed.flowVersion === "string" && parsed.flowVersion.trim().length > 0
|
|
104
|
+
? parsed.flowVersion
|
|
105
|
+
: FLOW_VERSION;
|
|
589
106
|
return {
|
|
590
|
-
version
|
|
591
|
-
flowVersion
|
|
592
|
-
harnesses
|
|
593
|
-
vcs,
|
|
594
|
-
strictness,
|
|
595
|
-
tddTestGlobs,
|
|
596
|
-
tdd: {
|
|
597
|
-
testPathPatterns: resolvedTddTestPathPatterns,
|
|
598
|
-
productionPathPatterns: resolvedTddProductionPathPatterns,
|
|
599
|
-
verificationRef: explicitTddVerificationRef ?? "auto"
|
|
600
|
-
},
|
|
601
|
-
compound: {
|
|
602
|
-
recurrenceThreshold: compoundRecurrenceThreshold
|
|
603
|
-
},
|
|
604
|
-
earlyLoop: {
|
|
605
|
-
enabled: earlyLoopEnabled,
|
|
606
|
-
maxIterations: earlyLoopMaxIterations
|
|
607
|
-
},
|
|
608
|
-
gitHookGuards,
|
|
609
|
-
defaultTrack,
|
|
610
|
-
languageRulePacks,
|
|
611
|
-
trackHeuristics,
|
|
612
|
-
sliceReview,
|
|
613
|
-
ironLaws,
|
|
614
|
-
optInAudits,
|
|
615
|
-
reviewLoop
|
|
107
|
+
version,
|
|
108
|
+
flowVersion,
|
|
109
|
+
harnesses: normalizedHarnesses
|
|
616
110
|
};
|
|
617
111
|
}
|
|
618
|
-
function
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
const output = {};
|
|
625
|
-
const ordered = [
|
|
626
|
-
"version",
|
|
627
|
-
"flowVersion",
|
|
628
|
-
"harnesses",
|
|
629
|
-
"vcs",
|
|
630
|
-
"strictness",
|
|
631
|
-
"tddTestGlobs",
|
|
632
|
-
"tdd",
|
|
633
|
-
"compound",
|
|
634
|
-
"earlyLoop",
|
|
635
|
-
"gitHookGuards",
|
|
636
|
-
"defaultTrack",
|
|
637
|
-
"languageRulePacks",
|
|
638
|
-
"trackHeuristics",
|
|
639
|
-
"sliceReview",
|
|
640
|
-
"ironLaws",
|
|
641
|
-
"optInAudits",
|
|
642
|
-
"reviewLoop"
|
|
643
|
-
];
|
|
644
|
-
for (const key of ordered) {
|
|
645
|
-
const value = config[key];
|
|
646
|
-
if (value === undefined)
|
|
647
|
-
continue;
|
|
648
|
-
if (mode === "full") {
|
|
649
|
-
output[key] = value;
|
|
650
|
-
continue;
|
|
651
|
-
}
|
|
652
|
-
// Minimal mode: always include the short list; advanced keys only when
|
|
653
|
-
// the caller explicitly opted in, or for auto-detected non-empty
|
|
654
|
-
// `languageRulePacks`.
|
|
655
|
-
if (isMinimalKey(key)) {
|
|
656
|
-
output[key] = value;
|
|
657
|
-
continue;
|
|
658
|
-
}
|
|
659
|
-
if (advanced?.has(key)) {
|
|
660
|
-
output[key] = value;
|
|
661
|
-
continue;
|
|
662
|
-
}
|
|
663
|
-
if (key === "languageRulePacks" && Array.isArray(value) && value.length > 0) {
|
|
664
|
-
output[key] = value;
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return output;
|
|
668
|
-
}
|
|
669
|
-
export async function writeConfig(projectRoot, config, options = {}) {
|
|
670
|
-
const serialisable = buildSerializableConfig(config, options);
|
|
112
|
+
export async function writeConfig(projectRoot, config, _options = {}) {
|
|
113
|
+
const serialisable = {
|
|
114
|
+
version: config.version,
|
|
115
|
+
flowVersion: config.flowVersion,
|
|
116
|
+
harnesses: config.harnesses
|
|
117
|
+
};
|
|
671
118
|
await writeFileSafe(configPath(projectRoot), stringify(serialisable));
|
|
672
119
|
}
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
* Used by `cclaw upgrade` to preserve the user's existing shape — if they
|
|
676
|
-
* wrote `tddTestGlobs` by hand, the upgrade keeps it; if they didn't, the
|
|
677
|
-
* upgrade stays minimal.
|
|
678
|
-
*/
|
|
679
|
-
export async function detectAdvancedKeys(projectRoot) {
|
|
680
|
-
const fullPath = configPath(projectRoot);
|
|
681
|
-
if (!(await exists(fullPath)))
|
|
682
|
-
return new Set();
|
|
683
|
-
try {
|
|
684
|
-
const parsedUnknown = parse(await fs.readFile(fullPath, "utf8"));
|
|
685
|
-
if (!isRecord(parsedUnknown))
|
|
686
|
-
return new Set();
|
|
687
|
-
const advancedCandidates = [
|
|
688
|
-
"tddTestGlobs",
|
|
689
|
-
"tdd",
|
|
690
|
-
"compound",
|
|
691
|
-
"earlyLoop",
|
|
692
|
-
"defaultTrack",
|
|
693
|
-
"languageRulePacks",
|
|
694
|
-
"trackHeuristics",
|
|
695
|
-
"sliceReview",
|
|
696
|
-
"ironLaws",
|
|
697
|
-
"optInAudits",
|
|
698
|
-
"reviewLoop"
|
|
699
|
-
];
|
|
700
|
-
const present = new Set();
|
|
701
|
-
for (const key of advancedCandidates) {
|
|
702
|
-
if (Object.prototype.hasOwnProperty.call(parsedUnknown, key) ||
|
|
703
|
-
(key === "earlyLoop" && Object.prototype.hasOwnProperty.call(parsedUnknown, "early_loop"))) {
|
|
704
|
-
present.add(key);
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
return present;
|
|
708
|
-
}
|
|
709
|
-
catch {
|
|
710
|
-
return new Set();
|
|
711
|
-
}
|
|
120
|
+
export async function detectAdvancedKeys(_projectRoot) {
|
|
121
|
+
return new Set();
|
|
712
122
|
}
|