* refactor: tighten sdk-first architecture seams Refs #3312 * refactor: finish state document seam cleanup Refs #3312 * test: harden minimal install cleanup assertion * ci: support sdk-scoped package lock * fix(3316): restore root package-lock.json and align changeset pr ref Reverts dec57a83 ("ci: support sdk-scoped package lock") and restores the root package-lock.json that c249d34d deleted. The deletion was the wrong direction: - The root package.json declares its own runtime and dev deps (@anthropic-ai/claude-agent-sdk, ws, c8). Without a root lockfile, `npm install --no-package-lock` resolves whatever satisfies semver at install time — CI today and CI in six months can install different transitive trees, defeating reproducibility. - The lockfile has been part of every release on this repo (long history on main); removing it loses the npm audit / Dependabot target without compensating benefit. - The CI workaround pattern (cache-dependency-path: sdk/package-lock.json + `npm install --no-package-lock`) papered over the symptom rather than fix the cause. Also fix the changeset pr: from 3312 (issue) to 3316 (PR). CONTEXT.md flags this exact failure mode as a recurring CodeRabbit finding. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: address coderabbit review findings * fix: close remaining coderabbit threads --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.7 KiB
JavaScript
160 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* Workstream Inventory Module
|
|
*
|
|
* Owns discovery and read-only projection of .planning/workstreams/* state.
|
|
* Command handlers should render outputs from this inventory instead of
|
|
* rescanning workstream directories directly.
|
|
*/
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
const { toPosixPath, readSubdirectories } = require('./core.cjs');
|
|
const scanPhasePlans = require('./plan-scan.cjs');
|
|
const { planningPaths, planningRoot, getActiveWorkstream } = require('./planning-workspace.cjs');
|
|
const { stateExtractField } = require('./state-document.cjs');
|
|
|
|
function workstreamsRoot(cwd) {
|
|
return path.join(planningRoot(cwd), 'workstreams');
|
|
}
|
|
|
|
function countRoadmapPhases(roadmapPath, fallbackCount) {
|
|
try {
|
|
const roadmapContent = fs.readFileSync(roadmapPath, 'utf-8');
|
|
const matches = roadmapContent.match(/^#{2,4}\s+Phase\s+[\w][\w.-]*/gm);
|
|
return matches ? matches.length : fallbackCount;
|
|
} catch {
|
|
return fallbackCount;
|
|
}
|
|
}
|
|
|
|
function countPhaseFiles(phaseDir) {
|
|
const scan = scanPhasePlans(phaseDir);
|
|
return { planCount: scan.planCount, summaryCount: scan.summaryCount };
|
|
}
|
|
|
|
function readStateProjection(statePath) {
|
|
try {
|
|
const stateContent = fs.readFileSync(statePath, 'utf-8');
|
|
return {
|
|
status: stateExtractField(stateContent, 'Status') || 'unknown',
|
|
current_phase: stateExtractField(stateContent, 'Current Phase'),
|
|
last_activity: stateExtractField(stateContent, 'Last Activity'),
|
|
};
|
|
} catch {
|
|
return {
|
|
status: 'unknown',
|
|
current_phase: null,
|
|
last_activity: null,
|
|
};
|
|
}
|
|
}
|
|
|
|
function inspectWorkstream(cwd, name, options = {}) {
|
|
const wsDir = path.join(workstreamsRoot(cwd), name);
|
|
if (!fs.existsSync(wsDir)) return null;
|
|
|
|
const active = options.active === undefined ? getActiveWorkstream(cwd) : options.active;
|
|
const p = planningPaths(cwd, name);
|
|
const phaseDirs = readSubdirectories(p.phases);
|
|
const phases = [];
|
|
let completedPhases = 0;
|
|
let totalPlans = 0;
|
|
let completedPlans = 0;
|
|
|
|
for (const dir of phaseDirs.sort()) {
|
|
const counts = countPhaseFiles(path.join(p.phases, dir));
|
|
const status = counts.summaryCount >= counts.planCount && counts.planCount > 0
|
|
? 'complete'
|
|
: counts.planCount > 0
|
|
? 'in_progress'
|
|
: 'pending';
|
|
|
|
totalPlans += counts.planCount;
|
|
completedPlans += Math.min(counts.summaryCount, counts.planCount);
|
|
if (status === 'complete') completedPhases++;
|
|
|
|
phases.push({
|
|
directory: dir,
|
|
status,
|
|
plan_count: counts.planCount,
|
|
summary_count: counts.summaryCount,
|
|
});
|
|
}
|
|
|
|
const roadmapPhaseCount = countRoadmapPhases(p.roadmap, phaseDirs.length);
|
|
const state = readStateProjection(p.state);
|
|
|
|
return {
|
|
name,
|
|
path: toPosixPath(path.relative(cwd, wsDir)),
|
|
active: name === active,
|
|
files: {
|
|
roadmap: fs.existsSync(p.roadmap),
|
|
state: fs.existsSync(p.state),
|
|
requirements: fs.existsSync(p.requirements),
|
|
},
|
|
status: state.status,
|
|
current_phase: state.current_phase,
|
|
last_activity: state.last_activity,
|
|
phases,
|
|
phase_count: phases.length,
|
|
completed_phases: completedPhases,
|
|
roadmap_phase_count: roadmapPhaseCount,
|
|
total_plans: totalPlans,
|
|
completed_plans: completedPlans,
|
|
progress_percent: roadmapPhaseCount > 0 ? Math.min(100, Math.round((completedPhases / roadmapPhaseCount) * 100)) : 0,
|
|
};
|
|
}
|
|
|
|
function listWorkstreamInventories(cwd) {
|
|
const wsRoot = workstreamsRoot(cwd);
|
|
if (!fs.existsSync(wsRoot)) {
|
|
return {
|
|
mode: 'flat',
|
|
active: null,
|
|
workstreams: [],
|
|
count: 0,
|
|
message: 'No workstreams — operating in flat mode',
|
|
};
|
|
}
|
|
|
|
const active = getActiveWorkstream(cwd);
|
|
const entries = fs.readdirSync(wsRoot, { withFileTypes: true });
|
|
const workstreams = [];
|
|
for (const entry of entries) {
|
|
if (!entry.isDirectory()) continue;
|
|
const inventory = inspectWorkstream(cwd, entry.name, { active });
|
|
if (inventory) workstreams.push(inventory);
|
|
}
|
|
|
|
return {
|
|
mode: 'workstream',
|
|
active,
|
|
workstreams,
|
|
count: workstreams.length,
|
|
};
|
|
}
|
|
|
|
function isCompletedInventory(inventory) {
|
|
const status = String(inventory && inventory.status ? inventory.status : '').toLowerCase();
|
|
return status.includes('milestone complete') || status.includes('archived');
|
|
}
|
|
|
|
function getOtherActiveWorkstreamInventories(cwd, excludeWs) {
|
|
return listWorkstreamInventories(cwd).workstreams
|
|
.filter(inventory => inventory.name !== excludeWs)
|
|
.filter(inventory => !isCompletedInventory(inventory));
|
|
}
|
|
|
|
module.exports = {
|
|
countPhaseFiles,
|
|
countRoadmapPhases,
|
|
getOtherActiveWorkstreamInventories,
|
|
inspectWorkstream,
|
|
isCompletedInventory,
|
|
listWorkstreamInventories,
|
|
workstreamsRoot,
|
|
};
|