* fix(commands): normalize gsd slash namespace drift * fix(#2855): address CodeRabbit findings on namespace drift PR Three CR findings, all valid: 1. autonomous.md line 783 still had `gsd:discuss-phase` (the PR's own normalization missed this line). Switched to `gsd-discuss-phase` and updated the matching test in autonomous-interactive.test.cjs that was asserting the now-retired colon form. 2. tests/bug-2543-gsd-slash-namespace.test.cjs source-grepped the fix-slash-commands.cjs script with .includes() rather than driving its transform behaviour. Refactored fix-slash-commands.cjs to export a pure transformContent(src, cmdNames) function, kept the CLI behaviour unchanged via require.main, and replaced the source-grep block with five behavioural cases: rewrite, multi-occurrence, idempotence on canonical input, no-op on gsd-sdk/gsd-tools, and word-boundary safety. 3. tests/bug-2808-skill-hyphen-name.test.cjs matched `name:` anywhere in SKILL.md; a stray name: in the body could satisfy the assertion. Scoped the lookup to the YAML frontmatter block via the suggested diff (parse the leading --- ... --- region first, then find name: inside it). Full suite: 5854/5854 passing. * fix(#2855): address remaining CodeRabbit findings on PR #2858 Three structural concerns flagged on the namespace-drift fix PR: 1. scripts/fix-slash-commands.cjs:24 — `buildPattern([])` compiled `/gsd:()(?=[^a-zA-Z0-9_-]|$)/g`. The empty capture group still matches any `/gsd:` token followed by a non-word boundary (whitespace, EOL, punctuation), rewriting it to a stray `/gsd-`. Verified live: `transformContent("/gsd:", [])` → `"/gsd-"`. Added a guard returning null from `buildPattern` on empty input and updated `transformContent` and `processDir` to no-op when the pattern is null. 2. tests/autonomous-interactive.test.cjs:44-47 — assertion was `content.includes('gsd-discuss-phase') && content.includes('INTERACTIVE')`, which would false-pass on any unrelated co-occurrence (e.g. `INTERACTIVE=""` initialization plus a stray `gsd-discuss-phase` prose mention). Replaced with a structural extraction: locate the `**If \`INTERACTIVE\` is set:**` branch, bound it by the next `**If` / `<step>` boundary, and assert the `Skill(skill="gsd-discuss-phase", ...)` invocation lives inside that region. Tolerates whitespace around `(`, `skill`, and `=`. 3. tests/bug-2808-skill-hyphen-name.test.cjs:104 — colon-call regex was `Skill\(skill=...` and missed valid formatting like `Skill(skill = "gsd:cmd")` or `Skill( skill = ...)`. Loosened to `Skill\(\s*skill\s*=\s*...` so reformatting drift can't slip past the namespace guard. Verification: 5854/5854 pass on `npm test` from the rebased branch. * fix(#2855): drop pre-validation filter that hid namespace drift CR finding on tests/bug-2808-skill-hyphen-name.test.cjs:128: the test collected generated skill directories with `.filter(entry => entry.isDirectory() && entry.name.startsWith('gsd-'))`, then validated namespace invariants over that filtered list. Anything that violated the prefix invariant — `gsd:extract-learnings` (colon form), `extract_learnings` without prefix, `Gsd-foo` mis-cased — would silently disappear from the iteration and the test would falsely pass. Drop the `startsWith('gsd-')` filter so every generated directory shows up. Add explicit assertions before the existing per-skill loop: - directory list is non-empty (catches a broken converter that produces nothing) - every directory begins with `gsd-` - every directory contains no `:` - every directory contains no `_` Re-audited the full PR diff for the same anti-pattern: only this one site filtered before validating the namespace; bug-2643 and commands-doc-parity also use `readdirSync().filter()` but only by file extension, which is correct. 5854/5854 on `npm test`. * fix(#2855): address remaining CR findings (1 active + 2 nitpicks) Three findings on PR #2858, all the same root cause: input narrowing before validation lets drift slip past the guards. 1. tests/bug-2808-...:104 (active) — `colonCallRe` captured local names with `[a-z0-9-]+`, which excluded the underscore. A drift like `Skill(skill="gsd:extract_learnings")` (deprecated colon syntax with the old underscore filename) silently slid through. Broadened the capture to `[^'"\s)]+` so any malformed local name is surfaced; surrounding pattern (whitespace tolerance, escape support, flags) unchanged. 2. tests/bug-2643-...:43 (nitpick) — `extractSkillNamesHyphen` and `extractSkillNamesColon` had the same over-strict capture plus relied on a single regex over raw bytes, which the project test- rigor memory bans (`feedback_no_source_grep_tests.md`). Replaced with `extractSkillCalls(content)` — a small structural extractor that walks `Skill(` openers, locates each call's matching `)`, parses the body's `skill = "..."` keyword argument with permissive whitespace + quoting + escape handling, and returns `{ name, raw }` records. The two namespace-form helpers become thin filters over the structured output. Tightened the body class to `[^'"\\]+` so a trailing escape `\` before the closing quote (as in `Skill(skill=\"gsd-foo\", …)` written inside another string context) doesn't get included in the captured name. 3. tests/bug-2543-...:44 (nitpick) — `DOC_SEARCH_FILES` was a hand- curated 7-entry array. Every doc added in the future would silently weaken drift detection until someone remembered to extend the list. Replaced with `discoverDocSearchFiles(ROOT)`: globs every `.md` under `docs/` and adds `README.md` if present. New docs are picked up automatically. Re-audited the diff surface for similar narrowings; no other sites filter or constrain before validating namespace invariants. 5854/5854 on `npm test`. * fix(#2855): recurse docs/ tree so localized translations are scanned too CR finding: discoverDocSearchFiles() stopped at docs/*.md, leaving localized translation trees (docs/ja-JP/, docs/zh-CN/, docs/ko-KR/, docs/pt-BR/) and other nested doc collections (docs/skills/, docs/superpowers/) invisible to the namespace-drift invariant. Verified the gap: docs/ has 6 nested directories with ~30 .md files that the previous top-level-only scan was skipping. None contain /gsd: references today, but a future translation update or new doc subdir could leak drift. Switch to an iterative stack walk so every .md under docs/ is scanned regardless of depth. Stack form (rather than recursion) avoids the risk of running into the call-stack limit on deep doc trees. 5854/5854 on `npm test`. --------- Co-authored-by: Tom Boucher <trekkie@nomorestars.com>
82 lines
3.0 KiB
JavaScript
82 lines
3.0 KiB
JavaScript
'use strict';
|
|
/**
|
|
* One-shot script: replace retired /gsd:<cmd> with /gsd-<cmd> for known command names.
|
|
* Only replaces when followed by a word boundary (space, newline, quote, backtick, ), end).
|
|
*
|
|
* The transform is exported as a pure function so it can be unit-tested directly
|
|
* (see tests/bug-2543-gsd-slash-namespace.test.cjs) without needing fixture files.
|
|
*/
|
|
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const COMMANDS_DIR = path.join(__dirname, '..', 'commands', 'gsd');
|
|
const SEARCH_DIRS = [
|
|
path.join(__dirname, '..', 'get-shit-done', 'bin', 'lib'),
|
|
path.join(__dirname, '..', 'get-shit-done', 'workflows'),
|
|
path.join(__dirname, '..', 'get-shit-done', 'references'),
|
|
path.join(__dirname, '..', 'get-shit-done', 'templates'),
|
|
path.join(__dirname, '..', 'get-shit-done', 'contexts'),
|
|
path.join(__dirname, '..', 'commands', 'gsd'),
|
|
];
|
|
const EXTENSIONS = new Set(['.md', '.cjs', '.js']);
|
|
|
|
function buildPattern(cmdNames) {
|
|
// Empty input would compile `/gsd:()(?=[^a-zA-Z0-9_-]|$)/g`, which the regex
|
|
// engine still matches at any `/gsd:` token followed by a non-word boundary
|
|
// (e.g. EOL, whitespace, punctuation) — rewriting it to a stray `/gsd-`.
|
|
// Short-circuit so the caller can no-op on a missing/empty registry rather
|
|
// than perform an unintended broad rewrite.
|
|
if (!Array.isArray(cmdNames) || cmdNames.length === 0) return null;
|
|
const sorted = [...cmdNames].sort((a, b) => b.length - a.length); // longest first to avoid partial matches
|
|
return new RegExp(`/gsd:(${sorted.join('|')})(?=[^a-zA-Z0-9_-]|$)`, 'g');
|
|
}
|
|
|
|
/**
|
|
* Pure transform: rewrite retired `/gsd:<cmd>` to `/gsd-<cmd>` for the given command names.
|
|
* Returns the rewritten string. Identifiers not in `cmdNames` (e.g. `/gsd:sdk`,
|
|
* `/gsd:tools`) are left untouched.
|
|
*/
|
|
function transformContent(src, cmdNames) {
|
|
const pattern = buildPattern(cmdNames);
|
|
if (!pattern) return src;
|
|
return src.replace(pattern, (_, cmd) => `/gsd-${cmd}`);
|
|
}
|
|
|
|
function readCmdNames() {
|
|
return fs.readdirSync(COMMANDS_DIR)
|
|
.filter(f => f.endsWith('.md'))
|
|
.map(f => f.replace(/\.md$/, ''));
|
|
}
|
|
|
|
function processDir(dir, cmdNames) {
|
|
const pattern = buildPattern(cmdNames);
|
|
if (!pattern) return;
|
|
let entries;
|
|
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
|
|
for (const e of entries) {
|
|
const full = path.join(dir, e.name);
|
|
if (e.isDirectory()) {
|
|
processDir(full, cmdNames);
|
|
} else if (EXTENSIONS.has(path.extname(e.name))) {
|
|
const src = fs.readFileSync(full, 'utf-8');
|
|
const replaced = transformContent(src, cmdNames);
|
|
if (replaced !== src) {
|
|
fs.writeFileSync(full, replaced, 'utf-8');
|
|
const count = (src.match(pattern) || []).length;
|
|
console.log(` ${count} replacements: ${path.relative(path.join(__dirname, '..'), full)}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (require.main === module) {
|
|
const cmdNames = readCmdNames();
|
|
for (const dir of SEARCH_DIRS) {
|
|
processDir(dir, cmdNames);
|
|
}
|
|
console.log('Done.');
|
|
}
|
|
|
|
module.exports = { transformContent, buildPattern };
|