deepflow 0.1.113 → 0.1.114
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/ac-coverage.js +77 -7
- package/hooks/df-spec-lint.js +49 -18
- package/package.json +1 -1
package/hooks/ac-coverage.js
CHANGED
|
@@ -3,15 +3,21 @@
|
|
|
3
3
|
// @hook-owner: deepflow
|
|
4
4
|
/**
|
|
5
5
|
* deepflow AC coverage checker
|
|
6
|
-
* Standalone script called by the orchestrator after ratchet checks.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* Hook mode (PostToolUse — auto-triggered on git commit):
|
|
8
|
+
* Reads PostToolUse event from stdin. Fires only when tool_name is "Bash"
|
|
9
|
+
* and the command contains "git commit". Auto-detects the current spec from
|
|
10
|
+
* specs/doing-*.md in cwd and scans snapshot test files for AC references.
|
|
11
|
+
* Emits SALVAGEABLE (exit 2) when ACs in the spec have no corresponding
|
|
12
|
+
* test references.
|
|
13
|
+
*
|
|
14
|
+
* CLI mode (called explicitly by orchestrator):
|
|
9
15
|
* node ac-coverage.js --spec <path> --test-files <file1,file2,...> --status <pass|fail|revert>
|
|
10
16
|
* node ac-coverage.js --spec <path> --snapshot <path> --status <pass|fail|revert>
|
|
11
17
|
*
|
|
12
18
|
* Exit codes:
|
|
13
|
-
* 0 — all ACs covered, no ACs in spec, or
|
|
14
|
-
* 2 — SALVAGEABLE: missed ACs detected
|
|
19
|
+
* 0 — all ACs covered, no ACs in spec, or non-commit event, or status != pass
|
|
20
|
+
* 2 — SALVAGEABLE: missed ACs detected (hook mode or CLI pass status)
|
|
15
21
|
* 1 — script error only
|
|
16
22
|
*/
|
|
17
23
|
|
|
@@ -19,6 +25,7 @@
|
|
|
19
25
|
|
|
20
26
|
const fs = require('fs');
|
|
21
27
|
const path = require('path');
|
|
28
|
+
const { readStdinIfMain } = require('./lib/hook-stdin');
|
|
22
29
|
|
|
23
30
|
// ── Argument parsing ────────────────────────────────────────────────────────
|
|
24
31
|
|
|
@@ -209,11 +216,74 @@ function run(args) {
|
|
|
209
216
|
}
|
|
210
217
|
}
|
|
211
218
|
|
|
212
|
-
// ──
|
|
219
|
+
// ── Hook entry point (PostToolUse stdin) ────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
function runAsHook(data) {
|
|
222
|
+
const toolName = data.tool_name || '';
|
|
223
|
+
const command = (data.tool_input && data.tool_input.command) || '';
|
|
224
|
+
|
|
225
|
+
// Only fire on git commit bash events
|
|
226
|
+
if (toolName !== 'Bash' || !/git\s+commit\b/.test(command)) return;
|
|
227
|
+
|
|
228
|
+
const cwd = data.cwd || process.cwd();
|
|
229
|
+
|
|
230
|
+
// Auto-detect spec: first doing-*.md in specs/
|
|
231
|
+
let specPath;
|
|
232
|
+
try {
|
|
233
|
+
const specsDir = path.join(cwd, 'specs');
|
|
234
|
+
const doing = fs.readdirSync(specsDir).filter(f => f.startsWith('doing-') && f.endsWith('.md'));
|
|
235
|
+
if (doing.length === 0) return;
|
|
236
|
+
specPath = path.join(specsDir, doing[0]);
|
|
237
|
+
} catch (_) {
|
|
238
|
+
return; // no specs dir — not a deepflow project
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const specContent = fs.readFileSync(specPath, 'utf8');
|
|
242
|
+
const specACs = extractSpecACs(specContent);
|
|
243
|
+
if (specACs.length === 0) return;
|
|
244
|
+
|
|
245
|
+
// Auto-detect test files: prefer snapshot, fall back to git ls-files
|
|
246
|
+
let testFiles = [];
|
|
247
|
+
try {
|
|
248
|
+
const { execFileSync } = require('child_process');
|
|
249
|
+
const snapshotCandidates = [
|
|
250
|
+
path.join(cwd, '.deepflow', 'auto-snapshot.txt'),
|
|
251
|
+
...fs.readdirSync(path.join(cwd, '.deepflow')).filter(f => f.startsWith('auto-snapshot')).map(f => path.join(cwd, '.deepflow', f)),
|
|
252
|
+
];
|
|
253
|
+
let snapshotPath = snapshotCandidates.find(p => fs.existsSync(p));
|
|
254
|
+
if (snapshotPath) {
|
|
255
|
+
testFiles = fs.readFileSync(snapshotPath, 'utf8').split('\n').filter(Boolean).map(f => path.resolve(cwd, f));
|
|
256
|
+
} else {
|
|
257
|
+
const out = execFileSync('git', ['ls-files'], { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
258
|
+
testFiles = out.split('\n').filter(f => /\.(test|spec)\.[^/]+$|^test_|_test\.[^/]+$|^tests\/|__tests__\//.test(f)).map(f => path.join(cwd, f));
|
|
259
|
+
}
|
|
260
|
+
} catch (_) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
testFiles = testFiles.filter(f => { try { fs.accessSync(f); return true; } catch (_) { return false; } });
|
|
265
|
+
if (testFiles.length === 0) return;
|
|
266
|
+
|
|
267
|
+
const coveredACs = scanTestFilesForACs(testFiles);
|
|
268
|
+
const missed = specACs.filter(ac => !coveredACs.has(ac));
|
|
269
|
+
|
|
270
|
+
if (missed.length > 0) {
|
|
271
|
+
process.stderr.write(`[ac-coverage] SALVAGEABLE: ${specACs.length - missed.length}/${specACs.length} ACs covered in tests — missing: ${missed.join(', ')}\nOVERRIDE:SALVAGEABLE\n`);
|
|
272
|
+
process.exit(2);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// ── CLI entry point ──────────────────────────────────────────────────────────
|
|
213
277
|
|
|
214
278
|
if (require.main === module) {
|
|
215
|
-
|
|
216
|
-
|
|
279
|
+
if (process.argv.length > 2) {
|
|
280
|
+
// CLI mode: explicit --spec / --status args from orchestrator
|
|
281
|
+
const args = parseArgs(process.argv.slice(2));
|
|
282
|
+
run(args);
|
|
283
|
+
} else {
|
|
284
|
+
// Hook mode: read PostToolUse event from stdin
|
|
285
|
+
readStdinIfMain(module, runAsHook);
|
|
286
|
+
}
|
|
217
287
|
}
|
|
218
288
|
|
|
219
289
|
module.exports = { extractSpecACs, extractACSection, scanTestFilesForACs, resolveTestFiles };
|
package/hooks/df-spec-lint.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
const fs = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
|
+
const { readStdinIfMain } = require('./lib/hook-stdin');
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Parse YAML frontmatter from the top of a markdown file.
|
|
@@ -345,40 +346,70 @@ function extractSection(content, sectionName) {
|
|
|
345
346
|
return capturing ? captured.join('\n') : null;
|
|
346
347
|
}
|
|
347
348
|
|
|
348
|
-
// ──
|
|
349
|
-
|
|
350
|
-
const filePath = process.argv[2];
|
|
351
|
-
if (!filePath) {
|
|
352
|
-
// Called as a PostToolUse hook without a spec file argument — no-op
|
|
353
|
-
process.exit(0);
|
|
354
|
-
}
|
|
349
|
+
// ── Spec file pattern (Write/Edit hook trigger) ──────────────────────────────
|
|
350
|
+
const SPEC_FILE_RE = /(?:^|\/)specs\/.*\.md$|(?:^|\/)(?:doing|done)-[^/]+\.md$/;
|
|
355
351
|
|
|
356
|
-
|
|
357
|
-
|
|
352
|
+
function lintSpecFile(filePath) {
|
|
353
|
+
let content;
|
|
354
|
+
try {
|
|
355
|
+
content = fs.readFileSync(filePath, 'utf8');
|
|
356
|
+
} catch (_) {
|
|
357
|
+
return; // file unreadable — don't block
|
|
358
|
+
}
|
|
359
|
+
const mode = 'auto';
|
|
358
360
|
const specsDir = path.resolve(path.dirname(filePath));
|
|
359
361
|
const result = validateSpec(content, { mode, specsDir, filename: path.basename(filePath) });
|
|
360
362
|
|
|
361
363
|
if (result.hard.length > 0) {
|
|
362
|
-
console.error('HARD invariant failures:');
|
|
364
|
+
console.error('[spec-lint] HARD invariant failures:');
|
|
363
365
|
for (const msg of result.hard) {
|
|
364
366
|
console.error(` [FAIL] ${msg}`);
|
|
365
367
|
}
|
|
368
|
+
process.exit(1);
|
|
366
369
|
}
|
|
367
370
|
|
|
368
371
|
if (result.advisory.length > 0) {
|
|
369
|
-
console.warn('Advisory warnings:');
|
|
370
372
|
for (const msg of result.advisory) {
|
|
371
|
-
console.warn(`
|
|
373
|
+
console.warn(`[spec-lint] [WARN] ${msg}`);
|
|
372
374
|
}
|
|
373
375
|
}
|
|
376
|
+
}
|
|
374
377
|
|
|
375
|
-
|
|
376
|
-
|
|
378
|
+
// ── Entry points ─────────────────────────────────────────────────────────────
|
|
379
|
+
if (require.main === module) {
|
|
380
|
+
if (process.argv[2]) {
|
|
381
|
+
// CLI mode: node df-spec-lint.js <spec-file.md> [--auto]
|
|
382
|
+
const filePath = process.argv[2];
|
|
383
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
384
|
+
const mode = process.argv.includes('--auto') ? 'auto' : 'interactive';
|
|
385
|
+
const specsDir = path.resolve(path.dirname(filePath));
|
|
386
|
+
const result = validateSpec(content, { mode, specsDir, filename: path.basename(filePath) });
|
|
387
|
+
|
|
388
|
+
if (result.hard.length > 0) {
|
|
389
|
+
console.error('HARD invariant failures:');
|
|
390
|
+
for (const msg of result.hard) console.error(` [FAIL] ${msg}`);
|
|
391
|
+
}
|
|
392
|
+
if (result.advisory.length > 0) {
|
|
393
|
+
console.warn('Advisory warnings:');
|
|
394
|
+
for (const msg of result.advisory) console.warn(` [WARN] ${msg}`);
|
|
395
|
+
}
|
|
396
|
+
if (result.hard.length === 0 && result.advisory.length === 0) {
|
|
397
|
+
console.log('All checks passed.');
|
|
398
|
+
}
|
|
399
|
+
console.log(`Spec layer: L${result.layer} (${['problem defined', 'requirements known', 'verifiable', 'fully constrained'][result.layer] || 'incomplete'})`);
|
|
400
|
+
process.exit(result.hard.length > 0 ? 1 : 0);
|
|
401
|
+
} else {
|
|
402
|
+
// Hook mode: read PostToolUse event from stdin
|
|
403
|
+
readStdinIfMain(module, (data) => {
|
|
404
|
+
const toolName = data.tool_name || '';
|
|
405
|
+
if (toolName !== 'Write' && toolName !== 'Edit') return;
|
|
406
|
+
|
|
407
|
+
const filePath = (data.tool_input && data.tool_input.file_path) || '';
|
|
408
|
+
if (!SPEC_FILE_RE.test(filePath)) return;
|
|
409
|
+
|
|
410
|
+
lintSpecFile(filePath);
|
|
411
|
+
});
|
|
377
412
|
}
|
|
378
|
-
|
|
379
|
-
console.log(`Spec layer: L${result.layer} (${['problem defined', 'requirements known', 'verifiable', 'fully constrained'][result.layer] || 'incomplete'})`);
|
|
380
|
-
|
|
381
|
-
process.exit(result.hard.length > 0 ? 1 : 0);
|
|
382
413
|
}
|
|
383
414
|
|
|
384
415
|
module.exports = { validateSpec, extractSection, computeLayer, parseFrontmatter };
|