Merge pull request #767 from trek-e/fix/759-auto-mode-stale-state-loop
fix: prevent stale state loop on auto-mode restart with existing worktree (#759)
This commit is contained in:
commit
392b75a4db
3 changed files with 23 additions and 42 deletions
|
|
@ -158,7 +158,15 @@ export function createAutoWorktree(basePath: string, milestoneId: string): strin
|
|||
// Planning artifacts may be untracked if the project's .gitignore had a
|
||||
// blanket .gsd/ rule (pre-v2.14.0). Without this copy, auto-mode loops
|
||||
// on plan-slice because the plan file doesn't exist in the worktree.
|
||||
copyPlanningArtifacts(basePath, info.path);
|
||||
//
|
||||
// IMPORTANT: Skip when re-attaching to an existing branch (#759).
|
||||
// The branch checkout already has committed artifacts with correct state
|
||||
// (e.g. [x] for completed slices). Copying from the project root would
|
||||
// overwrite them with stale data ([ ] checkboxes) because the root is
|
||||
// not always fully synced.
|
||||
if (!branchExists) {
|
||||
copyPlanningArtifacts(basePath, info.path);
|
||||
}
|
||||
|
||||
// Run user-configured post-create hook (#597) — e.g. copy .env, symlink assets
|
||||
const hookError = runWorktreePostCreateHook(basePath, info.path);
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import {
|
|||
|
||||
import { milestoneIdSort, findMilestoneIds } from './guided-flow.js';
|
||||
import { nativeBatchParseGsdFiles, type BatchParsedFile } from './native-parser-bridge.js';
|
||||
import { isDbAvailable, _getAdapter } from './gsd-db.js';
|
||||
|
||||
import { join, resolve } from 'path';
|
||||
import { debugCount, debugTime } from './debug-logger.js';
|
||||
|
|
@ -149,30 +148,12 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|||
const fileContentCache = new Map<string, string>();
|
||||
const gsdDir = gsdRoot(basePath);
|
||||
|
||||
// ── DB-first content loading ──
|
||||
// When the DB is available, load artifact content from the artifacts table
|
||||
// (indexed SELECT instead of O(N) file I/O). Falls back to native Rust batch
|
||||
// parser, which in turn falls back to sequential JS reads via cachedLoadFile.
|
||||
let dbContentLoaded = false;
|
||||
if (isDbAvailable()) {
|
||||
const adapter = _getAdapter();
|
||||
if (adapter) {
|
||||
try {
|
||||
const rows = adapter.prepare('SELECT path, full_content FROM artifacts').all();
|
||||
for (const row of rows) {
|
||||
const relPath = (row as Record<string, unknown>)['path'] as string;
|
||||
const content = (row as Record<string, unknown>)['full_content'] as string;
|
||||
const absPath = resolve(gsdDir, relPath);
|
||||
fileContentCache.set(absPath, content);
|
||||
}
|
||||
dbContentLoaded = rows.length > 0;
|
||||
} catch {
|
||||
// DB query failed — fall through to native batch parse
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!dbContentLoaded) {
|
||||
// NOTE: We intentionally do NOT load from the SQLite DB here (#759).
|
||||
// The DB's artifacts table is populated once during migrateFromMarkdown
|
||||
// and is never updated when files change on disk (e.g. roadmap [x] updates,
|
||||
// plan checkbox changes). Using stale DB content causes deriveState to
|
||||
// return incorrect phase/slice state, leading to infinite skip loops.
|
||||
// The native Rust batch parser is fast enough for state derivation.
|
||||
const batchFiles = nativeBatchParseGsdFiles(gsdDir);
|
||||
if (batchFiles) {
|
||||
for (const f of batchFiles) {
|
||||
|
|
@ -180,7 +161,6 @@ async function _deriveStateImpl(basePath: string): Promise<GSDState> {
|
|||
fileContentCache.set(absPath, f.rawContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load file content from batch cache first, falling back to disk read.
|
||||
|
|
|
|||
|
|
@ -248,31 +248,24 @@ async function main(): Promise<void> {
|
|||
}
|
||||
}
|
||||
|
||||
// ─── Test 5: Requirements counting from DB content ────────────────────
|
||||
console.log('\n=== derive-state-db: requirements from DB content ===');
|
||||
// ─── Test 5: Requirements counting from disk (DB no longer used for content) ─
|
||||
console.log('\n=== derive-state-db: requirements from disk content ===');
|
||||
{
|
||||
const base = createFixtureBase();
|
||||
try {
|
||||
// Write minimal milestone dir (needed for milestone discovery)
|
||||
mkdirSync(join(base, '.gsd', 'milestones', 'M001'), { recursive: true });
|
||||
// Do NOT write REQUIREMENTS.md to disk — only in DB
|
||||
|
||||
openDatabase(':memory:');
|
||||
insertArtifactRow('REQUIREMENTS.md', REQUIREMENTS_CONTENT, {
|
||||
artifact_type: 'requirements',
|
||||
});
|
||||
// Write REQUIREMENTS.md to disk (DB content is no longer used by deriveState)
|
||||
writeFile(base, 'REQUIREMENTS.md', REQUIREMENTS_CONTENT);
|
||||
|
||||
invalidateStateCache();
|
||||
const state = await deriveState(base);
|
||||
|
||||
// Requirements should come from DB
|
||||
assertEq(state.requirements?.active, 2, 'req-from-db: requirements.active = 2');
|
||||
assertEq(state.requirements?.validated, 1, 'req-from-db: requirements.validated = 1');
|
||||
assertEq(state.requirements?.total, 3, 'req-from-db: requirements.total = 3');
|
||||
|
||||
closeDatabase();
|
||||
// Requirements should come from disk
|
||||
assertEq(state.requirements?.active, 2, 'req-from-disk: requirements.active = 2');
|
||||
assertEq(state.requirements?.validated, 1, 'req-from-disk: requirements.validated = 1');
|
||||
assertEq(state.requirements?.total, 3, 'req-from-disk: requirements.total = 3');
|
||||
} finally {
|
||||
closeDatabase();
|
||||
cleanup(base);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue