@joshski/dust 0.1.97 → 0.1.98
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/dist/agent-events.d.ts +13 -0
- package/dist/artifacts/index.d.ts +13 -2
- package/dist/artifacts/parsed-artifact.d.ts +32 -0
- package/dist/artifacts.js +159 -0
- package/dist/bucket/repository.d.ts +2 -0
- package/dist/cli/commands/focus.d.ts +1 -1
- package/dist/cli/process-runner.d.ts +35 -0
- package/dist/dust.js +622 -450
- package/dist/lint/validators/content-validator.d.ts +5 -4
- package/dist/lint/validators/filename-validator.d.ts +2 -1
- package/dist/lint/validators/idea-validator.d.ts +4 -3
- package/dist/lint/validators/link-validator.d.ts +4 -3
- package/dist/lint/validators/principle-hierarchy.d.ts +3 -2
- package/dist/loop/events.d.ts +18 -1
- package/dist/loop/iteration.d.ts +4 -1
- package/dist/patch/fact.d.ts +15 -0
- package/dist/patch/idea.d.ts +23 -0
- package/dist/patch/index.d.ts +54 -0
- package/dist/patch/principle.d.ts +17 -0
- package/dist/patch/task.d.ts +25 -0
- package/dist/patch.js +1632 -0
- package/dist/validation/validation-pipeline.d.ts +42 -0
- package/dist/validation.js +427 -397
- package/package.json +5 -1
package/dist/validation.js
CHANGED
|
@@ -1,27 +1,177 @@
|
|
|
1
1
|
// lib/validation/index.ts
|
|
2
2
|
import { relative } from "node:path";
|
|
3
3
|
|
|
4
|
-
// lib/
|
|
5
|
-
function
|
|
6
|
-
const
|
|
7
|
-
|
|
4
|
+
// lib/validation/overlay-filesystem.ts
|
|
5
|
+
function createOverlayFileSystem(base, patchFiles, deletedPaths = new Set) {
|
|
6
|
+
const patchDirs = new Set;
|
|
7
|
+
for (const path of patchFiles.keys()) {
|
|
8
|
+
let dir = path;
|
|
9
|
+
while (dir.includes("/")) {
|
|
10
|
+
dir = dir.substring(0, dir.lastIndexOf("/"));
|
|
11
|
+
if (dir)
|
|
12
|
+
patchDirs.add(dir);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function isDeleted(path) {
|
|
16
|
+
return deletedPaths.has(path);
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
exists(path) {
|
|
20
|
+
if (isDeleted(path))
|
|
21
|
+
return false;
|
|
22
|
+
return patchFiles.has(path) || patchDirs.has(path) || base.exists(path);
|
|
23
|
+
},
|
|
24
|
+
async readFile(path) {
|
|
25
|
+
if (isDeleted(path)) {
|
|
26
|
+
const error = new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
27
|
+
error.code = "ENOENT";
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
const patchContent = patchFiles.get(path);
|
|
31
|
+
if (patchContent !== undefined) {
|
|
32
|
+
return patchContent;
|
|
33
|
+
}
|
|
34
|
+
return base.readFile(path);
|
|
35
|
+
},
|
|
36
|
+
async readdir(path) {
|
|
37
|
+
const prefix = `${path}/`;
|
|
38
|
+
const entries = new Set;
|
|
39
|
+
for (const patchPath of patchFiles.keys()) {
|
|
40
|
+
if (patchPath.startsWith(prefix)) {
|
|
41
|
+
const relative = patchPath.slice(prefix.length);
|
|
42
|
+
const firstSegment = relative.split("/")[0];
|
|
43
|
+
entries.add(firstSegment);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
const baseEntries = await base.readdir(path);
|
|
48
|
+
for (const entry of baseEntries) {
|
|
49
|
+
const entryPath = `${path}/${entry}`;
|
|
50
|
+
if (!isDeleted(entryPath)) {
|
|
51
|
+
entries.add(entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
if (error.code !== "ENOENT") {
|
|
56
|
+
throw error;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return Array.from(entries);
|
|
60
|
+
},
|
|
61
|
+
isDirectory(path) {
|
|
62
|
+
if (isDeleted(path))
|
|
63
|
+
return false;
|
|
64
|
+
return patchDirs.has(path) || base.isDirectory(path);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
8
67
|
}
|
|
9
|
-
|
|
10
|
-
|
|
68
|
+
|
|
69
|
+
// lib/artifacts/parsed-artifact.ts
|
|
70
|
+
var MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
71
|
+
function parseArtifact(filePath, content) {
|
|
72
|
+
const lines = content.split(`
|
|
73
|
+
`);
|
|
74
|
+
let title = null;
|
|
75
|
+
let titleLine = null;
|
|
76
|
+
let openingSentence = null;
|
|
77
|
+
let openingSentenceLine = null;
|
|
78
|
+
const sections = [];
|
|
79
|
+
const allLinks = [];
|
|
80
|
+
let currentSection = null;
|
|
81
|
+
let inCodeFence = false;
|
|
82
|
+
let openingSentenceResolved = false;
|
|
11
83
|
for (let i = 0;i < lines.length; i++) {
|
|
12
|
-
|
|
13
|
-
|
|
84
|
+
const line = lines[i];
|
|
85
|
+
const lineNumber = i + 1;
|
|
86
|
+
if (line.startsWith("```")) {
|
|
87
|
+
inCodeFence = !inCodeFence;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (inCodeFence) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const h1Match = line.match(/^#\s+(.+)$/);
|
|
94
|
+
if (h1Match) {
|
|
95
|
+
if (title === null) {
|
|
96
|
+
title = h1Match[1].trim();
|
|
97
|
+
titleLine = lineNumber;
|
|
98
|
+
} else {
|
|
99
|
+
if (currentSection !== null) {
|
|
100
|
+
currentSection.endLine = findLastNonEmptyLine(lines, currentSection.startLine, lineNumber - 2);
|
|
101
|
+
sections.push(currentSection);
|
|
102
|
+
currentSection = null;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const headingMatch = line.match(/^(#{2,6})\s+(.+)$/);
|
|
108
|
+
if (headingMatch) {
|
|
109
|
+
openingSentenceResolved = true;
|
|
110
|
+
if (currentSection !== null) {
|
|
111
|
+
currentSection.endLine = findLastNonEmptyLine(lines, currentSection.startLine, lineNumber - 2);
|
|
112
|
+
sections.push(currentSection);
|
|
113
|
+
}
|
|
114
|
+
currentSection = {
|
|
115
|
+
heading: headingMatch[2].trim(),
|
|
116
|
+
level: headingMatch[1].length,
|
|
117
|
+
startLine: lineNumber,
|
|
118
|
+
endLine: -1,
|
|
119
|
+
links: []
|
|
120
|
+
};
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (shouldCheckForOpeningSentence(title, openingSentenceResolved, line)) {
|
|
124
|
+
const result = tryExtractOpeningSentence(lines, i);
|
|
125
|
+
openingSentence = result.sentence;
|
|
126
|
+
openingSentenceLine = result.sentence !== null ? lineNumber : null;
|
|
127
|
+
openingSentenceResolved = true;
|
|
128
|
+
}
|
|
129
|
+
const linkMatches = line.matchAll(MARKDOWN_LINK_PATTERN);
|
|
130
|
+
for (const match of linkMatches) {
|
|
131
|
+
const link = {
|
|
132
|
+
text: match[1],
|
|
133
|
+
target: match[2],
|
|
134
|
+
line: lineNumber
|
|
135
|
+
};
|
|
136
|
+
allLinks.push(link);
|
|
137
|
+
if (currentSection !== null) {
|
|
138
|
+
currentSection.links.push(link);
|
|
139
|
+
}
|
|
14
140
|
}
|
|
15
141
|
}
|
|
16
|
-
|
|
142
|
+
if (currentSection !== null) {
|
|
143
|
+
currentSection.endLine = findLastNonEmptyLine(lines, currentSection.startLine, lines.length - 1);
|
|
144
|
+
sections.push(currentSection);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
filePath,
|
|
148
|
+
rawContent: content,
|
|
149
|
+
title,
|
|
150
|
+
titleLine,
|
|
151
|
+
openingSentence,
|
|
152
|
+
openingSentenceLine,
|
|
153
|
+
sections,
|
|
154
|
+
allLinks
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function shouldCheckForOpeningSentence(title, resolved, line) {
|
|
158
|
+
if (title === null || resolved) {
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
const trimmed = line.trim();
|
|
162
|
+
return trimmed !== "" && !isStructuralElement(trimmed);
|
|
163
|
+
}
|
|
164
|
+
function tryExtractOpeningSentence(lines, startIndex) {
|
|
165
|
+
const paragraph = collectParagraph(lines, startIndex);
|
|
166
|
+
return { sentence: extractFirstSentence(paragraph) };
|
|
17
167
|
}
|
|
18
|
-
function
|
|
19
|
-
for (let i =
|
|
168
|
+
function findLastNonEmptyLine(lines, contentStartIndex, fromIndex) {
|
|
169
|
+
for (let i = fromIndex;i >= contentStartIndex; i--) {
|
|
20
170
|
if (lines[i].trim() !== "") {
|
|
21
|
-
return i;
|
|
171
|
+
return i + 1;
|
|
22
172
|
}
|
|
23
173
|
}
|
|
24
|
-
return
|
|
174
|
+
return contentStartIndex;
|
|
25
175
|
}
|
|
26
176
|
var LIST_ITEM_PREFIXES = ["-", "*", "+"];
|
|
27
177
|
var STRUCTURAL_PREFIXES = ["#", "```", ">"];
|
|
@@ -52,27 +202,9 @@ function extractFirstSentence(paragraph) {
|
|
|
52
202
|
const match = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
|
|
53
203
|
return match ? match[1] : null;
|
|
54
204
|
}
|
|
55
|
-
function extractOpeningSentence(content) {
|
|
56
|
-
const lines = content.split(`
|
|
57
|
-
`);
|
|
58
|
-
const h1Index = findH1Index(lines);
|
|
59
|
-
if (h1Index === -1) {
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
const paragraphStart = findFirstNonBlankLineAfter(lines, h1Index);
|
|
63
|
-
if (paragraphStart === -1) {
|
|
64
|
-
return null;
|
|
65
|
-
}
|
|
66
|
-
const trimmedFirstLine = lines[paragraphStart].trim();
|
|
67
|
-
if (isStructuralElement(trimmedFirstLine)) {
|
|
68
|
-
return null;
|
|
69
|
-
}
|
|
70
|
-
const paragraph = collectParagraph(lines, paragraphStart);
|
|
71
|
-
return extractFirstSentence(paragraph);
|
|
72
|
-
}
|
|
73
205
|
|
|
74
206
|
// lib/lint/validators/content-validator.ts
|
|
75
|
-
var
|
|
207
|
+
var REQUIRED_TASK_HEADINGS = ["Blocked By", "Definition of Done"];
|
|
76
208
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
77
209
|
var NON_IMPERATIVE_STARTERS = new Set([
|
|
78
210
|
"the",
|
|
@@ -88,31 +220,32 @@ var NON_IMPERATIVE_STARTERS = new Set([
|
|
|
88
220
|
"you",
|
|
89
221
|
"i"
|
|
90
222
|
]);
|
|
91
|
-
function validateOpeningSentence(
|
|
92
|
-
|
|
93
|
-
if (!openingSentence) {
|
|
223
|
+
function validateOpeningSentence(artifact) {
|
|
224
|
+
if (!artifact.openingSentence) {
|
|
94
225
|
return {
|
|
95
|
-
file: filePath,
|
|
226
|
+
file: artifact.filePath,
|
|
227
|
+
line: artifact.titleLine ?? undefined,
|
|
96
228
|
message: "Missing or malformed opening sentence after H1 heading"
|
|
97
229
|
};
|
|
98
230
|
}
|
|
99
231
|
return null;
|
|
100
232
|
}
|
|
101
|
-
function validateOpeningSentenceLength(
|
|
102
|
-
const openingSentence =
|
|
233
|
+
function validateOpeningSentenceLength(artifact) {
|
|
234
|
+
const openingSentence = artifact.openingSentence;
|
|
103
235
|
if (!openingSentence) {
|
|
104
236
|
return null;
|
|
105
237
|
}
|
|
106
238
|
if (openingSentence.length > MAX_OPENING_SENTENCE_LENGTH) {
|
|
107
239
|
return {
|
|
108
|
-
file: filePath,
|
|
240
|
+
file: artifact.filePath,
|
|
241
|
+
line: artifact.openingSentenceLine ?? undefined,
|
|
109
242
|
message: `Opening sentence is ${openingSentence.length} characters (max ${MAX_OPENING_SENTENCE_LENGTH}). Split into multiple sentences; only the first sentence is checked.`
|
|
110
243
|
};
|
|
111
244
|
}
|
|
112
245
|
return null;
|
|
113
246
|
}
|
|
114
|
-
function validateImperativeOpeningSentence(
|
|
115
|
-
const openingSentence =
|
|
247
|
+
function validateImperativeOpeningSentence(artifact) {
|
|
248
|
+
const openingSentence = artifact.openingSentence;
|
|
116
249
|
if (!openingSentence) {
|
|
117
250
|
return null;
|
|
118
251
|
}
|
|
@@ -121,19 +254,21 @@ function validateImperativeOpeningSentence(filePath, content) {
|
|
|
121
254
|
if (NON_IMPERATIVE_STARTERS.has(lower) || lower.endsWith("ing")) {
|
|
122
255
|
const preview = openingSentence.length > 40 ? `${openingSentence.slice(0, 40)}...` : openingSentence;
|
|
123
256
|
return {
|
|
124
|
-
file: filePath,
|
|
257
|
+
file: artifact.filePath,
|
|
258
|
+
line: artifact.openingSentenceLine ?? undefined,
|
|
125
259
|
message: `Opening sentence should use imperative form (e.g., "Add X" not "This adds X"). Found: "${preview}"`
|
|
126
260
|
};
|
|
127
261
|
}
|
|
128
262
|
return null;
|
|
129
263
|
}
|
|
130
|
-
function validateTaskHeadings(
|
|
264
|
+
function validateTaskHeadings(artifact) {
|
|
131
265
|
const violations = [];
|
|
132
|
-
|
|
133
|
-
|
|
266
|
+
const sectionHeadings = new Set(artifact.sections.map((s) => s.heading));
|
|
267
|
+
for (const heading of REQUIRED_TASK_HEADINGS) {
|
|
268
|
+
if (!sectionHeadings.has(heading)) {
|
|
134
269
|
violations.push({
|
|
135
|
-
file: filePath,
|
|
136
|
-
message: `Missing required heading: "${heading}"`
|
|
270
|
+
file: artifact.filePath,
|
|
271
|
+
message: `Missing required heading: "## ${heading}"`
|
|
137
272
|
});
|
|
138
273
|
}
|
|
139
274
|
}
|
|
@@ -202,17 +337,17 @@ function validateFilename(filePath) {
|
|
|
202
337
|
}
|
|
203
338
|
return null;
|
|
204
339
|
}
|
|
205
|
-
function validateTitleFilenameMatch(
|
|
206
|
-
const title =
|
|
340
|
+
function validateTitleFilenameMatch(artifact) {
|
|
341
|
+
const title = artifact.title;
|
|
207
342
|
if (!title) {
|
|
208
343
|
return null;
|
|
209
344
|
}
|
|
210
|
-
const parts = filePath.split("/");
|
|
345
|
+
const parts = artifact.filePath.split("/");
|
|
211
346
|
const actualFilename = parts[parts.length - 1];
|
|
212
347
|
const expectedFilename = titleToFilename(title);
|
|
213
348
|
if (actualFilename !== expectedFilename) {
|
|
214
349
|
return {
|
|
215
|
-
file: filePath,
|
|
350
|
+
file: artifact.filePath,
|
|
216
351
|
message: `Filename "${actualFilename}" does not match title "${title}" (expected "${expectedFilename}")`
|
|
217
352
|
};
|
|
218
353
|
}
|
|
@@ -252,10 +387,11 @@ function validateH2Heading(filePath, line, lineNumber, inOpenQuestions, currentQ
|
|
|
252
387
|
}
|
|
253
388
|
return violations;
|
|
254
389
|
}
|
|
255
|
-
function validateIdeaOpenQuestions(
|
|
390
|
+
function validateIdeaOpenQuestions(artifact) {
|
|
256
391
|
const violations = [];
|
|
257
|
-
const lines =
|
|
392
|
+
const lines = artifact.rawContent.split(`
|
|
258
393
|
`);
|
|
394
|
+
const filePath = artifact.filePath;
|
|
259
395
|
const topLevelStructureMessage = "Open Questions must use `### Question?` headings and `#### Option` headings at the top level. Put supporting markdown (including lists and code blocks) under an option heading. Run `dust new idea` to see the expected format.";
|
|
260
396
|
let inOpenQuestions = false;
|
|
261
397
|
let currentQuestionLine = null;
|
|
@@ -331,8 +467,8 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
331
467
|
}
|
|
332
468
|
return violations;
|
|
333
469
|
}
|
|
334
|
-
function validateIdeaTransitionTitle(
|
|
335
|
-
const title =
|
|
470
|
+
function validateIdeaTransitionTitle(artifact, ideasPath, fileSystem) {
|
|
471
|
+
const title = artifact.title;
|
|
336
472
|
if (!title) {
|
|
337
473
|
return null;
|
|
338
474
|
}
|
|
@@ -342,7 +478,7 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
|
342
478
|
const ideaFilename = titleToFilename(ideaTitle);
|
|
343
479
|
if (!fileSystem.exists(`${ideasPath}/${ideaFilename}`)) {
|
|
344
480
|
return {
|
|
345
|
-
file: filePath,
|
|
481
|
+
file: artifact.filePath,
|
|
346
482
|
message: `Idea transition task references non-existent idea: "${ideaTitle}" (expected file "${ideaFilename}" in ideas/)`
|
|
347
483
|
};
|
|
348
484
|
}
|
|
@@ -351,37 +487,9 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
|
351
487
|
}
|
|
352
488
|
return null;
|
|
353
489
|
}
|
|
354
|
-
function
|
|
355
|
-
const lines = content.split(`
|
|
356
|
-
`);
|
|
357
|
-
let inSection = false;
|
|
358
|
-
let sectionContent = "";
|
|
359
|
-
let startLine = 0;
|
|
360
|
-
for (let i = 0;i < lines.length; i++) {
|
|
361
|
-
const line = lines[i];
|
|
362
|
-
if (line.startsWith("## ")) {
|
|
363
|
-
if (inSection)
|
|
364
|
-
break;
|
|
365
|
-
if (line.trimEnd() === `## ${sectionHeading}`) {
|
|
366
|
-
inSection = true;
|
|
367
|
-
startLine = i + 1;
|
|
368
|
-
}
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (line.startsWith("# ") && inSection)
|
|
372
|
-
break;
|
|
373
|
-
if (inSection) {
|
|
374
|
-
sectionContent += `${line}
|
|
375
|
-
`;
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (!inSection)
|
|
379
|
-
return null;
|
|
380
|
-
return { content: sectionContent, startLine };
|
|
381
|
-
}
|
|
382
|
-
function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSystem) {
|
|
490
|
+
function validateWorkflowTaskBodySection(artifact, ideasPath, fileSystem) {
|
|
383
491
|
const violations = [];
|
|
384
|
-
const title =
|
|
492
|
+
const title = artifact.title;
|
|
385
493
|
if (!title)
|
|
386
494
|
return violations;
|
|
387
495
|
let matchedPrefix = null;
|
|
@@ -394,10 +502,10 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
394
502
|
if (!matchedPrefix)
|
|
395
503
|
return violations;
|
|
396
504
|
const expectedHeading = WORKFLOW_PREFIX_TO_SECTION[matchedPrefix];
|
|
397
|
-
const section =
|
|
505
|
+
const section = artifact.sections.find((s) => s.heading === expectedHeading && s.level === 2);
|
|
398
506
|
if (!section) {
|
|
399
507
|
violations.push({
|
|
400
|
-
file: filePath,
|
|
508
|
+
file: artifact.filePath,
|
|
401
509
|
message: `Workflow task with "${matchedPrefix.trim()}" prefix is missing required "## ${expectedHeading}" section. Add a section with a link to the idea file, e.g.:
|
|
402
510
|
|
|
403
511
|
## ${expectedHeading}
|
|
@@ -406,25 +514,9 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
406
514
|
});
|
|
407
515
|
return violations;
|
|
408
516
|
}
|
|
409
|
-
|
|
410
|
-
const links = [];
|
|
411
|
-
const sectionLines = section.content.split(`
|
|
412
|
-
`);
|
|
413
|
-
for (let i = 0;i < sectionLines.length; i++) {
|
|
414
|
-
const line = sectionLines[i];
|
|
415
|
-
let match = linkRegex.exec(line);
|
|
416
|
-
while (match !== null) {
|
|
417
|
-
links.push({
|
|
418
|
-
text: match[1],
|
|
419
|
-
target: match[2],
|
|
420
|
-
line: section.startLine + i + 1
|
|
421
|
-
});
|
|
422
|
-
match = linkRegex.exec(line);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
if (links.length === 0) {
|
|
517
|
+
if (section.links.length === 0) {
|
|
426
518
|
violations.push({
|
|
427
|
-
file: filePath,
|
|
519
|
+
file: artifact.filePath,
|
|
428
520
|
message: `"## ${expectedHeading}" section contains no link. Add a markdown link to the idea file, e.g.:
|
|
429
521
|
|
|
430
522
|
- [Idea Title](../ideas/idea-slug.md)`,
|
|
@@ -432,10 +524,10 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
432
524
|
});
|
|
433
525
|
return violations;
|
|
434
526
|
}
|
|
435
|
-
const ideaLinks = links.filter((l) => l.target.includes("/ideas/") || l.target.startsWith("../ideas/"));
|
|
527
|
+
const ideaLinks = section.links.filter((l) => l.target.includes("/ideas/") || l.target.startsWith("../ideas/"));
|
|
436
528
|
if (ideaLinks.length === 0) {
|
|
437
529
|
violations.push({
|
|
438
|
-
file: filePath,
|
|
530
|
+
file: artifact.filePath,
|
|
439
531
|
message: `"## ${expectedHeading}" section contains no link to an idea file. Links must point to a file in ../ideas/, e.g.:
|
|
440
532
|
|
|
441
533
|
- [Idea Title](../ideas/idea-slug.md)`,
|
|
@@ -451,7 +543,7 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
451
543
|
const ideaFilePath = `${ideasPath}/${ideaSlug}.md`;
|
|
452
544
|
if (!fileSystem.exists(ideaFilePath)) {
|
|
453
545
|
violations.push({
|
|
454
|
-
file: filePath,
|
|
546
|
+
file: artifact.filePath,
|
|
455
547
|
message: `Link to idea "${link.text}" points to non-existent file: ${ideaSlug}.md. Either create the idea file at ideas/${ideaSlug}.md or update the link to point to an existing idea.`,
|
|
456
548
|
line: link.line
|
|
457
549
|
});
|
|
@@ -464,153 +556,129 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
464
556
|
import { dirname, resolve } from "node:path";
|
|
465
557
|
var SEMANTIC_RULES = [
|
|
466
558
|
{
|
|
467
|
-
|
|
559
|
+
sectionHeading: "Principles",
|
|
468
560
|
requiredPath: "/.dust/principles/",
|
|
469
561
|
description: "principle"
|
|
470
562
|
},
|
|
471
563
|
{
|
|
472
|
-
|
|
564
|
+
sectionHeading: "Blocked By",
|
|
473
565
|
requiredPath: "/.dust/tasks/",
|
|
474
566
|
description: "task"
|
|
475
567
|
}
|
|
476
568
|
];
|
|
477
|
-
function
|
|
569
|
+
function isExternalOrAnchorLink(target) {
|
|
570
|
+
return target.startsWith("http://") || target.startsWith("https://") || target.startsWith("#");
|
|
571
|
+
}
|
|
572
|
+
function isAnchorLink(target) {
|
|
573
|
+
return target.startsWith("#");
|
|
574
|
+
}
|
|
575
|
+
function isExternalLink(target) {
|
|
576
|
+
return target.startsWith("http://") || target.startsWith("https://");
|
|
577
|
+
}
|
|
578
|
+
function validateLinks(artifact, fileSystem) {
|
|
478
579
|
const violations = [];
|
|
479
|
-
const
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
502
|
-
const targetPath = linkTarget.split("#")[0];
|
|
503
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
504
|
-
if (!fileSystem.exists(resolvedPath)) {
|
|
505
|
-
violations.push({
|
|
506
|
-
file: filePath,
|
|
507
|
-
message: `Broken link: "${linkTarget}"`,
|
|
508
|
-
line: i + 1
|
|
509
|
-
});
|
|
510
|
-
}
|
|
511
|
-
match = linkPattern.exec(line);
|
|
580
|
+
const fileDir = dirname(artifact.filePath);
|
|
581
|
+
for (const link of artifact.allLinks) {
|
|
582
|
+
if (isExternalOrAnchorLink(link.target)) {
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
if (link.target.startsWith("/")) {
|
|
586
|
+
violations.push({
|
|
587
|
+
file: artifact.filePath,
|
|
588
|
+
message: `Absolute link not allowed: "${link.target}" (use a relative path instead)`,
|
|
589
|
+
line: link.line
|
|
590
|
+
});
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
const targetPath = link.target.split("#")[0];
|
|
594
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
595
|
+
if (!fileSystem.exists(resolvedPath)) {
|
|
596
|
+
violations.push({
|
|
597
|
+
file: artifact.filePath,
|
|
598
|
+
message: `Broken link: "${link.target}"`,
|
|
599
|
+
line: link.line
|
|
600
|
+
});
|
|
512
601
|
}
|
|
513
602
|
}
|
|
514
603
|
return violations;
|
|
515
604
|
}
|
|
516
|
-
function
|
|
605
|
+
function validateSectionLink(artifact, link, rule) {
|
|
606
|
+
const sectionLabel = `## ${rule.sectionHeading}`;
|
|
607
|
+
if (isAnchorLink(link.target)) {
|
|
608
|
+
return {
|
|
609
|
+
file: artifact.filePath,
|
|
610
|
+
message: `Link in "${sectionLabel}" must point to a ${rule.description} file, not an anchor: "${link.target}"`,
|
|
611
|
+
line: link.line
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
if (isExternalLink(link.target)) {
|
|
615
|
+
return {
|
|
616
|
+
file: artifact.filePath,
|
|
617
|
+
message: `Link in "${sectionLabel}" must point to a ${rule.description} file, not an external URL: "${link.target}"`,
|
|
618
|
+
line: link.line
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
const fileDir = dirname(artifact.filePath);
|
|
622
|
+
const targetPath = link.target.split("#")[0];
|
|
623
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
624
|
+
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
625
|
+
return {
|
|
626
|
+
file: artifact.filePath,
|
|
627
|
+
message: `Link in "${sectionLabel}" must point to a ${rule.description} file: "${link.target}"`,
|
|
628
|
+
line: link.line
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
return null;
|
|
632
|
+
}
|
|
633
|
+
function validateSemanticLinks(artifact) {
|
|
517
634
|
const violations = [];
|
|
518
|
-
const
|
|
519
|
-
|
|
520
|
-
const fileDir = dirname(filePath);
|
|
521
|
-
let currentSection = null;
|
|
522
|
-
for (let i = 0;i < lines.length; i++) {
|
|
523
|
-
const line = lines[i];
|
|
524
|
-
if (line.startsWith("## ")) {
|
|
525
|
-
currentSection = line;
|
|
526
|
-
continue;
|
|
527
|
-
}
|
|
528
|
-
const rule = SEMANTIC_RULES.find((r) => r.section === currentSection);
|
|
635
|
+
for (const section of artifact.sections) {
|
|
636
|
+
const rule = SEMANTIC_RULES.find((r) => r.sectionHeading === section.heading);
|
|
529
637
|
if (!rule)
|
|
530
638
|
continue;
|
|
531
|
-
const
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
if (linkTarget.startsWith("#")) {
|
|
536
|
-
violations.push({
|
|
537
|
-
file: filePath,
|
|
538
|
-
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an anchor: "${linkTarget}"`,
|
|
539
|
-
line: i + 1
|
|
540
|
-
});
|
|
541
|
-
match = linkPattern.exec(line);
|
|
542
|
-
continue;
|
|
543
|
-
}
|
|
544
|
-
if (linkTarget.startsWith("http://") || linkTarget.startsWith("https://")) {
|
|
545
|
-
violations.push({
|
|
546
|
-
file: filePath,
|
|
547
|
-
message: `Link in "${rule.section}" must point to a ${rule.description} file, not an external URL: "${linkTarget}"`,
|
|
548
|
-
line: i + 1
|
|
549
|
-
});
|
|
550
|
-
match = linkPattern.exec(line);
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
const targetPath = linkTarget.split("#")[0];
|
|
554
|
-
const resolvedPath = resolve(fileDir, targetPath);
|
|
555
|
-
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
556
|
-
violations.push({
|
|
557
|
-
file: filePath,
|
|
558
|
-
message: `Link in "${rule.section}" must point to a ${rule.description} file: "${linkTarget}"`,
|
|
559
|
-
line: i + 1
|
|
560
|
-
});
|
|
639
|
+
for (const link of section.links) {
|
|
640
|
+
const violation = validateSectionLink(artifact, link, rule);
|
|
641
|
+
if (violation) {
|
|
642
|
+
violations.push(violation);
|
|
561
643
|
}
|
|
562
|
-
match = linkPattern.exec(line);
|
|
563
644
|
}
|
|
564
645
|
}
|
|
565
646
|
return violations;
|
|
566
647
|
}
|
|
567
|
-
function validatePrincipleHierarchyLinks(
|
|
648
|
+
function validatePrincipleHierarchyLinks(artifact) {
|
|
568
649
|
const violations = [];
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
let currentSection = null;
|
|
573
|
-
for (let i = 0;i < lines.length; i++) {
|
|
574
|
-
const line = lines[i];
|
|
575
|
-
if (line.startsWith("## ")) {
|
|
576
|
-
currentSection = line;
|
|
577
|
-
continue;
|
|
578
|
-
}
|
|
579
|
-
if (currentSection !== "## Parent Principle" && currentSection !== "## Sub-Principles") {
|
|
650
|
+
const hierarchySections = ["Parent Principle", "Sub-Principles"];
|
|
651
|
+
for (const section of artifact.sections) {
|
|
652
|
+
if (!hierarchySections.includes(section.heading))
|
|
580
653
|
continue;
|
|
581
|
-
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const linkTarget = match[2];
|
|
586
|
-
if (linkTarget.startsWith("#")) {
|
|
654
|
+
const sectionLabel = `## ${section.heading}`;
|
|
655
|
+
const fileDir = dirname(artifact.filePath);
|
|
656
|
+
for (const link of section.links) {
|
|
657
|
+
if (isAnchorLink(link.target)) {
|
|
587
658
|
violations.push({
|
|
588
|
-
file: filePath,
|
|
589
|
-
message: `Link in "${
|
|
590
|
-
line:
|
|
659
|
+
file: artifact.filePath,
|
|
660
|
+
message: `Link in "${sectionLabel}" must point to a principle file, not an anchor: "${link.target}"`,
|
|
661
|
+
line: link.line
|
|
591
662
|
});
|
|
592
|
-
match = linkPattern.exec(line);
|
|
593
663
|
continue;
|
|
594
664
|
}
|
|
595
|
-
if (
|
|
665
|
+
if (isExternalLink(link.target)) {
|
|
596
666
|
violations.push({
|
|
597
|
-
file: filePath,
|
|
598
|
-
message: `Link in "${
|
|
599
|
-
line:
|
|
667
|
+
file: artifact.filePath,
|
|
668
|
+
message: `Link in "${sectionLabel}" must point to a principle file, not an external URL: "${link.target}"`,
|
|
669
|
+
line: link.line
|
|
600
670
|
});
|
|
601
|
-
match = linkPattern.exec(line);
|
|
602
671
|
continue;
|
|
603
672
|
}
|
|
604
|
-
const targetPath =
|
|
673
|
+
const targetPath = link.target.split("#")[0];
|
|
605
674
|
const resolvedPath = resolve(fileDir, targetPath);
|
|
606
675
|
if (!resolvedPath.includes("/.dust/principles/")) {
|
|
607
676
|
violations.push({
|
|
608
|
-
file: filePath,
|
|
609
|
-
message: `Link in "${
|
|
610
|
-
line:
|
|
677
|
+
file: artifact.filePath,
|
|
678
|
+
message: `Link in "${sectionLabel}" must point to a principle file: "${link.target}"`,
|
|
679
|
+
line: link.line
|
|
611
680
|
});
|
|
612
681
|
}
|
|
613
|
-
match = linkPattern.exec(line);
|
|
614
682
|
}
|
|
615
683
|
}
|
|
616
684
|
return violations;
|
|
@@ -618,58 +686,46 @@ function validatePrincipleHierarchyLinks(filePath, content) {
|
|
|
618
686
|
|
|
619
687
|
// lib/lint/validators/principle-hierarchy.ts
|
|
620
688
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
621
|
-
var REQUIRED_PRINCIPLE_HEADINGS = ["
|
|
622
|
-
function validatePrincipleHierarchySections(
|
|
689
|
+
var REQUIRED_PRINCIPLE_HEADINGS = ["Parent Principle", "Sub-Principles"];
|
|
690
|
+
function validatePrincipleHierarchySections(artifact) {
|
|
623
691
|
const violations = [];
|
|
692
|
+
const sectionHeadings = new Set(artifact.sections.map((s) => s.heading));
|
|
624
693
|
for (const heading of REQUIRED_PRINCIPLE_HEADINGS) {
|
|
625
|
-
if (!
|
|
694
|
+
if (!sectionHeadings.has(heading)) {
|
|
626
695
|
violations.push({
|
|
627
|
-
file: filePath,
|
|
628
|
-
message: `Missing required heading: "${heading}"`
|
|
696
|
+
file: artifact.filePath,
|
|
697
|
+
message: `Missing required heading: "## ${heading}"`
|
|
629
698
|
});
|
|
630
699
|
}
|
|
631
700
|
}
|
|
632
701
|
return violations;
|
|
633
702
|
}
|
|
634
|
-
function
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
703
|
+
function isLocalPrincipleLink(target, resolvedPath) {
|
|
704
|
+
const isLocalLink = !target.startsWith("#") && !target.startsWith("http://") && !target.startsWith("https://");
|
|
705
|
+
return isLocalLink && resolvedPath.includes("/.dust/principles/");
|
|
706
|
+
}
|
|
707
|
+
function extractPrincipleRelationships(artifact) {
|
|
708
|
+
const fileDir = dirname2(artifact.filePath);
|
|
638
709
|
const parentPrinciples = [];
|
|
639
710
|
const subPrinciples = [];
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (line.startsWith("## ")) {
|
|
643
|
-
currentSection = line;
|
|
711
|
+
for (const section of artifact.sections) {
|
|
712
|
+
if (section.heading !== "Parent Principle" && section.heading !== "Sub-Principles") {
|
|
644
713
|
continue;
|
|
645
714
|
}
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
}
|
|
649
|
-
const linkPattern = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
650
|
-
let match = linkPattern.exec(line);
|
|
651
|
-
while (match) {
|
|
652
|
-
const linkTarget = match[2];
|
|
653
|
-
const isLocalLink = !linkTarget.startsWith("#") && !linkTarget.startsWith("http://") && !linkTarget.startsWith("https://");
|
|
654
|
-
if (!isLocalLink) {
|
|
655
|
-
match = linkPattern.exec(line);
|
|
656
|
-
continue;
|
|
657
|
-
}
|
|
658
|
-
const targetPath = linkTarget.split("#")[0];
|
|
715
|
+
for (const link of section.links) {
|
|
716
|
+
const targetPath = link.target.split("#")[0];
|
|
659
717
|
const resolvedPath = resolve2(fileDir, targetPath);
|
|
660
|
-
if (!resolvedPath
|
|
661
|
-
match = linkPattern.exec(line);
|
|
718
|
+
if (!isLocalPrincipleLink(link.target, resolvedPath)) {
|
|
662
719
|
continue;
|
|
663
720
|
}
|
|
664
|
-
if (
|
|
721
|
+
if (section.heading === "Parent Principle") {
|
|
665
722
|
parentPrinciples.push(resolvedPath);
|
|
666
723
|
} else {
|
|
667
724
|
subPrinciples.push(resolvedPath);
|
|
668
725
|
}
|
|
669
|
-
match = linkPattern.exec(line);
|
|
670
726
|
}
|
|
671
727
|
}
|
|
672
|
-
return { filePath, parentPrinciples, subPrinciples };
|
|
728
|
+
return { filePath: artifact.filePath, parentPrinciples, subPrinciples };
|
|
673
729
|
}
|
|
674
730
|
function validateBidirectionalLinks(allPrincipleRelationships) {
|
|
675
731
|
const violations = [];
|
|
@@ -732,70 +788,134 @@ function validateNoCycles(allPrincipleRelationships) {
|
|
|
732
788
|
return violations;
|
|
733
789
|
}
|
|
734
790
|
|
|
735
|
-
// lib/validation/
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
791
|
+
// lib/validation/validation-pipeline.ts
|
|
792
|
+
var CONTENT_DIRS = ["principles", "facts", "ideas", "tasks"];
|
|
793
|
+
async function parseArtifacts(fileSystem, dustPath) {
|
|
794
|
+
const artifacts = new Map;
|
|
795
|
+
const byType = {
|
|
796
|
+
ideas: [],
|
|
797
|
+
tasks: [],
|
|
798
|
+
principles: [],
|
|
799
|
+
facts: []
|
|
800
|
+
};
|
|
801
|
+
const rootFiles = [];
|
|
802
|
+
const violations = [];
|
|
803
|
+
let rootEntries;
|
|
804
|
+
try {
|
|
805
|
+
rootEntries = await fileSystem.readdir(dustPath);
|
|
806
|
+
} catch (error) {
|
|
807
|
+
if (error.code === "ENOENT") {
|
|
808
|
+
rootEntries = [];
|
|
809
|
+
} else {
|
|
810
|
+
throw error;
|
|
744
811
|
}
|
|
745
812
|
}
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
if (isDeleted(path)) {
|
|
757
|
-
const error = new Error(`ENOENT: no such file or directory, open '${path}'`);
|
|
758
|
-
error.code = "ENOENT";
|
|
759
|
-
throw error;
|
|
760
|
-
}
|
|
761
|
-
const patchContent = patchFiles.get(path);
|
|
762
|
-
if (patchContent !== undefined) {
|
|
763
|
-
return patchContent;
|
|
813
|
+
for (const entry of rootEntries) {
|
|
814
|
+
if (!entry.endsWith(".md"))
|
|
815
|
+
continue;
|
|
816
|
+
const filePath = `${dustPath}/${entry}`;
|
|
817
|
+
let content;
|
|
818
|
+
try {
|
|
819
|
+
content = await fileSystem.readFile(filePath);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (error.code === "ENOENT") {
|
|
822
|
+
continue;
|
|
764
823
|
}
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
824
|
+
throw error;
|
|
825
|
+
}
|
|
826
|
+
const artifact = parseArtifact(filePath, content);
|
|
827
|
+
artifacts.set(filePath, artifact);
|
|
828
|
+
rootFiles.push(artifact);
|
|
829
|
+
}
|
|
830
|
+
for (const dir of CONTENT_DIRS) {
|
|
831
|
+
const dirPath = `${dustPath}/${dir}`;
|
|
832
|
+
violations.push(...await validateContentDirectoryFiles(dirPath, fileSystem));
|
|
833
|
+
let entries;
|
|
834
|
+
try {
|
|
835
|
+
entries = await fileSystem.readdir(dirPath);
|
|
836
|
+
} catch (error) {
|
|
837
|
+
if (error.code === "ENOENT") {
|
|
838
|
+
continue;
|
|
776
839
|
}
|
|
840
|
+
throw error;
|
|
841
|
+
}
|
|
842
|
+
for (const entry of entries) {
|
|
843
|
+
if (!entry.endsWith(".md"))
|
|
844
|
+
continue;
|
|
845
|
+
const filePath = `${dirPath}/${entry}`;
|
|
846
|
+
let content;
|
|
777
847
|
try {
|
|
778
|
-
|
|
779
|
-
for (const entry of baseEntries) {
|
|
780
|
-
const entryPath = `${path}/${entry}`;
|
|
781
|
-
if (!isDeleted(entryPath)) {
|
|
782
|
-
entries.add(entry);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
848
|
+
content = await fileSystem.readFile(filePath);
|
|
785
849
|
} catch (error) {
|
|
786
|
-
if (error.code
|
|
787
|
-
|
|
850
|
+
if (error.code === "ENOENT") {
|
|
851
|
+
continue;
|
|
788
852
|
}
|
|
853
|
+
throw error;
|
|
789
854
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (isDeleted(path))
|
|
794
|
-
return false;
|
|
795
|
-
return patchDirs.has(path) || base.isDirectory(path);
|
|
855
|
+
const artifact = parseArtifact(filePath, content);
|
|
856
|
+
artifacts.set(filePath, artifact);
|
|
857
|
+
byType[dir].push(artifact);
|
|
796
858
|
}
|
|
859
|
+
}
|
|
860
|
+
return {
|
|
861
|
+
context: {
|
|
862
|
+
artifacts,
|
|
863
|
+
byType,
|
|
864
|
+
rootFiles,
|
|
865
|
+
dustPath,
|
|
866
|
+
fileSystem
|
|
867
|
+
},
|
|
868
|
+
violations
|
|
797
869
|
};
|
|
798
870
|
}
|
|
871
|
+
function validateArtifacts(context) {
|
|
872
|
+
const violations = [];
|
|
873
|
+
const { byType, rootFiles, dustPath, fileSystem } = context;
|
|
874
|
+
const ideasPath = `${dustPath}/ideas`;
|
|
875
|
+
for (const artifact of rootFiles) {
|
|
876
|
+
violations.push(...validateLinks(artifact, fileSystem));
|
|
877
|
+
}
|
|
878
|
+
for (const artifacts of Object.values(byType)) {
|
|
879
|
+
for (const artifact of artifacts) {
|
|
880
|
+
const openingSentenceViolation = validateOpeningSentence(artifact);
|
|
881
|
+
if (openingSentenceViolation)
|
|
882
|
+
violations.push(openingSentenceViolation);
|
|
883
|
+
const lengthViolation = validateOpeningSentenceLength(artifact);
|
|
884
|
+
if (lengthViolation)
|
|
885
|
+
violations.push(lengthViolation);
|
|
886
|
+
const titleViolation = validateTitleFilenameMatch(artifact);
|
|
887
|
+
if (titleViolation)
|
|
888
|
+
violations.push(titleViolation);
|
|
889
|
+
violations.push(...validateLinks(artifact, fileSystem));
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
for (const artifact of byType.ideas) {
|
|
893
|
+
violations.push(...validateIdeaOpenQuestions(artifact));
|
|
894
|
+
}
|
|
895
|
+
for (const artifact of byType.tasks) {
|
|
896
|
+
const filenameViolation = validateFilename(artifact.filePath);
|
|
897
|
+
if (filenameViolation)
|
|
898
|
+
violations.push(filenameViolation);
|
|
899
|
+
violations.push(...validateTaskHeadings(artifact));
|
|
900
|
+
violations.push(...validateSemanticLinks(artifact));
|
|
901
|
+
const imperativeViolation = validateImperativeOpeningSentence(artifact);
|
|
902
|
+
if (imperativeViolation)
|
|
903
|
+
violations.push(imperativeViolation);
|
|
904
|
+
const ideaTransitionViolation = validateIdeaTransitionTitle(artifact, ideasPath, fileSystem);
|
|
905
|
+
if (ideaTransitionViolation)
|
|
906
|
+
violations.push(ideaTransitionViolation);
|
|
907
|
+
violations.push(...validateWorkflowTaskBodySection(artifact, ideasPath, fileSystem));
|
|
908
|
+
}
|
|
909
|
+
const allPrincipleRelationships = [];
|
|
910
|
+
for (const artifact of byType.principles) {
|
|
911
|
+
violations.push(...validatePrincipleHierarchySections(artifact));
|
|
912
|
+
violations.push(...validatePrincipleHierarchyLinks(artifact));
|
|
913
|
+
allPrincipleRelationships.push(extractPrincipleRelationships(artifact));
|
|
914
|
+
}
|
|
915
|
+
violations.push(...validateBidirectionalLinks(allPrincipleRelationships));
|
|
916
|
+
violations.push(...validateNoCycles(allPrincipleRelationships));
|
|
917
|
+
return violations;
|
|
918
|
+
}
|
|
799
919
|
|
|
800
920
|
// lib/validation/index.ts
|
|
801
921
|
var ALLOWED_ROOT_DIRECTORIES = [
|
|
@@ -855,36 +975,6 @@ function relativizeViolations(violations, cwd) {
|
|
|
855
975
|
file: relativizeViolationFilePath(violation.file, cwd)
|
|
856
976
|
}));
|
|
857
977
|
}
|
|
858
|
-
var CONTENT_DIRS = ["principles", "facts", "ideas", "tasks"];
|
|
859
|
-
function validateContentFile(filePath, content) {
|
|
860
|
-
const violations = [];
|
|
861
|
-
const openingSentence = validateOpeningSentence(filePath, content);
|
|
862
|
-
if (openingSentence)
|
|
863
|
-
violations.push(openingSentence);
|
|
864
|
-
const openingSentenceLength = validateOpeningSentenceLength(filePath, content);
|
|
865
|
-
if (openingSentenceLength)
|
|
866
|
-
violations.push(openingSentenceLength);
|
|
867
|
-
const titleFilename = validateTitleFilenameMatch(filePath, content);
|
|
868
|
-
if (titleFilename)
|
|
869
|
-
violations.push(titleFilename);
|
|
870
|
-
return violations;
|
|
871
|
-
}
|
|
872
|
-
function validateTaskFile(filePath, content, ideasPath, overlayFs) {
|
|
873
|
-
const violations = [];
|
|
874
|
-
const filenameViolation = validateFilename(filePath);
|
|
875
|
-
if (filenameViolation)
|
|
876
|
-
violations.push(filenameViolation);
|
|
877
|
-
violations.push(...validateTaskHeadings(filePath, content));
|
|
878
|
-
violations.push(...validateSemanticLinks(filePath, content));
|
|
879
|
-
const imperativeViolation = validateImperativeOpeningSentence(filePath, content);
|
|
880
|
-
if (imperativeViolation)
|
|
881
|
-
violations.push(imperativeViolation);
|
|
882
|
-
const ideaTransition = validateIdeaTransitionTitle(filePath, content, ideasPath, overlayFs);
|
|
883
|
-
if (ideaTransition)
|
|
884
|
-
violations.push(ideaTransition);
|
|
885
|
-
violations.push(...validateWorkflowTaskBodySection(filePath, content, ideasPath, overlayFs));
|
|
886
|
-
return violations;
|
|
887
|
-
}
|
|
888
978
|
function parsePatchFiles(dustPath, patch) {
|
|
889
979
|
const absolutePatchFiles = new Map;
|
|
890
980
|
const deletedPaths = new Set;
|
|
@@ -898,75 +988,15 @@ function parsePatchFiles(dustPath, patch) {
|
|
|
898
988
|
}
|
|
899
989
|
return { absolutePatchFiles, deletedPaths };
|
|
900
990
|
}
|
|
901
|
-
function collectPatchDirs(patch) {
|
|
902
|
-
const patchDirs = new Set;
|
|
903
|
-
for (const relativePath of Object.keys(patch.files)) {
|
|
904
|
-
const dir = relativePath.split("/")[0];
|
|
905
|
-
if (CONTENT_DIRS.includes(dir)) {
|
|
906
|
-
patchDirs.add(dir);
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
return patchDirs;
|
|
910
|
-
}
|
|
911
|
-
async function validatePrincipleRelationships(dustPath, overlayFs) {
|
|
912
|
-
const allRelationships = [];
|
|
913
|
-
const principlesPath = `${dustPath}/principles`;
|
|
914
|
-
try {
|
|
915
|
-
const existingFiles = await overlayFs.readdir(principlesPath);
|
|
916
|
-
for (const file of existingFiles) {
|
|
917
|
-
if (!file.endsWith(".md"))
|
|
918
|
-
continue;
|
|
919
|
-
const filePath = `${principlesPath}/${file}`;
|
|
920
|
-
const content = await overlayFs.readFile(filePath);
|
|
921
|
-
allRelationships.push(extractPrincipleRelationships(filePath, content));
|
|
922
|
-
}
|
|
923
|
-
} catch (error) {
|
|
924
|
-
if (error.code !== "ENOENT") {
|
|
925
|
-
throw error;
|
|
926
|
-
}
|
|
927
|
-
}
|
|
928
|
-
return [
|
|
929
|
-
...validateBidirectionalLinks(allRelationships),
|
|
930
|
-
...validateNoCycles(allRelationships)
|
|
931
|
-
];
|
|
932
|
-
}
|
|
933
991
|
async function validatePatch(fileSystem, dustPath, patch, options = {}) {
|
|
934
992
|
const cwd = options.cwd ?? process.cwd();
|
|
935
993
|
const { absolutePatchFiles, deletedPaths } = parsePatchFiles(dustPath, patch);
|
|
936
994
|
const overlayFs = createOverlayFileSystem(fileSystem, absolutePatchFiles, deletedPaths);
|
|
937
995
|
const violations = [];
|
|
938
996
|
violations.push(...validatePatchRootEntries(fileSystem, dustPath, patch));
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
}
|
|
943
|
-
const ideasPath = `${dustPath}/ideas`;
|
|
944
|
-
for (const [relativePath, content] of Object.entries(patch.files)) {
|
|
945
|
-
if (content === null)
|
|
946
|
-
continue;
|
|
947
|
-
if (!relativePath.endsWith(".md"))
|
|
948
|
-
continue;
|
|
949
|
-
const filePath = `${dustPath}/${relativePath}`;
|
|
950
|
-
const dir = relativePath.split("/")[0];
|
|
951
|
-
violations.push(...validateLinks(filePath, content, overlayFs));
|
|
952
|
-
if (CONTENT_DIRS.includes(dir)) {
|
|
953
|
-
violations.push(...validateContentFile(filePath, content));
|
|
954
|
-
}
|
|
955
|
-
if (dir === "ideas") {
|
|
956
|
-
violations.push(...validateIdeaOpenQuestions(filePath, content));
|
|
957
|
-
}
|
|
958
|
-
if (dir === "tasks") {
|
|
959
|
-
violations.push(...validateTaskFile(filePath, content, ideasPath, overlayFs));
|
|
960
|
-
}
|
|
961
|
-
if (dir === "principles") {
|
|
962
|
-
violations.push(...validatePrincipleHierarchySections(filePath, content));
|
|
963
|
-
violations.push(...validatePrincipleHierarchyLinks(filePath, content));
|
|
964
|
-
}
|
|
965
|
-
}
|
|
966
|
-
const hasPrinciplePatches = Object.keys(patch.files).some((p) => p.startsWith("principles/"));
|
|
967
|
-
if (hasPrinciplePatches) {
|
|
968
|
-
violations.push(...await validatePrincipleRelationships(dustPath, overlayFs));
|
|
969
|
-
}
|
|
997
|
+
const { context, violations: parseViolations } = await parseArtifacts(overlayFs, dustPath);
|
|
998
|
+
violations.push(...parseViolations);
|
|
999
|
+
violations.push(...validateArtifacts(context));
|
|
970
1000
|
return {
|
|
971
1001
|
valid: violations.length === 0,
|
|
972
1002
|
violations: relativizeViolations(violations, cwd)
|