cclaw-cli 0.48.2 → 0.48.3

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/install.js CHANGED
@@ -178,15 +178,15 @@ async function ensureStructure(projectRoot) {
178
178
  await ensureDir(path.join(projectRoot, dir));
179
179
  }
180
180
  }
181
- async function writeCommandContracts(projectRoot) {
182
- for (const stage of FLOW_STAGES) {
183
- await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandContract(stage));
184
- }
181
+ async function writeCommandContracts(projectRoot, track = "standard") {
182
+ await Promise.all(FLOW_STAGES.map(async (stage) => {
183
+ await writeFileSafe(runtimePath(projectRoot, "commands", `${stage}.md`), stageCommandContract(stage, track));
184
+ }));
185
185
  }
186
186
  async function writeArtifactTemplates(projectRoot) {
187
- for (const [fileName, content] of Object.entries(ARTIFACT_TEMPLATES)) {
187
+ await Promise.all(Object.entries(ARTIFACT_TEMPLATES).map(async ([fileName, content]) => {
188
188
  await writeFileSafe(runtimePath(projectRoot, "templates", fileName), content);
189
- }
189
+ }));
190
190
  }
191
191
  /**
192
192
  * Seed the `.cclaw/evals/` scaffold. Only writes files that do not already
@@ -699,7 +699,7 @@ async function writeHooks(projectRoot, config) {
699
699
  async function ensureKnowledgeStore(projectRoot) {
700
700
  const storePath = runtimePath(projectRoot, "knowledge.jsonl");
701
701
  if (!(await exists(storePath))) {
702
- await writeFileSafe(storePath, "");
702
+ await writeFileSafe(storePath, "", { mode: 0o600 });
703
703
  }
704
704
  const legacyMdPath = runtimePath(projectRoot, "knowledge.md");
705
705
  if (await exists(legacyMdPath)) {
@@ -874,7 +874,7 @@ async function ensureSessionStateFiles(projectRoot) {
874
874
  }
875
875
  const activityPath = path.join(stateDir, "stage-activity.jsonl");
876
876
  if (!(await exists(activityPath))) {
877
- await writeFileSafe(activityPath, "");
877
+ await writeFileSafe(activityPath, "", { mode: 0o600 });
878
878
  }
879
879
  const checkpointPath = path.join(stateDir, "checkpoint.json");
880
880
  if (!(await exists(checkpointPath))) {
@@ -887,7 +887,7 @@ async function ensureSessionStateFiles(projectRoot) {
887
887
  blockers: [],
888
888
  timestamp: new Date().toISOString()
889
889
  };
890
- await writeFileSafe(checkpointPath, `${JSON.stringify(initialCheckpoint, null, 2)}\n`);
890
+ await writeFileSafe(checkpointPath, `${JSON.stringify(initialCheckpoint, null, 2)}\n`, { mode: 0o600 });
891
891
  }
892
892
  const suggestionMemoryPath = path.join(stateDir, "suggestion-memory.json");
893
893
  if (!(await exists(suggestionMemoryPath))) {
@@ -897,11 +897,11 @@ async function ensureSessionStateFiles(projectRoot) {
897
897
  lastSuggestedStage: "",
898
898
  lastSuggestedAt: ""
899
899
  };
900
- await writeFileSafe(suggestionMemoryPath, `${JSON.stringify(suggestionMemory, null, 2)}\n`);
900
+ await writeFileSafe(suggestionMemoryPath, `${JSON.stringify(suggestionMemory, null, 2)}\n`, { mode: 0o600 });
901
901
  }
902
902
  const contextModePath = path.join(stateDir, "context-mode.json");
903
903
  if (!(await exists(contextModePath))) {
904
- await writeFileSafe(contextModePath, `${JSON.stringify(createInitialContextModeState(), null, 2)}\n`);
904
+ await writeFileSafe(contextModePath, `${JSON.stringify(createInitialContextModeState(), null, 2)}\n`, { mode: 0o600 });
905
905
  }
906
906
  const knowledgeDigestPath = path.join(stateDir, "knowledge-digest.md");
907
907
  if (!(await exists(knowledgeDigestPath))) {
@@ -909,18 +909,18 @@ async function ensureSessionStateFiles(projectRoot) {
909
909
  }
910
910
  const tddCycleLogPath = path.join(stateDir, "tdd-cycle-log.jsonl");
911
911
  if (!(await exists(tddCycleLogPath))) {
912
- await writeFileSafe(tddCycleLogPath, "");
912
+ await writeFileSafe(tddCycleLogPath, "", { mode: 0o600 });
913
913
  }
914
914
  const reconciliationNoticesPath = path.join(stateDir, "reconciliation-notices.json");
915
915
  if (!(await exists(reconciliationNoticesPath))) {
916
- await writeFileSafe(reconciliationNoticesPath, `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`);
916
+ await writeFileSafe(reconciliationNoticesPath, `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`, { mode: 0o600 });
917
917
  }
918
918
  const flowSnapshotPath = path.join(stateDir, "flow-state.snapshot.json");
919
919
  if (!(await exists(flowSnapshotPath))) {
920
920
  await writeFileSafe(flowSnapshotPath, `${JSON.stringify({
921
921
  capturedAt: new Date().toISOString(),
922
922
  state: flow
923
- }, null, 2)}\n`);
923
+ }, null, 2)}\n`, { mode: 0o600 });
924
924
  }
925
925
  }
926
926
  async function writeRulebook(projectRoot) {
@@ -978,7 +978,7 @@ async function writeState(projectRoot, config, forceReset = false) {
978
978
  return;
979
979
  }
980
980
  const state = createInitialFlowState({ track: config.defaultTrack ?? "standard" });
981
- await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`);
981
+ await writeFileSafe(statePath, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
982
982
  }
983
983
  async function writeAdapterManifest(projectRoot, harnesses) {
984
984
  const manifest = {
@@ -1198,13 +1198,15 @@ async function materializeRuntime(projectRoot, config, forceStateReset) {
1198
1198
  await ensureStructure(projectRoot);
1199
1199
  await cleanLegacyArtifacts(projectRoot);
1200
1200
  await cleanStaleFiles(projectRoot);
1201
- await writeCommandContracts(projectRoot);
1202
- await writeUtilityCommands(projectRoot, config);
1203
- await writeSkills(projectRoot, config);
1204
- await writeContextModes(projectRoot);
1205
- await writeArtifactTemplates(projectRoot);
1206
- await writeEvalScaffold(projectRoot);
1207
- await writeRulebook(projectRoot);
1201
+ await Promise.all([
1202
+ writeCommandContracts(projectRoot, config.defaultTrack ?? "standard"),
1203
+ writeUtilityCommands(projectRoot, config),
1204
+ writeSkills(projectRoot, config),
1205
+ writeContextModes(projectRoot),
1206
+ writeArtifactTemplates(projectRoot),
1207
+ writeEvalScaffold(projectRoot),
1208
+ writeRulebook(projectRoot)
1209
+ ]);
1208
1210
  await writeState(projectRoot, config, forceStateReset);
1209
1211
  await ensureRunSystem(projectRoot, { createIfMissing: false });
1210
1212
  await ensureSessionStateFiles(projectRoot);
@@ -433,8 +433,9 @@ async function runAdvanceStage(projectRoot, args, io) {
433
433
  const selectedGateIds = args.passedGateIds.length > 0
434
434
  ? args.passedGateIds.filter((gateId) => selectableGateIds.has(gateId))
435
435
  : requiredGateIds;
436
+ const selectedGateIdSet = new Set(selectedGateIds);
436
437
  const selectedTransitionGuards = selectedGateIds.filter((gateId) => transitionGuardIds.has(gateId));
437
- const missingRequired = requiredGateIds.filter((gateId) => !selectedGateIds.includes(gateId));
438
+ const missingRequired = requiredGateIds.filter((gateId) => !selectedGateIdSet.has(gateId));
438
439
  if (missingRequired.length > 0) {
439
440
  io.stderr.write(`cclaw internal advance-stage: required gates not selected as passed: ${missingRequired.join(", ")}.\n`);
440
441
  return 1;
@@ -464,7 +465,8 @@ async function runAdvanceStage(projectRoot, args, io) {
464
465
  }
465
466
  const catalog = flowState.stageGateCatalog[args.stage];
466
467
  const nextPassed = unique([...catalog.passed, ...selectedGateIds]).filter((gateId) => allowedGateIds.has(gateId));
467
- const nextBlocked = unique(catalog.blocked.filter((gateId) => !nextPassed.includes(gateId))).filter((gateId) => allowedGateIds.has(gateId));
468
+ const nextPassedSet = new Set(nextPassed);
469
+ const nextBlocked = unique(catalog.blocked.filter((gateId) => !nextPassedSet.has(gateId))).filter((gateId) => allowedGateIds.has(gateId));
468
470
  const conditional = new Set(catalog.conditional);
469
471
  const nextTriggered = unique([
470
472
  ...catalog.triggered.filter((gateId) => conditional.has(gateId)),
@@ -58,9 +58,17 @@ export interface AppendKnowledgeResult {
58
58
  errors: string[];
59
59
  appendedEntries: KnowledgeEntry[];
60
60
  }
61
+ export interface ReadKnowledgeOptions {
62
+ lockAware?: boolean;
63
+ }
64
+ export interface ReadKnowledgeResult {
65
+ entries: KnowledgeEntry[];
66
+ malformedLines: number;
67
+ }
61
68
  export declare function validateKnowledgeEntry(entry: unknown): {
62
69
  ok: boolean;
63
70
  errors: string[];
64
71
  };
65
72
  export declare function materializeKnowledgeEntry(seed: KnowledgeSeedEntry, defaults?: AppendKnowledgeDefaults): KnowledgeEntry;
73
+ export declare function readKnowledgeSafely(projectRoot: string, options?: ReadKnowledgeOptions): Promise<ReadKnowledgeResult>;
66
74
  export declare function appendKnowledge(projectRoot: string, seeds: KnowledgeSeedEntry[], defaults?: AppendKnowledgeDefaults): Promise<AppendKnowledgeResult>;
@@ -66,8 +66,78 @@ function dedupeKey(entry) {
66
66
  entry.severity === undefined ? "none" : entry.severity
67
67
  ].join("|");
68
68
  }
69
+ function emptyKnowledgeSnapshot() {
70
+ return {
71
+ lines: [],
72
+ entries: [],
73
+ malformedLines: 0,
74
+ keyToIndex: new Map(),
75
+ entryByIndex: new Map()
76
+ };
77
+ }
78
+ function parseKnowledgeSnapshot(raw) {
79
+ const lines = stripBom(raw).split(/\r?\n/u);
80
+ const entries = [];
81
+ const keyToIndex = new Map();
82
+ const entryByIndex = new Map();
83
+ let malformedLines = 0;
84
+ for (let i = 0; i < lines.length; i += 1) {
85
+ const trimmed = lines[i].trim();
86
+ if (trimmed.length === 0)
87
+ continue;
88
+ try {
89
+ const parsed = JSON.parse(trimmed);
90
+ const validated = validateKnowledgeEntry(parsed);
91
+ if (!validated.ok) {
92
+ malformedLines += 1;
93
+ continue;
94
+ }
95
+ const entry = parsed;
96
+ entries.push(entry);
97
+ const key = dedupeKey(entry);
98
+ if (!keyToIndex.has(key)) {
99
+ keyToIndex.set(key, i);
100
+ }
101
+ entryByIndex.set(i, entry);
102
+ }
103
+ catch {
104
+ malformedLines += 1;
105
+ }
106
+ }
107
+ return {
108
+ lines,
109
+ entries,
110
+ malformedLines,
111
+ keyToIndex,
112
+ entryByIndex
113
+ };
114
+ }
115
+ async function readKnowledgeSnapshot(filePath) {
116
+ try {
117
+ const raw = await fs.readFile(filePath, "utf8");
118
+ return parseKnowledgeSnapshot(raw);
119
+ }
120
+ catch (error) {
121
+ const code = error?.code;
122
+ if (code === "ENOENT") {
123
+ return emptyKnowledgeSnapshot();
124
+ }
125
+ throw error;
126
+ }
127
+ }
128
+ function mergeKnowledgeOccurrence(target, incoming) {
129
+ const mergedFrequency = target.frequency + Math.max(1, incoming.frequency);
130
+ const mergedLastSeen = target.last_seen_ts >= incoming.last_seen_ts
131
+ ? target.last_seen_ts
132
+ : incoming.last_seen_ts;
133
+ return {
134
+ ...target,
135
+ frequency: mergedFrequency,
136
+ last_seen_ts: mergedLastSeen
137
+ };
138
+ }
69
139
  function isIsoUtcTimestamp(value) {
70
- return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/u.test(value);
140
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?Z$/u.test(value);
71
141
  }
72
142
  function isNullableString(value) {
73
143
  return value === null || typeof value === "string";
@@ -176,29 +246,19 @@ export function materializeKnowledgeEntry(seed, defaults = {}) {
176
246
  }
177
247
  return entry;
178
248
  }
179
- async function readExistingKnowledgeKeys(filePath) {
180
- const keys = new Set();
181
- try {
182
- const raw = stripBom(await fs.readFile(filePath, "utf8"));
183
- const lines = raw.split(/\r?\n/u).map((line) => line.trim()).filter((line) => line.length > 0);
184
- for (const line of lines) {
185
- try {
186
- const parsed = JSON.parse(line);
187
- const validated = validateKnowledgeEntry(parsed);
188
- if (!validated.ok)
189
- continue;
190
- const entry = parsed;
191
- keys.add(dedupeKey(entry));
192
- }
193
- catch {
194
- // Ignore malformed historical lines for dedupe indexing.
195
- }
196
- }
197
- }
198
- catch {
199
- // Missing file is fine — treat as empty store.
249
+ export async function readKnowledgeSafely(projectRoot, options = {}) {
250
+ const filePath = knowledgePath(projectRoot);
251
+ const read = async () => {
252
+ const snapshot = await readKnowledgeSnapshot(filePath);
253
+ return {
254
+ entries: snapshot.entries,
255
+ malformedLines: snapshot.malformedLines
256
+ };
257
+ };
258
+ if (options.lockAware === false) {
259
+ return read();
200
260
  }
201
- return keys;
261
+ return withDirectoryLock(knowledgeLockPath(projectRoot), read);
202
262
  }
203
263
  export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
204
264
  if (seeds.length === 0) {
@@ -221,22 +281,42 @@ export async function appendKnowledge(projectRoot, seeds, defaults = {}) {
221
281
  const appendedEntries = [];
222
282
  await withDirectoryLock(knowledgeLockPath(projectRoot), async () => {
223
283
  await fs.mkdir(path.dirname(filePath), { recursive: true });
224
- const existingKeys = await readExistingKnowledgeKeys(filePath);
225
- const batchKeys = new Set();
226
- const linesToAppend = [];
284
+ const snapshot = await readKnowledgeSnapshot(filePath);
285
+ const updatedByIndex = new Map();
286
+ const batchEntries = new Map();
227
287
  for (const entry of materialized) {
228
288
  const key = dedupeKey(entry);
229
- if (existingKeys.has(key) || batchKeys.has(key)) {
289
+ const existingIndex = snapshot.keyToIndex.get(key);
290
+ if (existingIndex !== undefined) {
230
291
  skippedDuplicates += 1;
292
+ const base = updatedByIndex.get(existingIndex) ?? snapshot.entryByIndex.get(existingIndex);
293
+ if (base) {
294
+ updatedByIndex.set(existingIndex, mergeKnowledgeOccurrence(base, entry));
295
+ }
231
296
  continue;
232
297
  }
233
- batchKeys.add(key);
234
- existingKeys.add(key);
235
- appendedEntries.push(entry);
236
- linesToAppend.push(JSON.stringify(entry));
298
+ const existingBatchEntry = batchEntries.get(key);
299
+ if (existingBatchEntry) {
300
+ skippedDuplicates += 1;
301
+ batchEntries.set(key, mergeKnowledgeOccurrence(existingBatchEntry, entry));
302
+ continue;
303
+ }
304
+ batchEntries.set(key, { ...entry });
305
+ }
306
+ appendedEntries.push(...batchEntries.values());
307
+ if (updatedByIndex.size === 0 && batchEntries.size === 0) {
308
+ return;
237
309
  }
238
- if (linesToAppend.length > 0) {
239
- await fs.appendFile(filePath, `${linesToAppend.join("\n")}\n`, "utf8");
310
+ const rewrittenLines = snapshot.lines.map((line, index) => {
311
+ const updated = updatedByIndex.get(index);
312
+ return updated ? JSON.stringify(updated) : line;
313
+ }).filter((line) => line.trim().length > 0);
314
+ const linesToWrite = [
315
+ ...rewrittenLines,
316
+ ...Array.from(batchEntries.values(), (entry) => JSON.stringify(entry))
317
+ ];
318
+ if (linesToWrite.length > 0) {
319
+ await fs.writeFile(filePath, `${linesToWrite.join("\n")}\n`, "utf8");
240
320
  }
241
321
  });
242
322
  return {
@@ -2,6 +2,7 @@ import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { RUNTIME_ROOT } from "./constants.js";
4
4
  import { exists, stripBom } from "./fs-utils.js";
5
+ import { readKnowledgeSafely } from "./knowledge-store.js";
5
6
  function activeArtifactsPath(projectRoot) {
6
7
  return path.join(projectRoot, RUNTIME_ROOT, "artifacts");
7
8
  }
@@ -61,35 +62,44 @@ export async function evaluateRetroGate(projectRoot, state) {
61
62
  }
62
63
  }
63
64
  const shouldFallbackScan = compoundEntries <= 0 && (windowStartMs !== null || windowEndMs !== null);
64
- const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
65
- if (shouldFallbackScan && (await exists(knowledgeFile))) {
65
+ if (shouldFallbackScan) {
66
+ const countIfEligible = (parsed) => {
67
+ if (parsed.type !== "compound") {
68
+ return 0;
69
+ }
70
+ const created = typeof parsed.created === "string" ? parseIsoTimestamp(parsed.created) : null;
71
+ if (created === null || !inInclusiveWindow(created, windowStartMs, windowEndMs)) {
72
+ return 0;
73
+ }
74
+ const source = typeof parsed.source === "string"
75
+ ? parsed.source.trim().toLowerCase()
76
+ : null;
77
+ const legacyRetroStage = parsed.stage === "retro";
78
+ return source === "retro" || legacyRetroStage ? 1 : 0;
79
+ };
66
80
  try {
67
- const raw = stripBom(await fs.readFile(knowledgeFile, "utf8"));
81
+ const knowledgeFile = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
82
+ const { entries } = await readKnowledgeSafely(projectRoot);
68
83
  compoundEntries = 0;
69
- for (const line of raw.split(/\r?\n/)) {
70
- const trimmed = line.trim();
71
- if (!trimmed)
72
- continue;
73
- try {
74
- const parsed = JSON.parse(trimmed);
75
- if (parsed.type !== "compound") {
76
- continue;
77
- }
78
- const created = typeof parsed.created === "string" ? parseIsoTimestamp(parsed.created) : null;
79
- if (created === null || !inInclusiveWindow(created, windowStartMs, windowEndMs)) {
84
+ for (const parsed of entries) {
85
+ compoundEntries += countIfEligible(parsed);
86
+ }
87
+ // Backward compatibility for historical/hand-edited rows that don't pass
88
+ // strict knowledge schema validation but still carry retro evidence.
89
+ if (compoundEntries === 0 && (await exists(knowledgeFile))) {
90
+ const raw = stripBom(await fs.readFile(knowledgeFile, "utf8"));
91
+ for (const line of raw.split(/\r?\n/)) {
92
+ const trimmed = line.trim();
93
+ if (!trimmed)
80
94
  continue;
95
+ try {
96
+ const parsed = JSON.parse(trimmed);
97
+ compoundEntries += countIfEligible(parsed);
81
98
  }
82
- const source = typeof parsed.source === "string"
83
- ? parsed.source.trim().toLowerCase()
84
- : null;
85
- const legacyRetroStage = parsed.stage === "retro";
86
- if (source === "retro" || legacyRetroStage) {
87
- compoundEntries += 1;
99
+ catch {
100
+ // ignore malformed lines for retro gate calculation
88
101
  }
89
102
  }
90
- catch {
91
- // ignore malformed lines for retro gate calculation
92
- }
93
103
  }
94
104
  }
95
105
  catch {
@@ -4,6 +4,7 @@ import { RUNTIME_ROOT } from "./constants.js";
4
4
  import { createInitialFlowState } from "./flow-state.js";
5
5
  import { readActiveFeature, syncActiveFeatureSnapshot } from "./feature-system.js";
6
6
  import { ensureDir, exists, withDirectoryLock, writeFileSafe } from "./fs-utils.js";
7
+ import { readKnowledgeSafely } from "./knowledge-store.js";
7
8
  import { evaluateRetroGate } from "./retro-gate.js";
8
9
  import { ensureRunSystem, readFlowState, writeFlowState } from "./run-persistence.js";
9
10
  const RUNS_DIR_REL_PATH = `${RUNTIME_ROOT}/runs`;
@@ -79,9 +80,9 @@ async function snapshotStateDirectory(projectRoot, destinationRoot) {
79
80
  async function resetCarryoverStateFiles(projectRoot, activeRunId) {
80
81
  const stateDir = stateDirPath(projectRoot);
81
82
  await ensureDir(stateDir);
82
- await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`);
83
- await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "");
84
- await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`);
83
+ await writeFileSafe(path.join(stateDir, DELEGATION_LOG_FILE), `${JSON.stringify({ runId: activeRunId, entries: [] }, null, 2)}\n`, { mode: 0o600 });
84
+ await writeFileSafe(path.join(stateDir, TDD_CYCLE_LOG_FILE), "", { mode: 0o600 });
85
+ await writeFileSafe(path.join(stateDir, RECONCILIATION_NOTICES_FILE), `${JSON.stringify({ schemaVersion: 1, notices: [] }, null, 2)}\n`, { mode: 0o600 });
85
86
  }
86
87
  function toArchiveDate(date = new Date()) {
87
88
  const yyyy = date.getFullYear().toString();
@@ -312,12 +313,8 @@ export async function archiveRun(projectRoot, featureName, options = {}) {
312
313
  }
313
314
  const KNOWLEDGE_SOFT_THRESHOLD = 50;
314
315
  async function readKnowledgeStats(projectRoot) {
315
- const knowledgePath = path.join(projectRoot, RUNTIME_ROOT, "knowledge.jsonl");
316
- let activeEntryCount = 0;
317
- if (await exists(knowledgePath)) {
318
- const text = await fs.readFile(knowledgePath, "utf8");
319
- activeEntryCount = countActiveKnowledgeEntries(text);
320
- }
316
+ const { entries } = await readKnowledgeSafely(projectRoot);
317
+ const activeEntryCount = entries.length;
321
318
  return {
322
319
  activeEntryCount,
323
320
  softThreshold: KNOWLEDGE_SOFT_THRESHOLD,
@@ -381,7 +381,7 @@ export async function writeFlowState(projectRoot, state, options = {}) {
381
381
  }
382
382
  }
383
383
  const safe = coerceFlowState({ ...state });
384
- await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`);
384
+ await writeFileSafe(statePath, `${JSON.stringify(safe, null, 2)}\n`, { mode: 0o600 });
385
385
  });
386
386
  await syncActiveFeatureSnapshot(projectRoot);
387
387
  }
@@ -124,17 +124,17 @@ export async function buildTraceMatrix(projectRoot) {
124
124
  const acToTasks = new Map();
125
125
  for (const [task, acs] of taskToAcs) {
126
126
  for (const ac of acs) {
127
- const prev = acToTasks.get(ac) ?? [];
128
- if (!prev.includes(task)) {
129
- acToTasks.set(ac, [...prev, task]);
130
- }
127
+ const prev = acToTasks.get(ac) ?? new Set();
128
+ prev.add(task);
129
+ acToTasks.set(ac, prev);
131
130
  }
132
131
  }
133
132
  const entries = criterionIds.map((criterionId) => {
134
- const taskIds = acToTasks.get(criterionId) ?? [];
133
+ const taskIds = [...(acToTasks.get(criterionId) ?? new Set())];
134
+ const taskIdSet = new Set(taskIds);
135
135
  const testSlices = [];
136
136
  for (const [slice, tasks] of sliceToTasks) {
137
- if (tasks.some((t) => taskIds.includes(t))) {
137
+ if (tasks.some((t) => taskIdSet.has(t))) {
138
138
  testSlices.push(slice);
139
139
  }
140
140
  }
@@ -145,7 +145,7 @@ export async function buildTraceMatrix(projectRoot) {
145
145
  reviewFindings: layer1LinesForCriterion(layer1, criterionId)
146
146
  };
147
147
  });
148
- const orphanedCriteria = criterionIds.filter((ac) => (acToTasks.get(ac) ?? []).length === 0);
148
+ const orphanedCriteria = criterionIds.filter((ac) => (acToTasks.get(ac)?.size ?? 0) === 0);
149
149
  const tasksWithSlice = new Set();
150
150
  for (const tasks of sliceToTasks.values()) {
151
151
  for (const t of tasks) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cclaw-cli",
3
- "version": "0.48.2",
3
+ "version": "0.48.3",
4
4
  "description": "Installer-first flow toolkit for coding agents",
5
5
  "type": "module",
6
6
  "bin": {