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.
- package/bin/install-dynamic-hooks.test.js +461 -0
- package/bin/install.js +171 -250
- package/bin/install.test.js +205 -0
- package/bin/lineage-ingest.js +70 -0
- package/hooks/df-check-update.js +1 -0
- package/hooks/df-command-usage.js +18 -0
- package/hooks/df-dashboard-push.js +5 -4
- package/hooks/df-dashboard-push.test.js +256 -0
- package/hooks/df-execution-history.js +1 -0
- package/hooks/df-explore-protocol.js +83 -0
- package/hooks/df-explore-protocol.test.js +228 -0
- package/hooks/df-hook-event-tags.test.js +127 -0
- package/hooks/df-invariant-check.js +4 -3
- package/hooks/df-invariant-check.test.js +141 -0
- package/hooks/df-quota-logger.js +12 -23
- package/hooks/df-quota-logger.test.js +324 -0
- package/hooks/df-snapshot-guard.js +1 -0
- package/hooks/df-spec-lint.js +58 -1
- package/hooks/df-spec-lint.test.js +412 -0
- package/hooks/df-statusline.js +1 -0
- package/hooks/df-subagent-registry.js +1 -0
- package/hooks/df-tool-usage.js +13 -3
- package/hooks/df-worktree-guard.js +1 -0
- package/package.json +1 -1
- package/src/commands/df/debate.md +1 -1
- package/src/commands/df/eval.md +117 -0
- package/src/commands/df/execute.md +1 -1
- package/src/commands/df/fix.md +104 -0
- package/src/eval/git-memory.js +159 -0
- package/src/eval/git-memory.test.js +439 -0
- package/src/eval/hypothesis.js +80 -0
- package/src/eval/hypothesis.test.js +169 -0
- package/src/eval/loop.js +378 -0
- package/src/eval/loop.test.js +306 -0
- package/src/eval/metric-collector.js +163 -0
- package/src/eval/metric-collector.test.js +369 -0
- package/src/eval/metric-pivot.js +119 -0
- package/src/eval/metric-pivot.test.js +350 -0
- package/src/eval/mutator-prompt.js +106 -0
- package/src/eval/mutator-prompt.test.js +180 -0
- package/templates/config-template.yaml +5 -6
- package/templates/eval-fixture-template/config.yaml +39 -0
- package/templates/eval-fixture-template/fixture/.deepflow/decisions.md +5 -0
- package/templates/eval-fixture-template/fixture/hooks/invariant.js +28 -0
- package/templates/eval-fixture-template/fixture/package.json +12 -0
- package/templates/eval-fixture-template/fixture/specs/doing-example-task.md +18 -0
- package/templates/eval-fixture-template/fixture/src/commands/df/example.md +18 -0
- package/templates/eval-fixture-template/fixture/src/config.js +40 -0
- package/templates/eval-fixture-template/fixture/src/index.js +19 -0
- package/templates/eval-fixture-template/fixture/src/pipeline.js +40 -0
- package/templates/eval-fixture-template/fixture/src/skills/example-skill/SKILL.md +32 -0
- package/templates/eval-fixture-template/fixture/src/spec-loader.js +35 -0
- package/templates/eval-fixture-template/fixture/src/task-runner.js +32 -0
- package/templates/eval-fixture-template/fixture/src/verifier.js +37 -0
- package/templates/eval-fixture-template/hypotheses.md +14 -0
- package/templates/eval-fixture-template/spec.md +34 -0
- package/templates/eval-fixture-template/tests/behavior.test.js +69 -0
- package/templates/eval-fixture-template/tests/guard.test.js +108 -0
- package/templates/eval-fixture-template.test.js +318 -0
- package/templates/explore-agent.md +5 -74
- package/templates/explore-protocol.md +44 -0
- 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
|
+
});
|
package/hooks/df-spec-lint.js
CHANGED
|
@@ -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 };
|