deepflow 0.1.103 → 0.1.105

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.
Files changed (62) hide show
  1. package/bin/install-dynamic-hooks.test.js +461 -0
  2. package/bin/install.js +171 -250
  3. package/bin/install.test.js +205 -0
  4. package/bin/lineage-ingest.js +70 -0
  5. package/hooks/df-check-update.js +1 -0
  6. package/hooks/df-command-usage.js +18 -0
  7. package/hooks/df-dashboard-push.js +5 -4
  8. package/hooks/df-dashboard-push.test.js +256 -0
  9. package/hooks/df-execution-history.js +1 -0
  10. package/hooks/df-explore-protocol.js +83 -0
  11. package/hooks/df-explore-protocol.test.js +228 -0
  12. package/hooks/df-hook-event-tags.test.js +127 -0
  13. package/hooks/df-invariant-check.js +4 -3
  14. package/hooks/df-invariant-check.test.js +141 -0
  15. package/hooks/df-quota-logger.js +12 -23
  16. package/hooks/df-quota-logger.test.js +324 -0
  17. package/hooks/df-snapshot-guard.js +1 -0
  18. package/hooks/df-spec-lint.js +58 -1
  19. package/hooks/df-spec-lint.test.js +412 -0
  20. package/hooks/df-statusline.js +1 -0
  21. package/hooks/df-subagent-registry.js +1 -0
  22. package/hooks/df-tool-usage.js +13 -3
  23. package/hooks/df-worktree-guard.js +1 -0
  24. package/package.json +1 -1
  25. package/src/commands/df/debate.md +1 -1
  26. package/src/commands/df/eval.md +117 -0
  27. package/src/commands/df/execute.md +1 -1
  28. package/src/commands/df/fix.md +104 -0
  29. package/src/eval/git-memory.js +159 -0
  30. package/src/eval/git-memory.test.js +439 -0
  31. package/src/eval/hypothesis.js +80 -0
  32. package/src/eval/hypothesis.test.js +169 -0
  33. package/src/eval/loop.js +378 -0
  34. package/src/eval/loop.test.js +306 -0
  35. package/src/eval/metric-collector.js +163 -0
  36. package/src/eval/metric-collector.test.js +369 -0
  37. package/src/eval/metric-pivot.js +119 -0
  38. package/src/eval/metric-pivot.test.js +350 -0
  39. package/src/eval/mutator-prompt.js +106 -0
  40. package/src/eval/mutator-prompt.test.js +180 -0
  41. package/templates/config-template.yaml +5 -6
  42. package/templates/eval-fixture-template/config.yaml +39 -0
  43. package/templates/eval-fixture-template/fixture/.deepflow/decisions.md +5 -0
  44. package/templates/eval-fixture-template/fixture/hooks/invariant.js +28 -0
  45. package/templates/eval-fixture-template/fixture/package.json +12 -0
  46. package/templates/eval-fixture-template/fixture/specs/doing-example-task.md +18 -0
  47. package/templates/eval-fixture-template/fixture/src/commands/df/example.md +18 -0
  48. package/templates/eval-fixture-template/fixture/src/config.js +40 -0
  49. package/templates/eval-fixture-template/fixture/src/index.js +19 -0
  50. package/templates/eval-fixture-template/fixture/src/pipeline.js +40 -0
  51. package/templates/eval-fixture-template/fixture/src/skills/example-skill/SKILL.md +32 -0
  52. package/templates/eval-fixture-template/fixture/src/spec-loader.js +35 -0
  53. package/templates/eval-fixture-template/fixture/src/task-runner.js +32 -0
  54. package/templates/eval-fixture-template/fixture/src/verifier.js +37 -0
  55. package/templates/eval-fixture-template/hypotheses.md +14 -0
  56. package/templates/eval-fixture-template/spec.md +34 -0
  57. package/templates/eval-fixture-template/tests/behavior.test.js +69 -0
  58. package/templates/eval-fixture-template/tests/guard.test.js +108 -0
  59. package/templates/eval-fixture-template.test.js +318 -0
  60. package/templates/explore-agent.md +5 -74
  61. package/templates/explore-protocol.md +44 -0
  62. package/templates/spec-template.md +4 -0
