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.
@@ -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
- * Usage:
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 input status was fail/revert
14
- * 2 — SALVAGEABLE: missed ACs detected and input status was pass
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
- // ── Entry point ─────────────────────────────────────────────────────────────
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
- const args = parseArgs(process.argv.slice(2));
216
- run(args);
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 };
@@ -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
- // ── CLI entry point ──────────────────────────────────────────────────────
349
- if (require.main === module) {
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
- const content = fs.readFileSync(filePath, 'utf8');
357
- const mode = process.argv.includes('--auto') ? 'auto' : 'interactive';
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(` [WARN] ${msg}`);
373
+ console.warn(`[spec-lint] [WARN] ${msg}`);
372
374
  }
373
375
  }
376
+ }
374
377
 
375
- if (result.hard.length === 0 && result.advisory.length === 0) {
376
- console.log('All checks passed.');
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepflow",
3
- "version": "0.1.113",
3
+ "version": "0.1.114",
4
4
  "description": "Doing reveals what thinking can't predict — spec-driven iterative development for Claude Code",
5
5
  "keywords": [
6
6
  "claude",