cclaw-cli 0.48.21 → 0.48.22
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/content/node-hooks.d.ts +7 -0
- package/dist/content/node-hooks.js +78 -11
- package/dist/install.js +2 -1
- package/dist/internal/compound-readiness.d.ts +8 -0
- package/dist/internal/compound-readiness.js +44 -3
- package/dist/knowledge-store.d.ts +41 -2
- package/dist/knowledge-store.js +31 -2
- package/package.json +1 -1
|
@@ -8,6 +8,13 @@ export interface NodeHookRuntimeOptions {
|
|
|
8
8
|
strictness?: "advisory" | "strict";
|
|
9
9
|
tddTestPathPatterns?: string[];
|
|
10
10
|
tddProductionPathPatterns?: string[];
|
|
11
|
+
/**
|
|
12
|
+
* Baked-in default recurrence threshold for compound-readiness computed
|
|
13
|
+
* by the session-start hook. Derived from
|
|
14
|
+
* `config.compound.recurrenceThreshold` at install time; re-run
|
|
15
|
+
* `cclaw sync` after changing the config value so hook and CLI agree.
|
|
16
|
+
*/
|
|
17
|
+
compoundRecurrenceThreshold?: number;
|
|
11
18
|
}
|
|
12
19
|
/**
|
|
13
20
|
* Node-only hook runtime (single entrypoint).
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { DEFAULT_COMPOUND_RECURRENCE_THRESHOLD } from "../config.js";
|
|
1
2
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
3
|
+
import { SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD, SMALL_PROJECT_RECURRENCE_THRESHOLD } from "../knowledge-store.js";
|
|
2
4
|
function normalizePatterns(patterns, fallback) {
|
|
3
5
|
if (!patterns || patterns.length === 0)
|
|
4
6
|
return [...fallback];
|
|
@@ -18,6 +20,11 @@ export function nodeHookRuntimeScript(options = {}) {
|
|
|
18
20
|
"**/__tests__/**"
|
|
19
21
|
]);
|
|
20
22
|
const tddProductionPathPatterns = normalizePatterns(options.tddProductionPathPatterns, []);
|
|
23
|
+
const compoundRecurrenceThreshold = typeof options.compoundRecurrenceThreshold === "number" &&
|
|
24
|
+
Number.isInteger(options.compoundRecurrenceThreshold) &&
|
|
25
|
+
options.compoundRecurrenceThreshold >= 1
|
|
26
|
+
? options.compoundRecurrenceThreshold
|
|
27
|
+
: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
21
28
|
return `#!/usr/bin/env node
|
|
22
29
|
import fs from "node:fs/promises";
|
|
23
30
|
import path from "node:path";
|
|
@@ -31,6 +38,14 @@ const RUNTIME_ROOT = ${JSON.stringify(RUNTIME_ROOT)};
|
|
|
31
38
|
const DEFAULT_STRICTNESS = ${JSON.stringify(strictness)};
|
|
32
39
|
const DEFAULT_TDD_TEST_PATH_PATTERNS = ${JSON.stringify(tddTestPathPatterns)};
|
|
33
40
|
const DEFAULT_TDD_PRODUCTION_PATH_PATTERNS = ${JSON.stringify(tddProductionPathPatterns)};
|
|
41
|
+
// Compound-readiness recurrence threshold. Baked from
|
|
42
|
+
// \`config.compound.recurrenceThreshold\` at install time so the hook and
|
|
43
|
+
// \`cclaw internal compound-readiness\` agree on the same number. The
|
|
44
|
+
// small-project relaxation rule (<${SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD} archived runs
|
|
45
|
+
// -> min(base, ${SMALL_PROJECT_RECURRENCE_THRESHOLD})) is applied at runtime.
|
|
46
|
+
const COMPOUND_RECURRENCE_THRESHOLD = ${JSON.stringify(compoundRecurrenceThreshold)};
|
|
47
|
+
const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD)};
|
|
48
|
+
const SMALL_PROJECT_RECURRENCE_THRESHOLD = ${JSON.stringify(SMALL_PROJECT_RECURRENCE_THRESHOLD)};
|
|
34
49
|
|
|
35
50
|
function resolveStrictness() {
|
|
36
51
|
return process.env.CCLAW_STRICTNESS === "strict" ? "strict" : DEFAULT_STRICTNESS;
|
|
@@ -752,10 +767,14 @@ function stageSuggestion(stage) {
|
|
|
752
767
|
return map[stage] || "";
|
|
753
768
|
}
|
|
754
769
|
|
|
755
|
-
async function buildKnowledgeDigest(root, currentStage) {
|
|
770
|
+
async function buildKnowledgeDigest(root, currentStage, prereadRaw) {
|
|
756
771
|
const knowledgeFile = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
757
772
|
const digestFile = path.join(root, RUNTIME_ROOT, "state", "knowledge-digest.md");
|
|
758
|
-
|
|
773
|
+
// Caller may supply pre-read raw bytes to avoid re-reading knowledge.jsonl.
|
|
774
|
+
// Falls back to a local read if nothing is passed in.
|
|
775
|
+
const raw = typeof prereadRaw === "string"
|
|
776
|
+
? prereadRaw
|
|
777
|
+
: await readTextFile(knowledgeFile, "");
|
|
759
778
|
const lines = raw.split(/\\r?\\n/gu).map((line) => line.trim()).filter((line) => line.length > 0);
|
|
760
779
|
let learningsCount = 0;
|
|
761
780
|
const parsedRows = [];
|
|
@@ -885,7 +904,11 @@ async function handleSessionStart(runtime) {
|
|
|
885
904
|
const sessionDigest = (await readTextFile(sessionDigestFile, "")).trim();
|
|
886
905
|
const activitySummary = await readRecentActivityLines(activityFile);
|
|
887
906
|
const contextWarning = await readLatestContextWarningLine(contextWarningsFile);
|
|
888
|
-
|
|
907
|
+
// Read knowledge.jsonl exactly once per session-start; both the digest
|
|
908
|
+
// and compound-readiness derive from the same snapshot.
|
|
909
|
+
const knowledgeFilePath = path.join(runtime.root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
910
|
+
const knowledgeRaw = await readTextFile(knowledgeFilePath, "");
|
|
911
|
+
const knowledge = await buildKnowledgeDigest(runtime.root, state.currentStage, knowledgeRaw);
|
|
889
912
|
|
|
890
913
|
// Refresh Ralph Loop status each session-start so /cc-next and the model
|
|
891
914
|
// both read a consistent "iter=N, acClosed=[...]" snapshot. Runs only when
|
|
@@ -912,7 +935,11 @@ async function handleSessionStart(runtime) {
|
|
|
912
935
|
// where lifting becomes relevant; earlier stages update the file silently.
|
|
913
936
|
let compoundReadinessLine = "";
|
|
914
937
|
try {
|
|
915
|
-
const
|
|
938
|
+
const archivedRunsCount = await countArchivedRunsInline(runtime.root);
|
|
939
|
+
const readiness = await computeCompoundReadinessInline(runtime.root, {
|
|
940
|
+
prereadRaw: knowledgeRaw,
|
|
941
|
+
...(typeof archivedRunsCount === "number" ? { archivedRunsCount } : {})
|
|
942
|
+
});
|
|
916
943
|
await writeJsonFile(path.join(stateDir, "compound-readiness.json"), readiness);
|
|
917
944
|
if (state.currentStage === "review" || state.currentStage === "ship") {
|
|
918
945
|
if (readiness.readyCount === 0) {
|
|
@@ -1297,16 +1324,53 @@ async function handlePromptGuard(runtime) {
|
|
|
1297
1324
|
return 0;
|
|
1298
1325
|
}
|
|
1299
1326
|
|
|
1327
|
+
function normalizeCompoundLastUpdatedAt(date) {
|
|
1328
|
+
return date.toISOString().replace(/\\.\\d{3}Z$/u, "Z");
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
// Count archived runs as sub-directories under \`.cclaw/runs/\`. Missing
|
|
1332
|
+
// dir returns 0; unexpected errors return undefined so the caller can
|
|
1333
|
+
// skip the small-project relaxation rather than guess.
|
|
1334
|
+
async function countArchivedRunsInline(root) {
|
|
1335
|
+
const dir = path.join(root, RUNTIME_ROOT, "runs");
|
|
1336
|
+
try {
|
|
1337
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1338
|
+
return entries.filter((entry) => entry.isDirectory()).length;
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
1341
|
+
if (code === "ENOENT") return 0;
|
|
1342
|
+
return undefined;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1300
1346
|
// Mirrors src/knowledge-store.ts::computeCompoundReadiness — kept inline so
|
|
1301
1347
|
// SessionStart can refresh compound-readiness.json without the CLI binary.
|
|
1302
1348
|
// Any schema change must update src/knowledge-store.ts::computeCompoundReadiness
|
|
1303
|
-
// and src/internal/compound-readiness.ts in lockstep.
|
|
1349
|
+
// and src/internal/compound-readiness.ts in lockstep. Parity is enforced by
|
|
1350
|
+
// tests/unit/ralph-loop-parity.test.ts.
|
|
1304
1351
|
async function computeCompoundReadinessInline(root, options) {
|
|
1305
1352
|
const filePath = path.join(root, RUNTIME_ROOT, "knowledge.jsonl");
|
|
1306
|
-
|
|
1307
|
-
const
|
|
1308
|
-
? options.
|
|
1309
|
-
:
|
|
1353
|
+
// Caller may supply pre-read raw to avoid double-reading knowledge.jsonl.
|
|
1354
|
+
const raw = typeof (options && options.prereadRaw) === "string"
|
|
1355
|
+
? options.prereadRaw
|
|
1356
|
+
: await readTextFile(filePath, "");
|
|
1357
|
+
const baseThresholdRaw = options && options.threshold;
|
|
1358
|
+
const baseThreshold = Number.isInteger(baseThresholdRaw) && baseThresholdRaw >= 1
|
|
1359
|
+
? baseThresholdRaw
|
|
1360
|
+
: COMPOUND_RECURRENCE_THRESHOLD;
|
|
1361
|
+
const archivedRunsCount =
|
|
1362
|
+
typeof (options && options.archivedRunsCount) === "number" &&
|
|
1363
|
+
Number.isFinite(options.archivedRunsCount) &&
|
|
1364
|
+
options.archivedRunsCount >= 0
|
|
1365
|
+
? Math.floor(options.archivedRunsCount)
|
|
1366
|
+
: undefined;
|
|
1367
|
+
const smallProjectRelaxationApplied =
|
|
1368
|
+
archivedRunsCount !== undefined &&
|
|
1369
|
+
archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&
|
|
1370
|
+
baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD;
|
|
1371
|
+
const threshold = smallProjectRelaxationApplied
|
|
1372
|
+
? SMALL_PROJECT_RECURRENCE_THRESHOLD
|
|
1373
|
+
: baseThreshold;
|
|
1310
1374
|
const maxReady = Number.isInteger(options && options.maxReady) && options.maxReady >= 1
|
|
1311
1375
|
? options.maxReady
|
|
1312
1376
|
: 10;
|
|
@@ -1386,12 +1450,15 @@ async function computeCompoundReadinessInline(root, options) {
|
|
|
1386
1450
|
return String(a.trigger).localeCompare(String(b.trigger));
|
|
1387
1451
|
});
|
|
1388
1452
|
return {
|
|
1389
|
-
schemaVersion:
|
|
1453
|
+
schemaVersion: 2,
|
|
1390
1454
|
threshold,
|
|
1455
|
+
baseThreshold,
|
|
1456
|
+
...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),
|
|
1457
|
+
smallProjectRelaxationApplied,
|
|
1391
1458
|
clusterCount: buckets.size,
|
|
1392
1459
|
readyCount: ready.length,
|
|
1393
1460
|
ready: ready.slice(0, maxReady),
|
|
1394
|
-
lastUpdatedAt: new Date()
|
|
1461
|
+
lastUpdatedAt: normalizeCompoundLastUpdatedAt(new Date())
|
|
1395
1462
|
};
|
|
1396
1463
|
}
|
|
1397
1464
|
|
package/dist/install.js
CHANGED
|
@@ -731,7 +731,8 @@ async function writeHooks(projectRoot, config) {
|
|
|
731
731
|
await writeFileSafe(path.join(hooksDir, "run-hook.mjs"), nodeHookRuntimeScript({
|
|
732
732
|
strictness: effectiveStrictness,
|
|
733
733
|
tddTestPathPatterns: config.tdd?.testPathPatterns ?? config.tddTestGlobs,
|
|
734
|
-
tddProductionPathPatterns: config.tdd?.productionPathPatterns
|
|
734
|
+
tddProductionPathPatterns: config.tdd?.productionPathPatterns,
|
|
735
|
+
compoundRecurrenceThreshold: config.compound?.recurrenceThreshold
|
|
735
736
|
}));
|
|
736
737
|
const opencodePluginSource = opencodePluginJs();
|
|
737
738
|
await writeFileSafe(path.join(hooksDir, "opencode-plugin.mjs"), opencodePluginSource);
|
|
@@ -4,6 +4,14 @@ interface InternalIo {
|
|
|
4
4
|
stdout: Writable;
|
|
5
5
|
stderr: Writable;
|
|
6
6
|
}
|
|
7
|
+
/**
|
|
8
|
+
* Count archived runs as sub-directories under `.cclaw/runs/`. Missing
|
|
9
|
+
* dir / ENOENT is interpreted as zero — callers should NOT conflate
|
|
10
|
+
* that with "unknown" (undefined); we only return undefined on
|
|
11
|
+
* unexpected errors so the caller can choose to skip the relaxation
|
|
12
|
+
* rather than guess a number.
|
|
13
|
+
*/
|
|
14
|
+
export declare function countArchivedRunsSafely(projectRoot: string): Promise<number | undefined>;
|
|
7
15
|
/**
|
|
8
16
|
* Compact one-liner for session-digest / bootstrap surfaces.
|
|
9
17
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
1
2
|
import path from "node:path";
|
|
2
3
|
import { RUNTIME_ROOT } from "../constants.js";
|
|
3
4
|
import { readConfig } from "../config.js";
|
|
@@ -19,6 +20,11 @@ function parseArgs(tokens) {
|
|
|
19
20
|
const value = tokens[i + 1];
|
|
20
21
|
if (!value)
|
|
21
22
|
throw new Error("--threshold requires a numeric value");
|
|
23
|
+
// Strict: reject "2abc", "2.9", "-1", "" — parseInt would silently
|
|
24
|
+
// accept the first two and produce surprising behavior.
|
|
25
|
+
if (!/^\d+$/u.test(value)) {
|
|
26
|
+
throw new Error(`--threshold must be a positive integer, got ${value}`);
|
|
27
|
+
}
|
|
22
28
|
const parsed = Number.parseInt(value, 10);
|
|
23
29
|
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
24
30
|
throw new Error(`--threshold must be a positive integer, got ${value}`);
|
|
@@ -35,6 +41,29 @@ function parseArgs(tokens) {
|
|
|
35
41
|
function stateDir(projectRoot) {
|
|
36
42
|
return path.join(projectRoot, RUNTIME_ROOT, "state");
|
|
37
43
|
}
|
|
44
|
+
function archiveRunsDir(projectRoot) {
|
|
45
|
+
return path.join(projectRoot, RUNTIME_ROOT, "runs");
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Count archived runs as sub-directories under `.cclaw/runs/`. Missing
|
|
49
|
+
* dir / ENOENT is interpreted as zero — callers should NOT conflate
|
|
50
|
+
* that with "unknown" (undefined); we only return undefined on
|
|
51
|
+
* unexpected errors so the caller can choose to skip the relaxation
|
|
52
|
+
* rather than guess a number.
|
|
53
|
+
*/
|
|
54
|
+
export async function countArchivedRunsSafely(projectRoot) {
|
|
55
|
+
const dir = archiveRunsDir(projectRoot);
|
|
56
|
+
try {
|
|
57
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
58
|
+
return entries.filter((entry) => entry.isDirectory()).length;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
const code = error?.code;
|
|
62
|
+
if (code === "ENOENT")
|
|
63
|
+
return 0;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
38
67
|
/**
|
|
39
68
|
* Compact one-liner for session-digest / bootstrap surfaces.
|
|
40
69
|
*
|
|
@@ -51,14 +80,26 @@ export function formatCompoundReadinessLine(status) {
|
|
|
51
80
|
}
|
|
52
81
|
export async function runCompoundReadinessCommand(projectRoot, argv, io) {
|
|
53
82
|
const args = parseArgs(argv);
|
|
54
|
-
|
|
83
|
+
// Reading config is best-effort — but DO surface a stderr warning so
|
|
84
|
+
// mis-wired / malformed config shows up in hook-errors / CI logs
|
|
85
|
+
// instead of silently degrading to default threshold.
|
|
86
|
+
let config = null;
|
|
87
|
+
try {
|
|
88
|
+
config = await readConfig(projectRoot);
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
92
|
+
io.stderr.write(`[cclaw] compound-readiness: failed to read config (${detail}); falling back to default threshold\n`);
|
|
93
|
+
}
|
|
55
94
|
const threshold = args.threshold ??
|
|
56
95
|
(typeof config?.compound?.recurrenceThreshold === "number"
|
|
57
96
|
? config.compound.recurrenceThreshold
|
|
58
97
|
: undefined);
|
|
59
|
-
const
|
|
98
|
+
const archivedRunsCount = await countArchivedRunsSafely(projectRoot);
|
|
99
|
+
const { entries } = await readKnowledgeSafely(projectRoot, { lockAware: true });
|
|
60
100
|
const status = computeCompoundReadiness(entries, {
|
|
61
|
-
...(typeof threshold === "number" ? { threshold } : {})
|
|
101
|
+
...(typeof threshold === "number" ? { threshold } : {}),
|
|
102
|
+
...(typeof archivedRunsCount === "number" ? { archivedRunsCount } : {})
|
|
62
103
|
});
|
|
63
104
|
if (args.write) {
|
|
64
105
|
const target = path.join(stateDir(projectRoot), "compound-readiness.json");
|
|
@@ -98,9 +98,28 @@ export interface CompoundReadinessCluster {
|
|
|
98
98
|
maturity: KnowledgeEntryMaturity[];
|
|
99
99
|
}
|
|
100
100
|
export interface CompoundReadiness {
|
|
101
|
-
schemaVersion:
|
|
102
|
-
/**
|
|
101
|
+
schemaVersion: 2;
|
|
102
|
+
/**
|
|
103
|
+
* Effective recurrence threshold actually used. When
|
|
104
|
+
* `archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD`, this is
|
|
105
|
+
* `min(baseThreshold, SMALL_PROJECT_RECURRENCE_THRESHOLD)` — otherwise
|
|
106
|
+
* it equals `baseThreshold`.
|
|
107
|
+
*/
|
|
103
108
|
threshold: number;
|
|
109
|
+
/** Base threshold from config/CLI before small-project relaxation. */
|
|
110
|
+
baseThreshold: number;
|
|
111
|
+
/**
|
|
112
|
+
* Archived-run count observed at compute time (used to gate the
|
|
113
|
+
* small-project relaxation). Optional — the computation can run
|
|
114
|
+
* without knowing this and then no relaxation is applied.
|
|
115
|
+
*/
|
|
116
|
+
archivedRunsCount?: number;
|
|
117
|
+
/**
|
|
118
|
+
* True iff the effective threshold was lowered by the small-project
|
|
119
|
+
* relaxation rule. Always false when `archivedRunsCount` is not
|
|
120
|
+
* supplied.
|
|
121
|
+
*/
|
|
122
|
+
smallProjectRelaxationApplied: boolean;
|
|
104
123
|
/** Total number of (trigger, action) clusters seen, regardless of threshold. */
|
|
105
124
|
clusterCount: number;
|
|
106
125
|
/** Number of clusters that passed the threshold or critical override. */
|
|
@@ -117,7 +136,27 @@ export interface ComputeCompoundReadinessOptions {
|
|
|
117
136
|
/** Hard cap on `ready[]` to keep the surface digest concise. Default 10. */
|
|
118
137
|
maxReady?: number;
|
|
119
138
|
now?: Date;
|
|
139
|
+
/**
|
|
140
|
+
* Count of archived runs under `.cclaw/runs/`. When supplied and
|
|
141
|
+
* `< SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD`, the effective threshold
|
|
142
|
+
* is lowered to `min(threshold, SMALL_PROJECT_RECURRENCE_THRESHOLD)`.
|
|
143
|
+
* Matches the rule documented in `src/content/compound-command.ts`
|
|
144
|
+
* and `docs/config.md`.
|
|
145
|
+
*/
|
|
146
|
+
archivedRunsCount?: number;
|
|
120
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Single source of truth for the small-project relaxation rule.
|
|
150
|
+
*
|
|
151
|
+
* Kept exported so the inline hook mirror, the CLI command, and
|
|
152
|
+
* the `/cc-ops compound` skill all agree on the same numbers.
|
|
153
|
+
*/
|
|
154
|
+
export declare const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = 5;
|
|
155
|
+
export declare const SMALL_PROJECT_RECURRENCE_THRESHOLD = 2;
|
|
156
|
+
export declare function effectiveCompoundThreshold(baseThreshold: number, archivedRunsCount: number | undefined): {
|
|
157
|
+
threshold: number;
|
|
158
|
+
relaxationApplied: boolean;
|
|
159
|
+
};
|
|
121
160
|
/**
|
|
122
161
|
* Pure function — no filesystem side effects. Callers pass entries from
|
|
123
162
|
* `readKnowledgeSafely` and get a derived readiness snapshot suitable
|
package/dist/knowledge-store.js
CHANGED
|
@@ -5,6 +5,26 @@ import { RUNTIME_ROOT } from "./constants.js";
|
|
|
5
5
|
import { stripBom, withDirectoryLock } from "./fs-utils.js";
|
|
6
6
|
import { FLOW_STAGES } from "./types.js";
|
|
7
7
|
const DEFAULT_COMPOUND_READINESS_MAX_READY = 10;
|
|
8
|
+
/**
|
|
9
|
+
* Single source of truth for the small-project relaxation rule.
|
|
10
|
+
*
|
|
11
|
+
* Kept exported so the inline hook mirror, the CLI command, and
|
|
12
|
+
* the `/cc-ops compound` skill all agree on the same numbers.
|
|
13
|
+
*/
|
|
14
|
+
export const SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD = 5;
|
|
15
|
+
export const SMALL_PROJECT_RECURRENCE_THRESHOLD = 2;
|
|
16
|
+
export function effectiveCompoundThreshold(baseThreshold, archivedRunsCount) {
|
|
17
|
+
if (typeof archivedRunsCount === "number" &&
|
|
18
|
+
Number.isFinite(archivedRunsCount) &&
|
|
19
|
+
archivedRunsCount < SMALL_PROJECT_ARCHIVE_RUNS_THRESHOLD &&
|
|
20
|
+
baseThreshold > SMALL_PROJECT_RECURRENCE_THRESHOLD) {
|
|
21
|
+
return {
|
|
22
|
+
threshold: SMALL_PROJECT_RECURRENCE_THRESHOLD,
|
|
23
|
+
relaxationApplied: true
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
return { threshold: baseThreshold, relaxationApplied: false };
|
|
27
|
+
}
|
|
8
28
|
/**
|
|
9
29
|
* Pure function — no filesystem side effects. Callers pass entries from
|
|
10
30
|
* `readKnowledgeSafely` and get a derived readiness snapshot suitable
|
|
@@ -17,7 +37,7 @@ const DEFAULT_COMPOUND_READINESS_MAX_READY = 10;
|
|
|
17
37
|
*/
|
|
18
38
|
export function computeCompoundReadiness(entries, options = {}) {
|
|
19
39
|
const thresholdRaw = options.threshold ?? DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
20
|
-
const
|
|
40
|
+
const baseThreshold = Number.isInteger(thresholdRaw) && thresholdRaw >= 1
|
|
21
41
|
? thresholdRaw
|
|
22
42
|
: DEFAULT_COMPOUND_RECURRENCE_THRESHOLD;
|
|
23
43
|
const maxReadyRaw = options.maxReady ?? DEFAULT_COMPOUND_READINESS_MAX_READY;
|
|
@@ -25,6 +45,12 @@ export function computeCompoundReadiness(entries, options = {}) {
|
|
|
25
45
|
? maxReadyRaw
|
|
26
46
|
: DEFAULT_COMPOUND_READINESS_MAX_READY;
|
|
27
47
|
const now = options.now ?? new Date();
|
|
48
|
+
const archivedRunsCount = typeof options.archivedRunsCount === "number" &&
|
|
49
|
+
Number.isFinite(options.archivedRunsCount) &&
|
|
50
|
+
options.archivedRunsCount >= 0
|
|
51
|
+
? Math.floor(options.archivedRunsCount)
|
|
52
|
+
: undefined;
|
|
53
|
+
const { threshold, relaxationApplied } = effectiveCompoundThreshold(baseThreshold, archivedRunsCount);
|
|
28
54
|
const buckets = new Map();
|
|
29
55
|
for (const entry of entries) {
|
|
30
56
|
if (entry.maturity === "lifted-to-enforcement")
|
|
@@ -102,8 +128,11 @@ export function computeCompoundReadiness(entries, options = {}) {
|
|
|
102
128
|
return a.trigger.localeCompare(b.trigger);
|
|
103
129
|
});
|
|
104
130
|
return {
|
|
105
|
-
schemaVersion:
|
|
131
|
+
schemaVersion: 2,
|
|
106
132
|
threshold,
|
|
133
|
+
baseThreshold,
|
|
134
|
+
...(archivedRunsCount !== undefined ? { archivedRunsCount } : {}),
|
|
135
|
+
smallProjectRelaxationApplied: relaxationApplied,
|
|
107
136
|
clusterCount: buckets.size,
|
|
108
137
|
readyCount: ready.length,
|
|
109
138
|
ready: ready.slice(0, maxReady),
|