fix: scan bin/lib subdirectories in the inventory-manifest generator

gen-inventory-manifest.cjs's cli_modules family did a flat readdirSync
of gsd-core/bin/lib/, invisible to anything shipped in a subdirectory.
Found while registering this phase's 8 health-diagnostic-rules/*.cjs
files in docs/INVENTORY.md (Standards-axis review) — the automated
manifest cross-check couldn't see them even though the manual
INVENTORY.md rows were correct.

Adds collectOneLevelSubdirs (mirrors the existing collectNested's
defensive statOrNull style) and merges flat + one-level-subdirectory
results into cli_modules's single sorted array, using the same
<subdir>/<file>.cjs key format INVENTORY.md's rows already use.

Regenerating the manifest surfaced that three OTHER existing
subdirectories (installer-migrations/, host-integration-adapters/,
observability/ — pre-existing, unrelated to this phase) were equally
invisible and had zero docs/INVENTORY.md rows at all. Added all 15
missing rows rather than leave a gap the fix itself just exposed.

Also fixes 3 pre-existing lint-legacy-dir-name violations in the
installer-migrations rows (legitimate references to the historical
get-shit-done -> gsd-core rename these migrations clean up — marked
with the guard's own gsd-allow-legacy-name exemption) and a stale
health-diagnostic.cjs row that still said "RULES ships empty."
This commit is contained in:
sim
2026-08-13 02:56:12 -04:00
parent 9f8bcfa3a6
commit 42729b21fa
5 changed files with 201 additions and 12 deletions

View File

@@ -378,9 +378,19 @@
"graphify.cjs",
"gsd2-import.cjs",
"handshake-serialized.cjs",
"health-diagnostic-rules/agent-install.cjs",
"health-diagnostic-rules/config-validation.cjs",
"health-diagnostic-rules/milestone-archive-hygiene.cjs",
"health-diagnostic-rules/phase-structure.cjs",
"health-diagnostic-rules/roadmap-disk-consistency.cjs",
"health-diagnostic-rules/root-existence.cjs",
"health-diagnostic-rules/state-consistency.cjs",
"health-diagnostic-rules/worktree-health.cjs",
"health-diagnostic-types.cjs",
"health-diagnostic.cjs",
"hook-bus.cjs",
"host-integration-adapters/cline-sdk-binding.cjs",
"host-integration-adapters/imperative-hook-bus.cjs",
"host-integration-sdk.cjs",
"host-integration.cjs",
"host-runtime-detection.cjs",
@@ -394,6 +404,16 @@
"installer-migration-authoring.cjs",
"installer-migration-report.cjs",
"installer-migrations.cjs",
"installer-migrations/000-first-time-baseline.cjs",
"installer-migrations/001-legacy-orphan-files.cjs",
"installer-migrations/002-codex-legacy-hooks-json.cjs",
"installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs",
"installer-migrations/004-prune-stale-pristine-snapshots.cjs",
"installer-migrations/005-opencode-baseline-commands-dir.cjs",
"installer-migrations/006-pi-extension-cjs-to-js.cjs",
"installer-migrations/007-retire-config-root-commonjs-marker.cjs",
"installer-migrations/008-cursor-retire-commands-surface.cjs",
"installer-migrations/009-pi-retire-reserved-hooks-dir.cjs",
"intel-command-router.cjs",
"intel.cjs",
"io.cjs",
@@ -411,6 +431,9 @@
"model-profiles.cjs",
"model-resolver.cjs",
"normalize-test-command.cjs",
"observability/event.cjs",
"observability/logger.cjs",
"observability/redaction.cjs",
"onboard-projection.cjs",
"package-identity.cjs",
"package-legitimacy.cjs",

View File

@@ -432,6 +432,16 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| Module | Responsibility |
|--------|----------------|
| `installer-migrations/000-first-time-baseline.cjs` | Installer migration: records the first-time installer migration baseline scan — walks per-runtime install surfaces so pre-existing files are classified before any later migration runs |
| `installer-migrations/001-legacy-orphan-files.cjs` | Installer migration: removes manifest-managed legacy orphan hook files (`hooks/gsd-notify.sh`, `hooks/statusline.js`) |
| `installer-migrations/002-codex-legacy-hooks-json.cjs` | Installer migration: removes legacy Codex `hooks.json` GSD hook registrations |
| `installer-migrations/003-rename-get-shit-done-to-gsd-core.cjs` | Installer migration: removes stale legacy `get-shit-done/` runtime directory files after the rename to `gsd-core/` (#604) |<!-- gsd-allow-legacy-name -->
| `installer-migrations/004-prune-stale-pristine-snapshots.cjs` | Installer migration: removes stale `gsd-pristine/get-shit-done/` snapshot files left behind by the get-shit-done → gsd-core rename (#604, #934) |<!-- gsd-allow-legacy-name -->
| `installer-migrations/005-opencode-baseline-commands-dir.cjs` | Installer migration: baselines pre-existing OpenCode `commands/` (plural) files missed by migration 000's RUNTIME_SURFACES list (#2329 follow-up) |
| `installer-migrations/006-pi-extension-cjs-to-js.cjs` | Installer migration: retires pi's stale `extensions/gsd.cjs` after #2470 renamed the installed native extension to `extensions/gsd.js` |
| `installer-migrations/007-retire-config-root-commonjs-marker.cjs` | Installer migration: retires the config-root `{"type":"commonjs"}` marker that pre-#2544 installs wrote over `<configRoot>/package.json` |
| `installer-migrations/008-cursor-retire-commands-surface.cjs` | Installer migration: retires Cursor's duplicate `commands/` surface now that skills are the sole workflow surface (#2644) |
| `installer-migrations/009-pi-retire-reserved-hooks-dir.cjs` | Installer migration: retires pi's legacy `hooks/` directory after GSD's shared hook bundle moved to `gsd-hooks/` (#3023) |
| `active-workstream-store.cjs` | Workstream source precedence and selection (CLI `--ws` > `GSD_WORKSTREAM` env > stored pointer); name validation and environment propagation |
| `adr-parser.cjs` | ADR decision parser for plan-phase ingest express path; normalizes section synonyms, parses status/decision/scope fences, and enforces status rejection gates |
| `agent-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools agent` |
@@ -458,6 +468,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `claude-orchestration.cjs` | Claude Orchestration capability (#1143) — Workflow-tool backend detection + emitter; `detectWorkflowBackend` fail-closed gate (`{available, backend: 'workflow'\|'inline', reason}`, degrades to today's inline behavior unless every gate opens) and `emitWorkflowScript` (maps GSD's wave/plan model onto Workflow primitives: wave → sequential `parallel()` barriers, plan → `agent(...)` with per-plan worktree isolation mirroring the inline path). Pure, zero external dependencies, never throws; never invokes the Workflow tool itself |
| `cli-exit.cjs` | `ExitError` class and `runMain()` helper — CLI entrypoints throw `ExitError` instead of calling `process.exit()`; `runMain()` translates the outcome into `process.exitCode` so output flushes cleanly |
| `cjs-command-router-adapter.cjs` | Shared compatibility adapter for manifest-backed CJS command-family routers |
| `host-integration-adapters/cline-sdk-binding.cjs` | Cline SDK binding — pure AgentPlugin `beforeTool` planning-artifact guard and `createAgentModel` model-override resolution adapters, no `@cline/sdk` import (ADR-1239 Phase D, #2090) |
| `clock.cjs` | Injectable clock seam (now/sleep) for deterministic lock testing |
| `clusters.cjs` | Skill cluster definitions for the runtime surface module (ADR-0011 Phase 2) |
| `code-review-flags.cjs` | Typed flag parser for `/gsd-code-review`; exports `parseCodeReviewFlags(argv)` (→ `{ fix, all, auto, depth, files }`) and `resolveCodeReviewWorkflow(flags)` (→ `'code-review.md' \| 'code-review-fix.md'`); canonical dispatch seam for `--fix`/`--all`/`--auto` routing |
@@ -487,6 +498,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `eval-command-router.cjs` | Routes the `eval.score` verb (compiled from `src/eval-command-router.cts`, gitignored) — thin dispatcher into the eval scoring module (#1579) |
| `eval.cjs` | Deterministic eval scoring (compiled from `src/eval.cts`, gitignored) — `computeEvalScore` (coverage*0.6 + infra*0.4, bands 80/60/40) + `cmdEvalScore` CLI domain guard; moves the gsd-eval-auditor's weighted arithmetic out of the prompt into code (#10 / #1579) |
| `estimate-cli.cjs` | I/O seam over `phase-estimation.cjs` — the `estimate-check` and `estimate-calibration` query verbs; reads the `workflow.smart_zone_tokens` budget and `.planning/estimation-calibration.json`, both degrading to defaults rather than failing planning (#2630) |
| `observability/event.cjs` | DispatchEvent shape factory for every Hub dispatch — traceId/parentTraceId/command/result/timestamp record consumed by DispatchLogger (#177, ADR-0174 P1.3/P1.4) |
| `fallow-runner.cjs` | Fallow audit adapter for `/gsd-code-review`: binary resolution (`PATH` then `node_modules/.bin`), actionable missing-binary errors, and structural findings normalization |
| `federated-config.cjs` | Defensive merge of capability-declared config slices into the loadConfig return value — ADR-857 phase 3b; exports `mergeFederatedConfig({ configSchema, isCentralKey, userConfig })` → `{ values, validKeys, warnings }`; live for migrated Capability keys that are atomically removed from the central config schema |
| `frontmatter.cjs` | YAML frontmatter CRUD operations |
@@ -496,9 +508,10 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `graphify-command-router.cjs` | ADR-959 capability command router for `gsd-tools graphify` — dispatches build/query/status/diff subcommands; first real capability command cutover (phase 4d-impl-2) |
| `gsd2-import.cjs` | External-plan ingest for `/gsd-import --from-gsd2` |
| `health-diagnostic-types.cjs` | Shared, dependency-free `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums and `Diagnostic`/`Remedy`/`Rule` types for `validate health` — split out of `health-diagnostic.cjs` so its rule-group files can depend on the enums/types without a CJS circular require back into the evaluator (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
| `health-diagnostic.cjs` | Frozen rule-table contract for `validate health` — `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums, `Diagnostic`/`Remedy`/`Rule` shapes, the `RULES` table (empty in this phase; a later migration batch appends the 32 rules extracted from `cmdValidateHealth`), `evaluateRules` (throws on duplicate rule codes), and `applyRepairs` (the `--repair`/`--backfill` dispatcher — refuses `DESTRUCTIVE`-risk remedies) (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
| `health-diagnostic.cjs` | Frozen rule-table contract for `validate health` — `SEVERITY`/`REMEDY_ACTION`/`REMEDY_RISK` enums, `Diagnostic`/`Remedy`/`Rule` shapes, the fully-wired `RULES` table (the static concatenation of the 31 rules exported by the eight `health-diagnostic-rules/*.cjs` group files), `evaluateRules` (throws on duplicate rule codes), and `applyRepairs` (the real `--repair`/`--backfill` dispatcher with real per-action handlers — refuses `DESTRUCTIVE`-risk remedies) (ADR-3180 §8.2/§8.3/§8.5, Phase 11, #3309) |
| `host-integration.cjs` | Host-Integration Interface (ADR-1239 Phase A) — negotiated capability contract over the six host-integration points; `negotiateHostCapabilities` fail-closes on undeclared/unknown/`undocumented` values, typed degradation ladder, host-capability profiles; the 8 `runtime.hostIntegration` axes are validated in `capability-validator.cjs` and sourced per-CLI in `docs/reference/host-integration-capability-matrix.md` |
| `host-runtime-detection.cjs` | Host Runtime Detection Module (ADR-2313 Phase 5, #3245) — the detection rung beneath `GSD_RUNTIME` and `.planning/config.json` `runtime` that lets `init` report `agent_runtime: codex` inside a Codex session instead of the hardcoded `claude` default; `detectHostRuntime` returns the typed `{runtime, source, signal}` from citation-backed Codex signals (`CODEX_SANDBOX`/`CODEX_SANDBOX_NETWORK_DISABLED`, else `CODEX_HOME` + `config.toml`), `resolveReportedRuntime` composes the full ladder. Pure, injectable, never writes, never shells out |
| `host-integration-adapters/imperative-hook-bus.cjs` | Imperative hook-bus adapter — descriptor-driven `hooks.json` binding generalized from the Cursor-specific writer, resolving the negotiated `hookBus` axis against a host's documented `hostBehaviors.managedHookEvents` list; pure, no I/O (ADR-1239 Phase D, #2089) |
| `init-command-router.cjs` | Thin CJS subcommand router adapter for `gsd-tools init` |
| `init.cjs` | Compound context loading for each workflow type |
| `install-effort-resolver.cjs` | Install-time effort resolution — `readGsdEffectiveEffortConfig` (merges `~/.gsd/defaults.json` + project `.planning/config.json`) + `resolveInstallTimeEffort`, extracted from `bin/install.js` (#2071) so `gsd-tools effort sync` can require it from the shipped runtime instead of the never-copied package-root installer; install.js imports them back (single source) |
@@ -514,6 +527,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `io.cjs` | CLI I/O primitives — `output`/`error` emission, JSON-error mode, and large-payload temp-file spillover (extracted from `core.cjs`, ADR-857) |
| `learnings.cjs` | Cross-phase learnings extraction for `/gsd-extract-learnings` |
| `legacy-cleanup.cjs` | Detect and remove leftover get-shit-done-cc artifacts; exports `planLegacyCleanup` (pure scan) and `applyLegacyCleanup` (thin IO applier) that root out stale files from the old package across every GSD-managed runtime config directory (#607) |
| `observability/logger.cjs` | DispatchLogger interface + default implementation — silent on success, structured stderr JSON on error, opt-in `.gsd-trace.jsonl` audit file (`GSD_AUDIT=1`), args omitted unless `GSD_AUDIT_ARGS=1` (#177, ADR-0174 P1.3) |
| `loop-host-contract.cjs` | Generated Loop Host Contract — 12 loop points, per-step agent roles, and core artifacts for the five-step pipeline (discuss/plan/execute/verify/ship); emitted by `scripts/gen-loop-host-contract.cjs --write` (ADR-894 §3); consumed by `gen-capability-registry.cjs` |
| `loop-resolver.cjs` | Loop Extension Point resolver — ADR-857 phase 3c/6 registry-consuming query; given a canonical loop point, filters `byLoopPoint` by resolved Capability State plus config activation (`when` key traversal with prototype-pollution guard), returns `{ point, activeHooks, rendered }` envelope; `resolveLoopHooks` and `renderLoopHooks` are pure (no I/O); command surface: `gsd-tools loop render-hooks <point> [--config-dir <path>]` |
| `markdown-sectionizer.cjs` | Canonical markdown-structure parsing seam (ADR-1372, epic #1372) — pure, Node built-ins only; exports `stripFencedCode` (CommonMark-correct fence stripper, CRLF-safe), `stripInlineCode` (per-line CommonMark inline-code-span stripper, #2365), `tokenizeHeadings` (ATX headings outside fenced blocks), `collectSections`/`collectSection` (line-by-line section collection with `bodyStart`/`bodyEnd` offsets), `iterateBullets` (dash/checkbox/numbered markers), `extractTaggedBlocks` (inner text of `<tag>…</tag>` blocks, caller decides fence-stripping), `replaceSection` (pure character-offset body splice for read-modify-write callers), and `withSection` (resolve a section by heading/predicate and run an edit callback against ONLY its body, splicing the result back — ADR-2143 §4 bounded mutation); foundation for T0–T7 migration tiers retiring 8+ ad-hoc parsers |
@@ -543,6 +557,7 @@ Full listing: `gsd-core/bin/lib/*.cjs`.
| `profile-pipeline-command-router.cjs` | ADR-959 capability command router for the profile-pipeline command family — dispatches scan-sessions, extract-messages, profile-sample (pipeline phase) and write-profile, profile-questionnaire, generate-dev-preferences, generate-claude-profile, generate-claude-md (output phase); phase 6 cutover |
| `profile-pipeline.cjs` | User behavioral profiling data pipeline, session file scanning |
| `prompt-budget.cjs` | Pure token-budget accounting for review prompts — estimates tokens, applies deterministic trim priority (head-shrink PROJECT.md, proportional plan truncation, drop context/research/requirements, hard-fail guard), returns structured metadata for `review.max_prompt_tokens` (#3081) |
| `observability/redaction.cjs` | Arg redaction policy for dispatch events — args omitted from every emitted event by default, opt-in verbatim inclusion via `GSD_AUDIT_ARGS=1`; stateless env read, no module-level caching (#177) |
| `refactor-trigger-command-router.cjs` | ADR-959 capability command router for `gsd-tools refactor` (issue #1953) — dispatches evaluate/status/accept/decline subcommands for the complexity-triggered refactor capability; owns capability-activation gating, git invocation (via the `git-base-branch.cjs` `phaseStartCommit`/`changedFilesSince` adapters), config reads, phase-directory resolution, and the optional broken-windows ledger integration around the pure `complexity-trigger.cjs` leaf |
| `research-provider.cjs` | Research provider waterfall, confidence tiers, and planResearch (cache-hits + fetch plan) |
| `research-store.cjs` | Content-addressed research cache: sha256 keys, per-source TTL staleness, two-tier (user ~/.gsd / project .planning) store |

View File

@@ -117,6 +117,47 @@ function statOrNull(p) {
}
}
/**
* Collect `<dir>/<subdir>/<file>` entries as `<subdir>/<file>` keys — ONE level of
* subdirectory beneath `dir` itself, where the subdirectory's NAME is the thing being
* collected (unlike `collectNested`, there is no fixed subdir name to look for; every
* child directory of `dir` is scanned). This is what makes `gsd-core/bin/lib/<subdir>/*.cjs`
* (e.g. `health-diagnostic-rules/`, `installer-migrations/`, `host-integration-adapters/`,
* `observability/`) visible to the `cli_modules` family, mirroring the shape
* `docs/INVENTORY.md`'s CLI Modules table already uses for these files.
*
* Same defensive `statOrNull`-based style as `collectNested`: a stat/readdir failure on
* one entry is swallowed rather than thrown, so one unreadable subdirectory cannot take
* down `--check` for the whole repo.
*/
function collectOneLevelSubdirs({ dir, filter }) {
if (!fs.existsSync(dir)) return [];
const out = [];
let children;
try {
children = fs.readdirSync(dir);
} catch {
return [];
}
for (const child of children) {
const childStat = statOrNull(path.join(dir, child));
if (!childStat || !childStat.isDirectory()) continue;
const subdirPath = path.join(dir, child);
let files;
try {
files = fs.readdirSync(subdirPath);
} catch {
continue;
}
for (const file of files) {
const fileStat = statOrNull(path.join(subdirPath, file));
if (!fileStat || !fileStat.isFile() || !filter(file)) continue;
out.push([child, file].join('/'));
}
}
return out.sort();
}
function collectNested({ root, subdir, filter }) {
if (!fs.existsSync(root)) return [];
const out = [];
@@ -150,11 +191,16 @@ function collectNested({ root, subdir, filter }) {
function buildManifest() {
const manifest = { families: {} };
for (const { name, dir, filter, toName } of FAMILIES) {
manifest.families[name] = fs
const flat = fs
.readdirSync(dir)
.filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
.map(toName)
.sort();
.map(toName);
// `cli_modules` also ships subdirectory modules (`health-diagnostic-rules/`,
// `installer-migrations/`, `host-integration-adapters/`, `observability/`) invisible to
// the flat readdirSync above; merge them into the SAME sorted array, matching the single
// "CLI Modules" table shape docs/INVENTORY.md already uses (#3309).
const nested = name === 'cli_modules' ? collectOneLevelSubdirs({ dir, filter }) : [];
manifest.families[name] = [...flat, ...nested].sort();
}
for (const family of NESTED_FAMILIES) {
manifest.families[family.name] = collectNested(family);
@@ -209,4 +255,4 @@ if (require.main === module) {
// `DEFECT.GENERATIVE-FIX` divergence class: adding a family here while the test kept
// its own list meant the test silently verified fewer families than shipped, and still
// passed. The test now imports these, so the two surfaces cannot drift.
module.exports = { FAMILIES, NESTED_FAMILIES, collectNested, buildManifest };
module.exports = { FAMILIES, NESTED_FAMILIES, collectNested, collectOneLevelSubdirs, buildManifest };

View File

@@ -20,7 +20,7 @@ const MANIFEST_PATH = path.join(ROOT, 'docs', 'INVENTORY-MANIFEST.json');
// a family added to the generator but not here left this test silently verifying a
// subset while still reporting green. Importing makes divergence impossible rather than
// merely detectable.
const { FAMILIES, NESTED_FAMILIES, collectNested } = require('../scripts/gen-inventory-manifest.cjs');
const { FAMILIES, NESTED_FAMILIES, collectNested, collectOneLevelSubdirs } = require('../scripts/gen-inventory-manifest.cjs');
test('docs/INVENTORY-MANIFEST.json matches the filesystem', () => {
const committed = JSON.parse(fs.readFileSync(MANIFEST_PATH, 'utf8'));
@@ -28,11 +28,14 @@ test('docs/INVENTORY-MANIFEST.json matches the filesystem', () => {
const removals = [];
for (const { name, dir, filter, toName } of FAMILIES) {
const live = new Set(
fs.readdirSync(dir)
.filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
.map(toName),
);
const flat = fs.readdirSync(dir)
.filter((f) => fs.statSync(path.join(dir, f)).isFile() && filter(f))
.map(toName);
// `cli_modules` also ships one level of subdirectory modules (#3309); mirror
// buildManifest's special-case merge exactly, or this test would report every
// subdirectory file as a phantom removal.
const nested = name === 'cli_modules' ? collectOneLevelSubdirs({ dir, filter }) : [];
const live = new Set([...flat, ...nested]);
const recorded = new Set((committed.families || {})[name] || []);
for (const entry of live) {

View File

@@ -18,10 +18,11 @@ const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { collectNested } = require('../scripts/gen-inventory-manifest.cjs');
const { collectNested, collectOneLevelSubdirs } = require('../scripts/gen-inventory-manifest.cjs');
const { cleanup } = require('./helpers.cjs');
const MD_ONLY = (f) => f.endsWith('.md');
const CJS_ONLY = (f) => f.endsWith('.cjs');
/** Build a fixture tree: {parentName: {subdirName: [fileNames]}}. */
function buildTree(spec) {
@@ -174,3 +175,104 @@ test('collectNested is deterministic and sorted', (t) => {
assert.deepStrictEqual(first, second);
assert.deepStrictEqual(first, ['alpha/steps/c.md', 'zeta/steps/a.md', 'zeta/steps/b.md']);
});
// ─── collectOneLevelSubdirs — cli_modules one-level subdir scan (#3309) ───────
//
// Unlike collectNested's FIXED subdir name (many parents, one shared child-dir
// name like `steps`), this helper's `dir` IS the parent, and every one of ITS
// child directories is the thing being collected — e.g. `bin/lib/health-diagnostic-rules/`.
/** Build a fixture tree directly under `dir`: {subdirName: [fileNames]} \| flat file list. */
function buildSubdirTree(dir, spec) {
for (const [subdir, files] of Object.entries(spec)) {
const subdirPath = path.join(dir, subdir);
fs.mkdirSync(subdirPath, { recursive: true });
for (const f of files) fs.writeFileSync(path.join(subdirPath, f), '// fixture\n');
}
}
test('collectOneLevelSubdirs picks up a file inside a subdirectory', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
buildSubdirTree(dir, { 'health-diagnostic-rules': ['root-existence.cjs'] });
assert.deepStrictEqual(
collectOneLevelSubdirs({ dir, filter: CJS_ONLY }),
['health-diagnostic-rules/root-existence.cjs'],
);
});
test('collectOneLevelSubdirs merges multiple subdirectories, sorted', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
buildSubdirTree(dir, {
observability: ['redaction.cjs', 'event.cjs'],
'installer-migrations': ['001-x.cjs'],
});
assert.deepStrictEqual(
collectOneLevelSubdirs({ dir, filter: CJS_ONLY }),
['installer-migrations/001-x.cjs', 'observability/event.cjs', 'observability/redaction.cjs'],
);
});
test('collectOneLevelSubdirs contributes nothing for an empty subdirectory', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
fs.mkdirSync(path.join(dir, 'empty-subdir'), { recursive: true });
assert.deepStrictEqual(collectOneLevelSubdirs({ dir, filter: CJS_ONLY }), []);
});
test('collectOneLevelSubdirs ignores files directly in dir (flat scan owns those)', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
fs.writeFileSync(path.join(dir, 'top-level.cjs'), '// fixture\n');
buildSubdirTree(dir, { subdir: ['nested.cjs'] });
assert.deepStrictEqual(collectOneLevelSubdirs({ dir, filter: CJS_ONLY }), ['subdir/nested.cjs']);
});
test('collectOneLevelSubdirs applies the filter and ignores non-matching files', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
buildSubdirTree(dir, { subdir: ['keep.cjs', 'skip.md', 'skip.json'] });
assert.deepStrictEqual(collectOneLevelSubdirs({ dir, filter: CJS_ONLY }), ['subdir/keep.cjs']);
});
test('collectOneLevelSubdirs does not recurse past one level', (t) => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
buildSubdirTree(dir, { subdir: ['top.cjs'] });
const deep = path.join(dir, 'subdir', 'deeper');
fs.mkdirSync(deep, { recursive: true });
fs.writeFileSync(path.join(deep, 'too-deep.cjs'), '// fixture\n');
assert.deepStrictEqual(collectOneLevelSubdirs({ dir, filter: CJS_ONLY }), ['subdir/top.cjs']);
});
test('collectOneLevelSubdirs on a missing dir contributes nothing rather than throwing', () => {
assert.deepStrictEqual(
collectOneLevelSubdirs({ dir: path.join(os.tmpdir(), 'gsd-3309-does-not-exist'), filter: CJS_ONLY }),
[],
);
});
test('collectOneLevelSubdirs skips a dangling symlink without crashing the walk', (t) => {
if (process.platform === 'win32') {
t.skip('symlink creation requires elevation on Windows; the unstattable-entry path is asserted on macOS + Linux');
return;
}
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-3309-onelevel-'));
t.after(() => cleanup(dir));
buildSubdirTree(dir, { subdir: ['real.cjs'] });
fs.symlinkSync(path.join(dir, 'subdir', 'nope.cjs'), path.join(dir, 'subdir', 'dangling.cjs'));
assert.deepStrictEqual(
collectOneLevelSubdirs({ dir, filter: CJS_ONLY }),
['subdir/real.cjs'],
'one unreadable entry must not take down the whole scan',
);
});