Refactor SDK-first architecture seams (#3316)
* 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>
This commit is contained in:
@@ -244,6 +244,131 @@ describe('stateUpdate', () => {
|
||||
expect(data.updated).toBe(false);
|
||||
});
|
||||
|
||||
it('preserves curated progress frontmatter during body-only updates', async () => {
|
||||
const stateContent = `---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v3.0
|
||||
milestone_name: SDK-First Migration
|
||||
status: executing
|
||||
progress:
|
||||
total_phases: 12
|
||||
completed_phases: 6
|
||||
total_plans: 22
|
||||
completed_plans: 22
|
||||
percent: 50
|
||||
---
|
||||
|
||||
# Project State
|
||||
|
||||
## Current Position
|
||||
|
||||
Status: Executing
|
||||
Last Activity: 2026-01-01
|
||||
Progress: [█████░░░░░] 50%
|
||||
`;
|
||||
await setupTestProject(tmpDir, stateContent);
|
||||
|
||||
const { stateUpdate } = await import('./state-mutation.js');
|
||||
const { stateJson } = await import('./state.js');
|
||||
|
||||
const result = await stateUpdate(['Last Activity', '2026-05-07'], tmpDir);
|
||||
expect((result.data as Record<string, unknown>).updated).toBe(true);
|
||||
|
||||
const loaded = await stateJson([], tmpDir);
|
||||
const progress = (loaded.data as Record<string, unknown>).progress as Record<string, unknown>;
|
||||
expect(Number(progress.total_phases)).toBe(12);
|
||||
expect(Number(progress.completed_phases)).toBe(6);
|
||||
expect(Number(progress.total_plans)).toBe(22);
|
||||
expect(Number(progress.completed_plans)).toBe(22);
|
||||
expect(Number(progress.percent)).toBe(50);
|
||||
|
||||
const after = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
expect(after).toContain('Last Activity: 2026-05-07');
|
||||
});
|
||||
|
||||
it('resyncs progress frontmatter when updating the Progress body field', async () => {
|
||||
const stateContent = `---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v3.0
|
||||
milestone_name: SDK-First Migration
|
||||
status: executing
|
||||
progress:
|
||||
total_phases: 12
|
||||
completed_phases: 6
|
||||
total_plans: 22
|
||||
completed_plans: 22
|
||||
percent: 50
|
||||
---
|
||||
|
||||
# Project State
|
||||
|
||||
## Current Position
|
||||
|
||||
Status: Executing
|
||||
Progress: [█████░░░░░] 50%
|
||||
`;
|
||||
await setupTestProject(tmpDir, stateContent);
|
||||
await rm(join(tmpDir, '.planning', 'ROADMAP.md'), { force: true });
|
||||
|
||||
const { stateUpdate } = await import('./state-mutation.js');
|
||||
await stateUpdate(['Progress', '[████████░░] 80%'], tmpDir);
|
||||
|
||||
const after = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
const { extractFrontmatter } = await import('./frontmatter.js');
|
||||
const fm = extractFrontmatter(after);
|
||||
const progress = fm.progress as Record<string, unknown>;
|
||||
expect(Number(progress.percent)).toBe(80);
|
||||
});
|
||||
|
||||
it('syncs full-file workstream STATE.md frontmatter from the selected workstream', async () => {
|
||||
const planningDir = join(tmpDir, '.planning');
|
||||
const wsDir = join(planningDir, 'workstreams', 'feature');
|
||||
await mkdir(join(wsDir, 'phases'), { recursive: true });
|
||||
|
||||
await writeFile(
|
||||
join(planningDir, 'STATE.md'),
|
||||
[
|
||||
'---',
|
||||
'milestone: v0.1',
|
||||
'milestone_name: Root Milestone',
|
||||
'---',
|
||||
'',
|
||||
'# Root State',
|
||||
'',
|
||||
'Status: Root',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeFile(join(planningDir, 'ROADMAP.md'), '# Roadmap\n\n## v0.1 Root Milestone\n', 'utf-8');
|
||||
await writeFile(
|
||||
join(wsDir, 'STATE.md'),
|
||||
[
|
||||
'---',
|
||||
'milestone: v2.0',
|
||||
'milestone_name: Feature Milestone',
|
||||
'---',
|
||||
'',
|
||||
'# Workstream State',
|
||||
'',
|
||||
'Status: Executing',
|
||||
'',
|
||||
].join('\n'),
|
||||
'utf-8',
|
||||
);
|
||||
await writeFile(join(wsDir, 'ROADMAP.md'), '# Roadmap\n\n## v2.0 Feature Milestone\n', 'utf-8');
|
||||
|
||||
const { readModifyWriteStateMdFull } = await import('./state-mutation.js');
|
||||
await readModifyWriteStateMdFull(tmpDir, content => content.replace('Status: Executing', 'Status: Complete'), 'feature');
|
||||
|
||||
const after = await readFile(join(wsDir, 'STATE.md'), 'utf-8');
|
||||
const { extractFrontmatter } = await import('./frontmatter.js');
|
||||
const fm = extractFrontmatter(after);
|
||||
expect(fm.milestone).toBe('v2.0');
|
||||
expect(fm.milestone_name).toBe('Feature Milestone');
|
||||
expect(fm.status).toBe('completed');
|
||||
});
|
||||
|
||||
it('throws on missing args', async () => {
|
||||
const { stateUpdate } = await import('./state-mutation.js');
|
||||
|
||||
@@ -277,6 +402,45 @@ describe('statePatch', () => {
|
||||
const content = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
expect(content).toContain('done');
|
||||
});
|
||||
|
||||
it('preserves curated progress frontmatter when patching body-only fields', async () => {
|
||||
const stateContent = `---
|
||||
gsd_state_version: 1.0
|
||||
milestone: v3.0
|
||||
milestone_name: SDK-First Migration
|
||||
status: executing
|
||||
progress:
|
||||
total_phases: 12
|
||||
completed_phases: 6
|
||||
total_plans: 22
|
||||
completed_plans: 22
|
||||
percent: 50
|
||||
---
|
||||
|
||||
# Project State
|
||||
|
||||
## Current Position
|
||||
|
||||
Status: Executing
|
||||
Last Activity: 2026-01-01
|
||||
Progress: [█████░░░░░] 50%
|
||||
`;
|
||||
await setupTestProject(tmpDir, stateContent);
|
||||
|
||||
const { statePatch } = await import('./state-mutation.js');
|
||||
await statePatch([JSON.stringify({ 'Last Activity': '2026-05-07' })], tmpDir);
|
||||
|
||||
const after = await readFile(join(tmpDir, '.planning', 'STATE.md'), 'utf-8');
|
||||
const { extractFrontmatter } = await import('./frontmatter.js');
|
||||
const fm = extractFrontmatter(after);
|
||||
const progress = fm.progress as Record<string, unknown>;
|
||||
expect(Number(progress.total_phases)).toBe(12);
|
||||
expect(Number(progress.completed_phases)).toBe(6);
|
||||
expect(Number(progress.total_plans)).toBe(22);
|
||||
expect(Number(progress.completed_plans)).toBe(22);
|
||||
expect(Number(progress.percent)).toBe(50);
|
||||
expect(after).toContain('Last Activity: 2026-05-07');
|
||||
});
|
||||
});
|
||||
|
||||
// ─── stateBeginPhase ────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user