refactor: hard-fork GSD -> MSD (Make Software Done)

Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
This commit is contained in:
Jakub Zych
2026-10-06 01:47:40 +02:00
parent fe069b2a56
commit a9a7a328e6
2763 changed files with 78465 additions and 78434 deletions

View File

@@ -3,10 +3,10 @@
/**
* Unit tests for the smart-entry situation classifier.
*
* Spec: docs/superpowers/specs/2026-06-27-gsd-smart-entry-design.md
* Spec: docs/superpowers/specs/2026-06-27-msd-smart-entry-design.md
*
* Covers: all 11 situations, priority ordering (paused beats blocked), JSON
* shape invariants (exactly one recommended, commands are /gsd:* slash forms),
* shape invariants (exactly one recommended, commands are /msd:* slash forms),
* and the --json / human output modes.
*/
@@ -20,9 +20,9 @@ const { runNode } = require('./helpers/process-seam.cjs');
const { gitOrThrow, throwIfFailed } = require('./helpers/git-fixture.cjs');
const { PROBE_TIMEOUT_MS } = require('./helpers/timeouts.cjs');
const smartEntry = require('../gsd-core/bin/lib/smart-entry.cjs');
const smartEntry = require('../msd-core/bin/lib/smart-entry.cjs');
const { classify, classifyProject, detectSignals, SITUATIONS } = smartEntry;
const TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
const TOOLS = path.join(__dirname, '..', 'msd-core', 'bin', 'msd-tools.cjs');
// ─── Fixture helpers ─────────────────────────────────────────────────────────
@@ -31,7 +31,7 @@ const TOOLS = path.join(__dirname, '..', 'gsd-core', 'bin', 'gsd-tools.cjs');
* @returns {string} tmpDir path
*/
function makeProject({ state, roadmap = false, git = false, verifyFail = false } = {}) {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-'));
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-'));
fs.mkdirSync(path.join(tmpDir, '.planning', 'phases'), { recursive: true });
if (state !== undefined) {
fs.writeFileSync(path.join(tmpDir, '.planning', 'STATE.md'), state);
@@ -92,7 +92,7 @@ describe('smart-entry: situation coverage', () => {
const CASES = [
['no-project', () => {
// Truly empty dir — no .planning at all.
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-none-'));
const d = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-none-'));
return track(d);
}],
['paused', () => track(makeProject({ state: state({ status: 'planning', paused_at: '2026-06-01T00:00:00Z' }), roadmap: true }))],
@@ -204,7 +204,7 @@ describe('smart-entry: idle-stranded (git-dependent)', () => {
const actions = smartEntry.actionsFor(situation, forced);
assert.equal(actions[0].id, 'ship');
assert.equal(actions[0].recommended, true);
assert.equal(actions[0].command, '/gsd:ship');
assert.equal(actions[0].command, '/msd:ship');
});
});
@@ -287,7 +287,7 @@ describe('smart-entry: priority ordering', () => {
});
test('no-project beats everything (no .planning)', () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-empty-'));
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-empty-'));
track(tmpDir);
const result = classifyProject(tmpDir);
assert.equal(result.situation, 'no-project');
@@ -303,7 +303,7 @@ describe('smart-entry: real STATE.md schema (nested progress YAML + body Phase f
function realState({ status, phase, totalPhases, percent }) {
const fm = [
'---',
'gsd_state_version: 1.0',
'msd_state_version: 1.0',
`status: ${status}`,
'last_activity: 2026-06-13',
'progress:',
@@ -352,7 +352,7 @@ describe('smart-entry: real STATE.md schema (nested progress YAML + body Phase f
}));
const result = classifyProject(dir);
assert.equal(result.situation, 'executing');
// Forward motion delegates to /gsd:progress --next, not a raw execute-phase.
// Forward motion delegates to /msd:progress --next, not a raw execute-phase.
assert.equal(result.recommended, 'progress-next');
});
});
@@ -360,14 +360,14 @@ describe('smart-entry: real STATE.md schema (nested progress YAML + body Phase f
describe('smart-entry: in-project advancement delegates to the gated engine', () => {
afterEach(removeAll);
// Reconciliation guard (#1787): /gsd:next must not re-implement forward routing.
// Reconciliation guard (#1787): /msd:next must not re-implement forward routing.
// For every in-project forward-motion situation the recommended action is
// `/gsd:progress --next` (workflows/next.md), so Route 0's resume-incomplete
// `/msd:progress --next` (workflows/next.md), so Route 0's resume-incomplete
// -phase invariant + Gates 1-3 are never bypassed. Re-deriving advancement here
// is what got the old flat /gsd-next removed (#3054). The specific command
// is what got the old flat /msd-next removed (#3054). The specific command
// (execute-phase / plan-phase / verify-work) stays as an explicit secondary.
for (const situation of ['planning', 'executing', 'verify-pending']) {
test(`${situation}: recommended action is /gsd:progress --next`, () => {
test(`${situation}: recommended action is /msd:progress --next`, () => {
const signals = {
current_phase: 2, total_phases: 5, status: situation, progress: 40,
has_planning: true, has_roadmap: true, git_dirty: false, git_unpushed: false,
@@ -376,19 +376,19 @@ describe('smart-entry: in-project advancement delegates to the gated engine', ()
const actions = smartEntry.actionsFor(situation, signals);
const recommended = actions.filter((a) => a.recommended);
assert.equal(recommended.length, 1, 'exactly one recommended action');
assert.equal(recommended[0].command, '/gsd:progress --next');
assert.equal(recommended[0].command, '/msd:progress --next');
});
}
// Remediation / lifecycle situations are OFF the linear advance path — they
// keep direct specific recommendations (their distinct value over --next).
const DIRECT = {
'no-project': '/gsd:new-project',
paused: '/gsd:resume-work',
blocked: '/gsd:debug',
'verify-failed': '/gsd:verify-work',
'idle-stranded': '/gsd:ship',
complete: '/gsd:new-milestone',
'no-project': '/msd:new-project',
paused: '/msd:resume-work',
blocked: '/msd:debug',
'verify-failed': '/msd:verify-work',
'idle-stranded': '/msd:ship',
complete: '/msd:new-milestone',
};
for (const [situation, command] of Object.entries(DIRECT)) {
test(`${situation}: keeps its direct recommendation (${command})`, () => {
@@ -407,7 +407,7 @@ describe('smart-entry: in-project advancement delegates to the gated engine', ()
describe('smart-entry: per-situation action invariants (all 11)', () => {
// Lock the action-set contract for EVERY situation, not just the 6 sampled by
// the JSON-shape test: exactly one recommended, 1-4 actions, unique ids, and
// /gsd:* command forms. Guards the reconciliation (and future edits) against
// /msd:* command forms. Guards the reconciliation (and future edits) against
// silently breaking these for a less-common situation.
const sampleSignals = {
current_phase: 2, total_phases: 5, status: 'executing', progress: 60,
@@ -415,14 +415,14 @@ describe('smart-entry: per-situation action invariants (all 11)', () => {
paused: false, blockers: [], has_git: true, verify_failed: false, stale_activity: false,
};
for (const situation of SITUATIONS) {
test(`${situation}: exactly one recommended, 1-4 unique-id /gsd:* actions`, () => {
test(`${situation}: exactly one recommended, 1-4 unique-id /msd:* actions`, () => {
const actions = smartEntry.actionsFor(situation, sampleSignals);
assert.ok(actions.length >= 1 && actions.length <= 4, `${situation}: 1-4 actions (got ${actions.length})`);
assert.equal(actions.filter((a) => a.recommended).length, 1, `${situation}: exactly one recommended`);
const ids = actions.map((a) => a.id);
assert.equal(new Set(ids).size, ids.length, `${situation}: action ids are unique`);
for (const a of actions) {
assert.ok(a.command.startsWith('/gsd:'), `${situation}/${a.id}: command is a /gsd: slash form`);
assert.ok(a.command.startsWith('/msd:'), `${situation}/${a.id}: command is a /msd: slash form`);
}
});
}
@@ -431,7 +431,7 @@ describe('smart-entry: per-situation action invariants (all 11)', () => {
describe('smart-entry: JSON shape invariants', () => {
afterEach(removeAll);
test('every situation yields exactly one recommended action and /gsd:* commands', () => {
test('every situation yields exactly one recommended action and /msd:* commands', () => {
const dirs = [
track(makeProject()), // no-project
track(makeProject({ state: state({ status: 'planning', paused_at: '2026-06-01T00:00:00Z' }), roadmap: true })), // paused
@@ -446,7 +446,7 @@ describe('smart-entry: JSON shape invariants', () => {
assert.equal(recommended.length, 1, `${result.situation}: exactly one recommended`);
assert.equal(recommended[0].id, result.recommended, `${result.situation}: recommended id matches`);
for (const a of result.actions) {
assert.ok(a.command.startsWith('/gsd:'), `${result.situation}/${a.id}: command is a slash form`);
assert.ok(a.command.startsWith('/msd:'), `${result.situation}/${a.id}: command is a slash form`);
}
assert.ok(result.summary.length > 0, `${result.situation}: summary non-empty`);
assert.ok(result.actions.length >= 1 && result.actions.length <= 4, `${result.situation}: 1-4 actions`);
@@ -454,7 +454,7 @@ describe('smart-entry: JSON shape invariants', () => {
});
});
describe('smart-entry: CLI dispatch (gsd-tools smart-entry)', () => {
describe('smart-entry: CLI dispatch (msd-tools smart-entry)', () => {
afterEach(removeAll);
test('#4023: --json preserves decimal current_phase as a phase-id string', () => {
@@ -464,7 +464,7 @@ describe('smart-entry: CLI dispatch (gsd-tools smart-entry)', () => {
roadmap: true,
}));
const r = runNode([TOOLS, 'smart-entry', '--json', '--cwd', dir], { timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(r, 'gsd-tools smart-entry --json');
throwIfFailed(r, 'msd-tools smart-entry --json');
const out = JSON.parse(r.stdout);
assert.equal(out.signals.current_phase, phase);
}
@@ -472,21 +472,21 @@ describe('smart-entry: CLI dispatch (gsd-tools smart-entry)', () => {
test('--json in an empty dir returns no-project machine JSON', () => {
// A bare tmpdir with no .planning is a true no-project.
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-bare-'));
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-bare-'));
track(bare);
const r = runNode([TOOLS, 'smart-entry', '--json', '--cwd', bare], { timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(r, 'gsd-tools smart-entry --json');
throwIfFailed(r, 'msd-tools smart-entry --json');
const j = JSON.parse(r.stdout);
assert.equal(j.situation, 'no-project');
assert.equal(j.recommended, 'new-project');
assert.equal(j.actions[0].command, '/gsd:new-project');
assert.equal(j.actions[0].command, '/msd:new-project');
});
test('default (human) mode prints a plain summary line, not JSON', () => {
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-se-human-'));
const bare = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-se-human-'));
track(bare);
const r = runNode([TOOLS, 'smart-entry', '--cwd', bare], { timeoutMs: PROBE_TIMEOUT_MS });
throwIfFailed(r, 'gsd-tools smart-entry');
throwIfFailed(r, 'msd-tools smart-entry');
const out = r.stdout;
assert.ok(!out.startsWith('{'), 'human mode is not JSON');
assert.match(out, /No project yet/);
@@ -618,7 +618,7 @@ describe('smart-entry: stale_activity honors the template\'s "date — descripti
// not depend on the wall clock.
const FIXED_NOW = () => Date.parse('2026-08-01T00:00:00Z');
// gsd-core's own STATE.md carries last_activity as "YYYY-MM-DD — <description>"
// msd-core's own STATE.md carries last_activity as "YYYY-MM-DD — <description>"
// (templates/state.md prescribes `Last activity: [YYYY-MM-DD] — [What happened]`
// for the body; the frontmatter mirrors it). Before the fix, parseActivityTimestamp
// ran Date.parse on the whole string → NaN → staleActivity failed OPEN to false,
@@ -628,7 +628,7 @@ describe('smart-entry: stale_activity honors the template\'s "date — descripti
test('frontmatter last_activity with " — description" suffix is detected stale', () => {
const stateMd = [
'---',
'gsd_state_version: 1.0',
'msd_state_version: 1.0',
'status: executing',
'last_activity: 2026-06-08 — Milestone 2 executed autonomously (all passed)',
'progress:',
@@ -975,7 +975,7 @@ describe('smart-entry: stale_activity honors the template\'s "date — descripti
const {
_resetUnusableInputWarningsForTests,
_unusableInputEmissionCountForTests,
} = require('../gsd-core/bin/lib/unusable-input.cjs');
} = require('../msd-core/bin/lib/unusable-input.cjs');
_resetUnusableInputWarningsForTests();
const stateMd = [
'---',
@@ -1007,7 +1007,7 @@ describe('detectSignals — state_head commit-age freshness (#2573)', () => {
afterEach(() => { while (dirs.length) cleanup(dirs.pop()); });
function gitProject(stateHead) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2573-se-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2573-se-'));
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
runGit(['init', '-q'], { cwd: dir });
runGit(['config', 'user.email', 't@t.com'], { cwd: dir });
@@ -1080,7 +1080,7 @@ describe('detectSignals — state_head commit-age freshness (#2573)', () => {
});
test('non-git project → null (no signal), never throws', () => {
const dir = track(fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2573-nogit-')));
const dir = track(fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2573-nogit-')));
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
fs.writeFileSync(
path.join(dir, '.planning', 'STATE.md'),
@@ -1101,9 +1101,9 @@ describe('detectSignals — state_head commit-age freshness (#2573)', () => {
const {
_resetUnusableInputWarningsForTests,
_unusableInputEmissionCountForTests,
} = require('../gsd-core/bin/lib/unusable-input.cjs');
} = require('../msd-core/bin/lib/unusable-input.cjs');
_resetUnusableInputWarningsForTests();
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-2573-compose-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'msd-2573-compose-'));
fs.mkdirSync(path.join(dir, '.planning'), { recursive: true });
runGit(['init', '-q'], { cwd: dir });
runGit(['config', 'user.email', 't@t.com'], { cwd: dir });
@@ -1149,7 +1149,7 @@ describe('#3099: unusable last_activity emits a diagnostic', () => {
const {
_resetUnusableInputWarningsForTests,
_unusableInputEmissionCountForTests,
} = require('../gsd-core/bin/lib/unusable-input.cjs');
} = require('../msd-core/bin/lib/unusable-input.cjs');
function makeStateWithActivity(activity) {
return [