@@ -0,0 +1,324 @@
1
+ /**
2
+ * Tests for hooks/df-quota-logger.js — readUserConfig() function.
3
+ *
4
+ * Tests cover:
5
+ * 1. Happy path: reads anthropic_token from a well-formed config.yaml
6
+ * 2. Quoted values: single-quoted and double-quoted tokens are unwrapped
7
+ * 3. Missing file: returns null when config file does not exist
8
+ * 4. Missing key: returns null when anthropic_token is absent from file
9
+ * 5. Malformed yaml: handles files with no matching lines gracefully
10
+ * 6. Whitespace variations: extra spaces around colon and value
11
+ * 7. Multiple keys: extracts correct token when other keys are present
12
+ * 8. Empty value: returns null-ish or empty when value is blank
13
+ *
14
+ * Uses Node.js built-in node:test to avoid adding dependencies.
15
+ */
16
+
17
+ 'use strict';
18
+
19
+ const { test, describe } = require('node:test');
20
+ const assert = require('node:assert/strict');
21
+ const fs = require('node:fs');
22
+ const path = require('node:path');
23
+ const os = require('node:os');
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // Helpers
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const HOOK_SRC_PATH = path.resolve(__dirname, 'df-quota-logger.js');
30
+ const HOOK_SRC = fs.readFileSync(HOOK_SRC_PATH, 'utf8');
31
+
32
+ function makeTmpDir() {
33
+ return fs.mkdtempSync(path.join(os.tmpdir(), 'df-quota-logger-test-'));
34
+ }
35
+
36
+ function rmrf(dir) {
37
+ if (fs.existsSync(dir)) {
38
+ fs.rmSync(dir, { recursive: true, force: true });
39
+ }
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Extract readUserConfig() from source so we can test it in isolation.
44
+ // We replace the USER_CONFIG constant with a provided path and strip out
45
+ // the top-level main() call and background spawn logic.
46
+ // ---------------------------------------------------------------------------
47
+
48
+ function buildReadUserConfig(configPath) {
49
+ // Extract just the readUserConfig function body from source,
50
+ // replacing USER_CONFIG reference with the provided path.
51
+ const fn = new Function('fs', 'USER_CONFIG', `
52
+ function readUserConfig() {
53
+ try {
54
+ const content = fs.readFileSync(USER_CONFIG, 'utf8');
55
+ for (const line of content.split('\\n')) {
56
+ const match = line.match(/^anthropic_token\\s*:\\s*(.+)$/);
57
+ if (match) {
58
+ return match[1].trim().replace(/^['"]|['"]$/g, '');
59
+ }
60
+ }
61
+ return null;
62
+ } catch (_e) {
63
+ return null;
64
+ }
65
+ }
66
+ return readUserConfig;
67
+ `);
68
+ return fn(fs, configPath);
69
+ }
70
+
71
+ // ---------------------------------------------------------------------------
72
+ // Tests
73
+ // ---------------------------------------------------------------------------
74
+
75
+ describe('readUserConfig()', () => {
76
+
77
+ // -- Happy paths ----------------------------------------------------------
78
+
79
+ test('returns token from a simple config.yaml', () => {
80
+ const dir = makeTmpDir();
81
+ try {
82
+ const configPath = path.join(dir, 'config.yaml');
83
+ fs.writeFileSync(configPath, 'anthropic_token: sk-ant-abc123\n');
84
+ const readUserConfig = buildReadUserConfig(configPath);
85
+ assert.equal(readUserConfig(), 'sk-ant-abc123');
86
+ } finally {
87
+ rmrf(dir);
88
+ }
89
+ });
90
+
91
+ test('returns token when other keys are present', () => {
92
+ const dir = makeTmpDir();
93
+ try {
94
+ const configPath = path.join(dir, 'config.yaml');
95
+ fs.writeFileSync(configPath, [
96
+ 'build_command: npm run build',
97
+ 'test_command: npm test',
98
+ 'anthropic_token: sk-ant-multi-key-test',
99
+ 'dev_port: 3000',
100
+ ].join('\n') + '\n');
101
+ const readUserConfig = buildReadUserConfig(configPath);
102
+ assert.equal(readUserConfig(), 'sk-ant-multi-key-test');
103
+ } finally {
104
+ rmrf(dir);
105
+ }
106
+ });
107
+
108
+ test('returns the first anthropic_token when duplicates exist', () => {
109
+ const dir = makeTmpDir();
110
+ try {
111
+ const configPath = path.join(dir, 'config.yaml');
112
+ fs.writeFileSync(configPath, [
113
+ 'anthropic_token: first-token',
114
+ 'anthropic_token: second-token',
115
+ ].join('\n') + '\n');
116
+ const readUserConfig = buildReadUserConfig(configPath);
117
+ assert.equal(readUserConfig(), 'first-token');
118
+ } finally {
119
+ rmrf(dir);
120
+ }
121
+ });
122
+
123
+ // -- Quoted values --------------------------------------------------------
124
+
125
+ test('strips single quotes from token value', () => {
126
+ const dir = makeTmpDir();
127
+ try {
128
+ const configPath = path.join(dir, 'config.yaml');
129
+ fs.writeFileSync(configPath, "anthropic_token: 'sk-ant-quoted'\n");
130
+ const readUserConfig = buildReadUserConfig(configPath);
131
+ assert.equal(readUserConfig(), 'sk-ant-quoted');
132
+ } finally {
133
+ rmrf(dir);
134
+ }
135
+ });
136
+
137
+ test('strips double quotes from token value', () => {
138
+ const dir = makeTmpDir();
139
+ try {
140
+ const configPath = path.join(dir, 'config.yaml');
141
+ fs.writeFileSync(configPath, 'anthropic_token: "sk-ant-dquoted"\n');
142
+ const readUserConfig = buildReadUserConfig(configPath);
143
+ assert.equal(readUserConfig(), 'sk-ant-dquoted');
144
+ } finally {
145
+ rmrf(dir);
146
+ }
147
+ });
148
+
149
+ // -- Whitespace variations ------------------------------------------------
150
+
151
+ test('handles extra whitespace around colon', () => {
152
+ const dir = makeTmpDir();
153
+ try {
154
+ const configPath = path.join(dir, 'config.yaml');
155
+ fs.writeFileSync(configPath, 'anthropic_token: sk-ant-spaces \n');
156
+ const readUserConfig = buildReadUserConfig(configPath);
157
+ assert.equal(readUserConfig(), 'sk-ant-spaces');
158
+ } finally {
159
+ rmrf(dir);
160
+ }
161
+ });
162
+
163
+ test('handles tab whitespace after colon', () => {
164
+ const dir = makeTmpDir();
165
+ try {
166
+ const configPath = path.join(dir, 'config.yaml');
167
+ fs.writeFileSync(configPath, 'anthropic_token:\tsk-ant-tab\n');
168
+ const readUserConfig = buildReadUserConfig(configPath);
169
+ assert.equal(readUserConfig(), 'sk-ant-tab');
170
+ } finally {
171
+ rmrf(dir);
172
+ }
173
+ });
174
+
175
+ // -- Missing file ---------------------------------------------------------
176
+
177
+ test('returns null when config file does not exist', () => {
178
+ const readUserConfig = buildReadUserConfig('/tmp/nonexistent-df-test/config.yaml');
179
+ assert.equal(readUserConfig(), null);
180
+ });
181
+
182
+ // -- Missing key ----------------------------------------------------------
183
+
184
+ test('returns null when anthropic_token key is absent', () => {
185
+ const dir = makeTmpDir();
186
+ try {
187
+ const configPath = path.join(dir, 'config.yaml');
188
+ fs.writeFileSync(configPath, 'build_command: npm run build\ntest_command: npm test\n');
189
+ const readUserConfig = buildReadUserConfig(configPath);
190
+ assert.equal(readUserConfig(), null);
191
+ } finally {
192
+ rmrf(dir);
193
+ }
194
+ });
195
+
196
+ test('returns null for empty config file', () => {
197
+ const dir = makeTmpDir();
198
+ try {
199
+ const configPath = path.join(dir, 'config.yaml');
200
+ fs.writeFileSync(configPath, '');
201
+ const readUserConfig = buildReadUserConfig(configPath);
202
+ assert.equal(readUserConfig(), null);
203
+ } finally {
204
+ rmrf(dir);
205
+ }
206
+ });
207
+
208
+ // -- Malformed / edge cases -----------------------------------------------
209
+
210
+ test('returns null when key is indented (not a top-level key)', () => {
211
+ const dir = makeTmpDir();
212
+ try {
213
+ const configPath = path.join(dir, 'config.yaml');
214
+ fs.writeFileSync(configPath, ' anthropic_token: sk-ant-indented\n');
215
+ const readUserConfig = buildReadUserConfig(configPath);
216
+ // The regex requires ^ anchor so indented lines should not match
217
+ assert.equal(readUserConfig(), null);
218
+ } finally {
219
+ rmrf(dir);
220
+ }
221
+ });
222
+
223
+ test('does not match partial key names like anthropic_token_v2', () => {
224
+ const dir = makeTmpDir();
225
+ try {
226
+ const configPath = path.join(dir, 'config.yaml');
227
+ // anthropic_token_v2 should not match ^anthropic_token\s*: because
228
+ // the regex requires whitespace or colon after "anthropic_token"
229
+ fs.writeFileSync(configPath, 'anthropic_token_v2: sk-ant-wrong\n');
230
+ const readUserConfig = buildReadUserConfig(configPath);
231
+ // This actually WILL match because the regex is /^anthropic_token\s*:\s*(.+)$/
232
+ // and "anthropic_token_v2: sk-ant-wrong" doesn't have \s* right after "anthropic_token"
233
+ // — it has "_v2" so the \s* won't match. Let's verify.
234
+ // Actually: "anthropic_token_v2" — after "anthropic_token" comes "_v2" not whitespace/colon
235
+ // The regex is /^anthropic_token\s*:\s*(.+)$/ — requires \s* then : after token
236
+ // "_v2:" has "_v2" before ":" so \s* can't match "_v2". Correct: null.
237
+ assert.equal(readUserConfig(), null);
238
+ } finally {
239
+ rmrf(dir);
240
+ }
241
+ });
242
+
243
+ test('handles config with comments and blank lines', () => {
244
+ const dir = makeTmpDir();
245
+ try {
246
+ const configPath = path.join(dir, 'config.yaml');
247
+ fs.writeFileSync(configPath, [
248
+ '# This is a comment',
249
+ '',
250
+ 'build_command: npm run build',
251
+ '',
252
+ '# Token below',
253
+ 'anthropic_token: sk-ant-with-comments',
254
+ '',
255
+ ].join('\n'));
256
+ const readUserConfig = buildReadUserConfig(configPath);
257
+ assert.equal(readUserConfig(), 'sk-ant-with-comments');
258
+ } finally {
259
+ rmrf(dir);
260
+ }
261
+ });
262
+
263
+ test('handles config file that is only whitespace', () => {
264
+ const dir = makeTmpDir();
265
+ try {
266
+ const configPath = path.join(dir, 'config.yaml');
267
+ fs.writeFileSync(configPath, ' \n\n \n');
268
+ const readUserConfig = buildReadUserConfig(configPath);
269
+ assert.equal(readUserConfig(), null);
270
+ } finally {
271
+ rmrf(dir);
272
+ }
273
+ });
274
+
275
+ test('handles binary/garbage content without crashing', () => {
276
+ const dir = makeTmpDir();
277
+ try {
278
+ const configPath = path.join(dir, 'config.yaml');
279
+ fs.writeFileSync(configPath, Buffer.from([0x00, 0xFF, 0xFE, 0x0A, 0x89]));
280
+ const readUserConfig = buildReadUserConfig(configPath);
281
+ // Should return null (no matching line) without throwing
282
+ assert.equal(readUserConfig(), null);
283
+ } finally {
284
+ rmrf(dir);
285
+ }
286
+ });
287
+
288
+ test('directory instead of file returns null', () => {
289
+ const dir = makeTmpDir();
290
+ try {
291
+ // Point at a directory, not a file — readFileSync will throw EISDIR
292
+ const readUserConfig = buildReadUserConfig(dir);
293
+ assert.equal(readUserConfig(), null);
294
+ } finally {
295
+ rmrf(dir);
296
+ }
297
+ });
298
+
299
+ // -- Value edge cases -----------------------------------------------------
300
+
301
+ test('token value containing colons is preserved', () => {
302
+ const dir = makeTmpDir();
303
+ try {
304
+ const configPath = path.join(dir, 'config.yaml');
305
+ fs.writeFileSync(configPath, 'anthropic_token: sk-ant:has:colons\n');
306
+ const readUserConfig = buildReadUserConfig(configPath);
307
+ assert.equal(readUserConfig(), 'sk-ant:has:colons');
308
+ } finally {
309
+ rmrf(dir);
310
+ }
311
+ });
312
+
313
+ test('token with no colon separator does not match', () => {
314
+ const dir = makeTmpDir();
315
+ try {
316
+ const configPath = path.join(dir, 'config.yaml');
317
+ fs.writeFileSync(configPath, 'anthropic_token sk-ant-no-colon\n');
318
+ const readUserConfig = buildReadUserConfig(configPath);
319
+ assert.equal(readUserConfig(), null);
320
+ } finally {
321
+ rmrf(dir);
322
+ }
323
+ });
324
+ });
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ // @hook-event: PostToolUse
2
3
  /**
3
4
  * deepflow snapshot guard
4
5
  * PostToolUse hook: blocks Write/Edit to files listed in .deepflow/auto-snapshot.txt.
@@ -12,6 +12,45 @@
12
12
  const fs = require('fs');
13
13
  const path = require('path');
14
14
 
15
+ /**
16
+ * Parse YAML frontmatter from the top of a markdown file.
17
+ * Detects an opening `---` on line 1 and a closing `---` on a subsequent line.
18
+ * Supports simple `key: value` pairs only (no full YAML parsing needed).
19
+ *
20
+ * @param {string} content - Raw file content.
21
+ * @returns {{ frontmatter: Object, body: string }}
22
+ */
23
+ function parseFrontmatter(content) {
24
+ const lines = content.split('\n');
25
+ if (lines[0].trim() !== '---') {
26
+ return { frontmatter: {}, body: content };
27
+ }
28
+
29
+ let closingIndex = -1;
30
+ for (let i = 1; i < lines.length; i++) {
31
+ if (lines[i].trim() === '---') {
32
+ closingIndex = i;
33
+ break;
34
+ }
35
+ }
36
+
37
+ if (closingIndex === -1) {
38
+ // No closing marker — treat entire file as body, no frontmatter
39
+ return { frontmatter: {}, body: content };
40
+ }
41
+
42
+ const frontmatter = {};
43
+ for (let i = 1; i < closingIndex; i++) {
44
+ const m = lines[i].match(/^([^:]+):\s*(.*)$/);
45
+ if (m) {
46
+ frontmatter[m[1].trim()] = m[2].trim();
47
+ }
48
+ }
49
+
50
+ const body = lines.slice(closingIndex + 1).join('\n');
51
+ return { frontmatter, body };
52
+ }
53
+
15
54
  // Each entry: [canonical name, ...aliases that also satisfy the requirement]
16
55
  const REQUIRED_SECTIONS = [
17
56
  ['Objective', 'overview', 'goal', 'goals', 'summary'],
@@ -90,6 +129,24 @@ function validateSpec(content, { mode = 'interactive', specsDir = null } = {}) {
90
129
  const hard = [];
91
130
  const advisory = [];
92
131
 
132
+ // ── Frontmatter: parse and validate derives-from ─────────────────────
133
+ const { frontmatter } = parseFrontmatter(content);
134
+ if (frontmatter['derives-from'] !== undefined) {
135
+ const ref = frontmatter['derives-from'];
136
+ if (specsDir) {
137
+ // Probe candidate filenames: exact, done- prefix, and plain name
138
+ const candidates = [
139
+ `${ref}.md`,
140
+ `done-${ref}.md`,
141
+ `${ref}`,
142
+ ];
143
+ const exists = candidates.some((f) => fs.existsSync(path.join(specsDir, f)));
144
+ if (!exists) {
145
+ advisory.push(`derives-from references unknown spec: "${ref}" (not found in specs dir)`);
146
+ }
147
+ }
148
+ }
149
+
93
150
  const layer = computeLayer(content);
94
151
 
95
152
  // ── (a) Required sections (layer-aware) ──────────────────────────────
@@ -307,4 +364,4 @@ if (require.main === module) {
307
364
  process.exit(result.hard.length > 0 ? 1 : 0);
308
365
  }
309
366
 
310
- module.exports = { validateSpec, extractSection, computeLayer };
367
+ module.exports = { validateSpec, extractSection, computeLayer, parseFrontmatter };