Phase 3 of the CJS↔SDK hard-seam migration (parent #3524). Introduces the Builder/Reader pattern for paired Modules with mixed pure-and-I/O concerns — the template for Phase 4 and follow-up enhancements that migrate other paired Modules. Phase 1 and Phase 2 migrated Modules where both sides used character-equivalent logic. Phase 3 introduces the case where the pure logic is shareable but the I/O is legitimately per-side. The Builder/Reader split resolves this: - The Builder is pure — accepts pre-collected data (BuilderInputs struct), returns the typed projection. One source of truth; one generator-emitted CJS mirror. Drift is structurally impossible. - The Readers are per-side hand-authored Adapters that do the fs reads in their native idiom (currently both sync; either side can go async later without touching the Builder), then delegate to the Builder. - sdk/src/workstream-inventory/builder.ts — Builder source. 170 lines. Pure. Exports buildWorkstreamInventory(inputs), isCompletedInventory(status), plus the three typed inventory interfaces (WorkstreamPhaseInventory, WorkstreamInventory, WorkstreamInventoryList). - sdk/src/workstream-inventory/builder.test.ts — 18 vitest pinning fixtures across all status branches, progress-percent clamping, active-marker projection, and isCompletedInventory classifier. - sdk/scripts/gen-workstream-inventory-builder.mjs — generator. Captures function bodies via Function.prototype.toString(); emits with the standard GENERATED FILE banner. Includes a small `const relative = path.relative;` preamble in the output to handle ESM destructured imports in the compiled source. - sdk/scripts/check-workstream-inventory-builder-fresh.mjs — freshness check. Imports the generator function directly (rather than duplicating logic) — a cleaner pattern than Phase 1/2's approach. - get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs — generator-emitted CJS mirror. - tests/workstream-inventory-builder-generator.test.cjs — 16 parity assertions confirming CJS-generated output == SDK source output for every fixture. - bin/lib/workstream-inventory.cjs: 159 → 132 lines. Projection logic gone. `inspectWorkstream` and `listWorkstreamInventories` collect BuilderInputs via the existing sync fs functions and delegate to the Builder. `isCompletedInventory` re-exported from the Builder (its signature changed from object→string, but no external callers exist so the change is safe). - sdk/src/query/workstream-inventory.ts: 196 → 143 lines. Same shape, sync fs (the SDK was already sync — surprise from recon). Types re-exported from the Builder. - sdk/package.json: gen:workstream-inventory-builder and check:workstream-inventory-builder-fresh scripts. - package.json: proxy for the freshness check. - .githooks/pre-commit: drift block. - .github/workflows/test.yml: drift check step. - CONTEXT.md: amended "Workstream Inventory Module" entry to document the Builder/Reader split. - docs/INVENTORY.md, docs/INVENTORY-MANIFEST.json: +1 module count, +1 row for the generated builder. - Full suite: 9229/9229 pass (baseline 9215 + 14 net new from the parity assertions). - Vitest: 18 Builder fixtures pass. - Reader shrink: -27 lines on CJS, -53 lines on SDK. - Net diff (modified files only): +68 / -133 = 65-line reduction. New files (Builder, generator, freshness check, parity test) add ~600 lines of new structured code. 1. `isCompletedInventory` signature changed from isCompletedInventory(inventory: object) to isCompletedInventory(status: string). Original CJS exported the object form but no external caller passed an object — they all passed inventory.status. Verified by grep before committing. 2. Generator preamble. The compiled ESM uses `import { relative } from 'node:path'`, making `relative` a free variable in `buildWorkstreamInventory`. The generator emits `const relative = path.relative;` so the captured function body works in CJS. 3. Freshness check imports the generator. The freshness check imports the generator's buildWorkstreamInventoryBuilderCjs() function directly rather than duplicating generation logic. Cleaner than Phase 1/2; future generators should follow this. Shareable via the Builder/Reader pattern in future enhancements: - frontmatter (pure YAML/markdown parsing) - plan-scan (pure PLAN.md structure parsing) - decisions (pure decision-record parsing) - secrets (regex-based detection in text) - uat (UAT-criteria parsing) Structural divergence — different approach needed: - state — sync vs async file ops; mutation paths differ. - workstream — lifecycle ops; per-side API surface differs. - phase, roadmap, init, profile-output, template — large surfaces; each its own potential enhancement. None of these is in scope for Phase 3. Closes #3544.
120 lines
3.8 KiB
JavaScript
120 lines
3.8 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Generator for the Workstream Inventory Builder CJS artifact.
|
|
*
|
|
* Reads the compiled ESM output from sdk/dist/workstream-inventory/builder.js,
|
|
* extracts function source via Function.prototype.toString() for exports
|
|
* and via source-text extraction for internal helpers, then emits
|
|
* get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs.
|
|
*
|
|
* Run: cd sdk && npm run gen:workstream-inventory-builder
|
|
* Freshness check: node sdk/scripts/check-workstream-inventory-builder-fresh.mjs
|
|
*/
|
|
|
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
export const BANNER = `'use strict';
|
|
|
|
/**
|
|
* GENERATED FILE — DO NOT EDIT.
|
|
*
|
|
* Source: sdk/src/workstream-inventory/builder.ts
|
|
* Regenerate: cd sdk && npm run gen:workstream-inventory-builder
|
|
*
|
|
* Workstream Inventory Builder — pure projection from pre-collected
|
|
* filesystem data to typed WorkstreamInventory. No I/O. No async.
|
|
*/
|
|
|
|
`;
|
|
|
|
/**
|
|
* Extract a top-level function declaration (non-exported) from a JS source
|
|
* string by scanning for `function <name>(` and capturing the entire body
|
|
* including balanced braces.
|
|
*/
|
|
export function extractFunctionFromSource(source, name) {
|
|
const marker = `function ${name}(`;
|
|
const start = source.indexOf(marker);
|
|
if (start === -1) {
|
|
throw new Error(`Could not find function ${name} in compiled source`);
|
|
}
|
|
// Find the opening brace
|
|
const braceOpen = source.indexOf('{', start);
|
|
if (braceOpen === -1) {
|
|
throw new Error(`Could not find opening brace for function ${name}`);
|
|
}
|
|
// Walk forward counting braces until balanced
|
|
let depth = 0;
|
|
let i = braceOpen;
|
|
for (; i < source.length; i++) {
|
|
if (source[i] === '{') depth++;
|
|
else if (source[i] === '}') {
|
|
depth--;
|
|
if (depth === 0) break;
|
|
}
|
|
}
|
|
if (depth !== 0) {
|
|
throw new Error(`Could not find closing brace for function ${name}`);
|
|
}
|
|
// Return from `function name(` through the closing `}`
|
|
return source.slice(start, i + 1);
|
|
}
|
|
|
|
export async function buildWorkstreamInventoryBuilderCjs() {
|
|
// Load the compiled ESM module to get exports via Function.prototype.toString()
|
|
const distUrl = new URL('../dist/workstream-inventory/builder.js', import.meta.url);
|
|
const {
|
|
buildWorkstreamInventory,
|
|
isCompletedInventory,
|
|
} = await import(distUrl.href);
|
|
|
|
// Also read the compiled JS as text to extract non-exported helpers
|
|
const compiledSource = await readFile(fileURLToPath(distUrl), 'utf-8');
|
|
|
|
// Extract non-exported helpers from source text
|
|
const toPosixPathBody = extractFunctionFromSource(compiledSource, 'toPosixPath');
|
|
|
|
// Get exported function bodies via Function.prototype.toString()
|
|
const isCompletedInventoryBody = isCompletedInventory.toString();
|
|
const buildWorkstreamInventoryBody = buildWorkstreamInventory.toString();
|
|
|
|
const parts = [
|
|
BANNER.trimEnd(),
|
|
'',
|
|
"const path = require('path');",
|
|
'const relative = path.relative;',
|
|
'',
|
|
'// Internal helpers',
|
|
toPosixPathBody,
|
|
'',
|
|
isCompletedInventoryBody,
|
|
'',
|
|
buildWorkstreamInventoryBody,
|
|
'',
|
|
'module.exports = { buildWorkstreamInventory, isCompletedInventory };',
|
|
'',
|
|
];
|
|
|
|
return parts.join('\n');
|
|
}
|
|
|
|
async function main() {
|
|
const content = await buildWorkstreamInventoryBuilderCjs();
|
|
const outPath = fileURLToPath(
|
|
new URL('../../get-shit-done/bin/lib/workstream-inventory-builder.generated.cjs', import.meta.url),
|
|
);
|
|
await writeFile(outPath, content, 'utf-8');
|
|
console.log(`Written: ${outPath}`);
|
|
}
|
|
|
|
// Only run main() when this file is the entry point, not when imported.
|
|
const scriptPath = fileURLToPath(import.meta.url);
|
|
const entryPath = process.argv[1] ? new URL(process.argv[1], 'file://').pathname : '';
|
|
if (scriptPath === entryPath || process.argv[1] === scriptPath) {
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|