@joshski/dust 0.1.97 → 0.1.99
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 +14 -2
- package/dist/artifacts/parsed-artifact.d.ts +32 -0
- package/dist/artifacts.js +166 -1
- package/dist/audits/checks-audit.d.ts +13 -13
- package/dist/audits.js +49 -271
- package/dist/bucket/agent-capabilities.d.ts +30 -0
- package/dist/bucket/repository-git.d.ts +2 -1
- package/dist/bucket/repository.d.ts +9 -1
- package/dist/bucket/server-messages.d.ts +36 -1
- package/dist/cli/commands/focus.d.ts +1 -1
- package/dist/cli/process-runner.d.ts +35 -0
- package/dist/dust.js +1015 -791
- 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 +8 -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 +1645 -0
- package/dist/tech-stack/index.d.ts +22 -0
- package/dist/validation/validation-pipeline.d.ts +42 -0
- package/dist/validation.js +451 -407
- 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_PATTERN2 = /\[([^\]]+)\]\(([^)]+)\)/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;
|
|
14
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_PATTERN2);
|
|
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
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (currentSection !== null) {
|
|
143
|
+
currentSection.endLine = findLastNonEmptyLine(lines, currentSection.startLine, lines.length - 1);
|
|
144
|
+
sections.push(currentSection);
|
|
15
145
|
}
|
|
16
|
-
return
|
|
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,28 @@ function extractFirstSentence(paragraph) {
|
|
|
52
202
|
const match = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
|
|
53
203
|
return match ? match[1] : null;
|
|
54
204
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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);
|
|
205
|
+
|
|
206
|
+
// lib/artifacts/workflow-tasks.ts
|
|
207
|
+
var IDEA_TRANSITION_PREFIXES = [
|
|
208
|
+
"Refine Idea: ",
|
|
209
|
+
"Decompose Idea: ",
|
|
210
|
+
"Shelve Idea: ",
|
|
211
|
+
"Expedite Idea: "
|
|
212
|
+
];
|
|
213
|
+
function titleToFilename(title) {
|
|
214
|
+
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
72
215
|
}
|
|
73
216
|
|
|
217
|
+
// lib/artifacts/index.ts
|
|
218
|
+
var ARTIFACT_TYPES = [
|
|
219
|
+
"facts",
|
|
220
|
+
"ideas",
|
|
221
|
+
"principles",
|
|
222
|
+
"tasks"
|
|
223
|
+
];
|
|
224
|
+
|
|
74
225
|
// lib/lint/validators/content-validator.ts
|
|
75
|
-
var
|
|
226
|
+
var REQUIRED_TASK_HEADINGS = ["Blocked By", "Definition of Done"];
|
|
76
227
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
77
228
|
var NON_IMPERATIVE_STARTERS = new Set([
|
|
78
229
|
"the",
|
|
@@ -88,31 +239,32 @@ var NON_IMPERATIVE_STARTERS = new Set([
|
|
|
88
239
|
"you",
|
|
89
240
|
"i"
|
|
90
241
|
]);
|
|
91
|
-
function validateOpeningSentence(
|
|
92
|
-
|
|
93
|
-
if (!openingSentence) {
|
|
242
|
+
function validateOpeningSentence(artifact) {
|
|
243
|
+
if (!artifact.openingSentence) {
|
|
94
244
|
return {
|
|
95
|
-
file: filePath,
|
|
245
|
+
file: artifact.filePath,
|
|
246
|
+
line: artifact.titleLine ?? undefined,
|
|
96
247
|
message: "Missing or malformed opening sentence after H1 heading"
|
|
97
248
|
};
|
|
98
249
|
}
|
|
99
250
|
return null;
|
|
100
251
|
}
|
|
101
|
-
function validateOpeningSentenceLength(
|
|
102
|
-
const openingSentence =
|
|
252
|
+
function validateOpeningSentenceLength(artifact) {
|
|
253
|
+
const openingSentence = artifact.openingSentence;
|
|
103
254
|
if (!openingSentence) {
|
|
104
255
|
return null;
|
|
105
256
|
}
|
|
106
257
|
if (openingSentence.length > MAX_OPENING_SENTENCE_LENGTH) {
|
|
107
258
|
return {
|
|
108
|
-
file: filePath,
|
|
259
|
+
file: artifact.filePath,
|
|
260
|
+
line: artifact.openingSentenceLine ?? undefined,
|
|
109
261
|
message: `Opening sentence is ${openingSentence.length} characters (max ${MAX_OPENING_SENTENCE_LENGTH}). Split into multiple sentences; only the first sentence is checked.`
|
|
110
262
|
};
|
|
111
263
|
}
|
|
112
264
|
return null;
|
|
113
265
|
}
|
|
114
|
-
function validateImperativeOpeningSentence(
|
|
115
|
-
const openingSentence =
|
|
266
|
+
function validateImperativeOpeningSentence(artifact) {
|
|
267
|
+
const openingSentence = artifact.openingSentence;
|
|
116
268
|
if (!openingSentence) {
|
|
117
269
|
return null;
|
|
118
270
|
}
|
|
@@ -121,19 +273,21 @@ function validateImperativeOpeningSentence(filePath, content) {
|
|
|
121
273
|
if (NON_IMPERATIVE_STARTERS.has(lower) || lower.endsWith("ing")) {
|
|
122
274
|
const preview = openingSentence.length > 40 ? `${openingSentence.slice(0, 40)}...` : openingSentence;
|
|
123
275
|
return {
|
|
124
|
-
file: filePath,
|
|
276
|
+
file: artifact.filePath,
|
|
277
|
+
line: artifact.openingSentenceLine ?? undefined,
|
|
125
278
|
message: `Opening sentence should use imperative form (e.g., "Add X" not "This adds X"). Found: "${preview}"`
|
|
126
279
|
};
|
|
127
280
|
}
|
|
128
281
|
return null;
|
|
129
282
|
}
|
|
130
|
-
function validateTaskHeadings(
|
|
283
|
+
function validateTaskHeadings(artifact) {
|
|
131
284
|
const violations = [];
|
|
132
|
-
|
|
133
|
-
|
|
285
|
+
const sectionHeadings = new Set(artifact.sections.map((s) => s.heading));
|
|
286
|
+
for (const heading of REQUIRED_TASK_HEADINGS) {
|
|
287
|
+
if (!sectionHeadings.has(heading)) {
|
|
134
288
|
violations.push({
|
|
135
|
-
file: filePath,
|
|
136
|
-
message: `Missing required heading: "${heading}"`
|
|
289
|
+
file: artifact.filePath,
|
|
290
|
+
message: `Missing required heading: "## ${heading}"`
|
|
137
291
|
});
|
|
138
292
|
}
|
|
139
293
|
}
|
|
@@ -141,6 +295,7 @@ function validateTaskHeadings(filePath, content) {
|
|
|
141
295
|
}
|
|
142
296
|
|
|
143
297
|
// lib/lint/validators/directory-validator.ts
|
|
298
|
+
var EXPECTED_DIRECTORIES = [...ARTIFACT_TYPES, "config"];
|
|
144
299
|
async function validateContentDirectoryFiles(dirPath, fileSystem) {
|
|
145
300
|
const violations = [];
|
|
146
301
|
let entries;
|
|
@@ -178,17 +333,6 @@ async function validateContentDirectoryFiles(dirPath, fileSystem) {
|
|
|
178
333
|
return violations;
|
|
179
334
|
}
|
|
180
335
|
|
|
181
|
-
// lib/artifacts/workflow-tasks.ts
|
|
182
|
-
var IDEA_TRANSITION_PREFIXES = [
|
|
183
|
-
"Refine Idea: ",
|
|
184
|
-
"Decompose Idea: ",
|
|
185
|
-
"Shelve Idea: ",
|
|
186
|
-
"Expedite Idea: "
|
|
187
|
-
];
|
|
188
|
-
function titleToFilename(title) {
|
|
189
|
-
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
190
|
-
}
|
|
191
|
-
|
|
192
336
|
// lib/lint/validators/filename-validator.ts
|
|
193
337
|
var SLUG_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
194
338
|
function validateFilename(filePath) {
|
|
@@ -202,17 +346,17 @@ function validateFilename(filePath) {
|
|
|
202
346
|
}
|
|
203
347
|
return null;
|
|
204
348
|
}
|
|
205
|
-
function validateTitleFilenameMatch(
|
|
206
|
-
const title =
|
|
349
|
+
function validateTitleFilenameMatch(artifact) {
|
|
350
|
+
const title = artifact.title;
|
|
207
351
|
if (!title) {
|
|
208
352
|
return null;
|
|
209
353
|
}
|
|
210
|
-
const parts = filePath.split("/");
|
|
354
|
+
const parts = artifact.filePath.split("/");
|
|
211
355
|
const actualFilename = parts[parts.length - 1];
|
|
212
356
|
const expectedFilename = titleToFilename(title);
|
|
213
357
|
if (actualFilename !== expectedFilename) {
|
|
214
358
|
return {
|
|
215
|
-
file: filePath,
|
|
359
|
+
file: artifact.filePath,
|
|
216
360
|
message: `Filename "${actualFilename}" does not match title "${title}" (expected "${expectedFilename}")`
|
|
217
361
|
};
|
|
218
362
|
}
|
|
@@ -252,10 +396,11 @@ function validateH2Heading(filePath, line, lineNumber, inOpenQuestions, currentQ
|
|
|
252
396
|
}
|
|
253
397
|
return violations;
|
|
254
398
|
}
|
|
255
|
-
function validateIdeaOpenQuestions(
|
|
399
|
+
function validateIdeaOpenQuestions(artifact) {
|
|
256
400
|
const violations = [];
|
|
257
|
-
const lines =
|
|
401
|
+
const lines = artifact.rawContent.split(`
|
|
258
402
|
`);
|
|
403
|
+
const filePath = artifact.filePath;
|
|
259
404
|
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
405
|
let inOpenQuestions = false;
|
|
261
406
|
let currentQuestionLine = null;
|
|
@@ -331,18 +476,24 @@ function validateIdeaOpenQuestions(filePath, content) {
|
|
|
331
476
|
}
|
|
332
477
|
return violations;
|
|
333
478
|
}
|
|
334
|
-
function validateIdeaTransitionTitle(
|
|
335
|
-
const title =
|
|
479
|
+
function validateIdeaTransitionTitle(artifact, ideasPath, fileSystem) {
|
|
480
|
+
const title = artifact.title;
|
|
336
481
|
if (!title) {
|
|
337
482
|
return null;
|
|
338
483
|
}
|
|
339
484
|
for (const prefix of IDEA_TRANSITION_PREFIXES) {
|
|
340
485
|
if (title.startsWith(prefix)) {
|
|
486
|
+
if (prefix === "Expedite Idea: ") {
|
|
487
|
+
const hasIdeaDescriptionSection = artifact.sections.some((s) => s.heading === "Idea Description" && s.level === 2);
|
|
488
|
+
if (hasIdeaDescriptionSection) {
|
|
489
|
+
return null;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
341
492
|
const ideaTitle = title.slice(prefix.length);
|
|
342
493
|
const ideaFilename = titleToFilename(ideaTitle);
|
|
343
494
|
if (!fileSystem.exists(`${ideasPath}/${ideaFilename}`)) {
|
|
344
495
|
return {
|
|
345
|
-
file: filePath,
|
|
496
|
+
file: artifact.filePath,
|
|
346
497
|
message: `Idea transition task references non-existent idea: "${ideaTitle}" (expected file "${ideaFilename}" in ideas/)`
|
|
347
498
|
};
|
|
348
499
|
}
|
|
@@ -351,37 +502,9 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
|
351
502
|
}
|
|
352
503
|
return null;
|
|
353
504
|
}
|
|
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) {
|
|
505
|
+
function validateWorkflowTaskBodySection(artifact, ideasPath, fileSystem) {
|
|
383
506
|
const violations = [];
|
|
384
|
-
const title =
|
|
507
|
+
const title = artifact.title;
|
|
385
508
|
if (!title)
|
|
386
509
|
return violations;
|
|
387
510
|
let matchedPrefix = null;
|
|
@@ -394,10 +517,10 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
394
517
|
if (!matchedPrefix)
|
|
395
518
|
return violations;
|
|
396
519
|
const expectedHeading = WORKFLOW_PREFIX_TO_SECTION[matchedPrefix];
|
|
397
|
-
const section =
|
|
520
|
+
const section = artifact.sections.find((s) => s.heading === expectedHeading && s.level === 2);
|
|
398
521
|
if (!section) {
|
|
399
522
|
violations.push({
|
|
400
|
-
file: filePath,
|
|
523
|
+
file: artifact.filePath,
|
|
401
524
|
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
525
|
|
|
403
526
|
## ${expectedHeading}
|
|
@@ -406,25 +529,9 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
406
529
|
});
|
|
407
530
|
return violations;
|
|
408
531
|
}
|
|
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) {
|
|
532
|
+
if (section.links.length === 0) {
|
|
426
533
|
violations.push({
|
|
427
|
-
file: filePath,
|
|
534
|
+
file: artifact.filePath,
|
|
428
535
|
message: `"## ${expectedHeading}" section contains no link. Add a markdown link to the idea file, e.g.:
|
|
429
536
|
|
|
430
537
|
- [Idea Title](../ideas/idea-slug.md)`,
|
|
@@ -432,10 +539,10 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
432
539
|
});
|
|
433
540
|
return violations;
|
|
434
541
|
}
|
|
435
|
-
const ideaLinks = links.filter((l) => l.target.includes("/ideas/") || l.target.startsWith("../ideas/"));
|
|
542
|
+
const ideaLinks = section.links.filter((l) => l.target.includes("/ideas/") || l.target.startsWith("../ideas/"));
|
|
436
543
|
if (ideaLinks.length === 0) {
|
|
437
544
|
violations.push({
|
|
438
|
-
file: filePath,
|
|
545
|
+
file: artifact.filePath,
|
|
439
546
|
message: `"## ${expectedHeading}" section contains no link to an idea file. Links must point to a file in ../ideas/, e.g.:
|
|
440
547
|
|
|
441
548
|
- [Idea Title](../ideas/idea-slug.md)`,
|
|
@@ -451,7 +558,7 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
451
558
|
const ideaFilePath = `${ideasPath}/${ideaSlug}.md`;
|
|
452
559
|
if (!fileSystem.exists(ideaFilePath)) {
|
|
453
560
|
violations.push({
|
|
454
|
-
file: filePath,
|
|
561
|
+
file: artifact.filePath,
|
|
455
562
|
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
563
|
line: link.line
|
|
457
564
|
});
|
|
@@ -464,153 +571,129 @@ function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSyste
|
|
|
464
571
|
import { dirname, resolve } from "node:path";
|
|
465
572
|
var SEMANTIC_RULES = [
|
|
466
573
|
{
|
|
467
|
-
|
|
574
|
+
sectionHeading: "Principles",
|
|
468
575
|
requiredPath: "/.dust/principles/",
|
|
469
576
|
description: "principle"
|
|
470
577
|
},
|
|
471
578
|
{
|
|
472
|
-
|
|
579
|
+
sectionHeading: "Blocked By",
|
|
473
580
|
requiredPath: "/.dust/tasks/",
|
|
474
581
|
description: "task"
|
|
475
582
|
}
|
|
476
583
|
];
|
|
477
|
-
function
|
|
584
|
+
function isExternalOrAnchorLink(target) {
|
|
585
|
+
return target.startsWith("http://") || target.startsWith("https://") || target.startsWith("#");
|
|
586
|
+
}
|
|
587
|
+
function isAnchorLink(target) {
|
|
588
|
+
return target.startsWith("#");
|
|
589
|
+
}
|
|
590
|
+
function isExternalLink(target) {
|
|
591
|
+
return target.startsWith("http://") || target.startsWith("https://");
|
|
592
|
+
}
|
|
593
|
+
function validateLinks(artifact, fileSystem) {
|
|
478
594
|
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);
|
|
595
|
+
const fileDir = dirname(artifact.filePath);
|
|
596
|
+
for (const link of artifact.allLinks) {
|
|
597
|
+
if (isExternalOrAnchorLink(link.target)) {
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
600
|
+
if (link.target.startsWith("/")) {
|
|
601
|
+
violations.push({
|
|
602
|
+
file: artifact.filePath,
|
|
603
|
+
message: `Absolute link not allowed: "${link.target}" (use a relative path instead)`,
|
|
604
|
+
line: link.line
|
|
605
|
+
});
|
|
606
|
+
continue;
|
|
607
|
+
}
|
|
608
|
+
const targetPath = link.target.split("#")[0];
|
|
609
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
610
|
+
if (!fileSystem.exists(resolvedPath)) {
|
|
611
|
+
violations.push({
|
|
612
|
+
file: artifact.filePath,
|
|
613
|
+
message: `Broken link: "${link.target}"`,
|
|
614
|
+
line: link.line
|
|
615
|
+
});
|
|
512
616
|
}
|
|
513
617
|
}
|
|
514
618
|
return violations;
|
|
515
619
|
}
|
|
516
|
-
function
|
|
620
|
+
function validateSectionLink(artifact, link, rule) {
|
|
621
|
+
const sectionLabel = `## ${rule.sectionHeading}`;
|
|
622
|
+
if (isAnchorLink(link.target)) {
|
|
623
|
+
return {
|
|
624
|
+
file: artifact.filePath,
|
|
625
|
+
message: `Link in "${sectionLabel}" must point to a ${rule.description} file, not an anchor: "${link.target}"`,
|
|
626
|
+
line: link.line
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
if (isExternalLink(link.target)) {
|
|
630
|
+
return {
|
|
631
|
+
file: artifact.filePath,
|
|
632
|
+
message: `Link in "${sectionLabel}" must point to a ${rule.description} file, not an external URL: "${link.target}"`,
|
|
633
|
+
line: link.line
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
const fileDir = dirname(artifact.filePath);
|
|
637
|
+
const targetPath = link.target.split("#")[0];
|
|
638
|
+
const resolvedPath = resolve(fileDir, targetPath);
|
|
639
|
+
if (!resolvedPath.includes(rule.requiredPath)) {
|
|
640
|
+
return {
|
|
641
|
+
file: artifact.filePath,
|
|
642
|
+
message: `Link in "${sectionLabel}" must point to a ${rule.description} file: "${link.target}"`,
|
|
643
|
+
line: link.line
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
function validateSemanticLinks(artifact) {
|
|
517
649
|
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);
|
|
650
|
+
for (const section of artifact.sections) {
|
|
651
|
+
const rule = SEMANTIC_RULES.find((r) => r.sectionHeading === section.heading);
|
|
529
652
|
if (!rule)
|
|
530
653
|
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
|
-
});
|
|
654
|
+
for (const link of section.links) {
|
|
655
|
+
const violation = validateSectionLink(artifact, link, rule);
|
|
656
|
+
if (violation) {
|
|
657
|
+
violations.push(violation);
|
|
561
658
|
}
|
|
562
|
-
match = linkPattern.exec(line);
|
|
563
659
|
}
|
|
564
660
|
}
|
|
565
661
|
return violations;
|
|
566
662
|
}
|
|
567
|
-
function validatePrincipleHierarchyLinks(
|
|
663
|
+
function validatePrincipleHierarchyLinks(artifact) {
|
|
568
664
|
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") {
|
|
665
|
+
const hierarchySections = ["Parent Principle", "Sub-Principles"];
|
|
666
|
+
for (const section of artifact.sections) {
|
|
667
|
+
if (!hierarchySections.includes(section.heading))
|
|
580
668
|
continue;
|
|
581
|
-
}
|
|
582
|
-
const
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
const linkTarget = match[2];
|
|
586
|
-
if (linkTarget.startsWith("#")) {
|
|
669
|
+
const sectionLabel = `## ${section.heading}`;
|
|
670
|
+
const fileDir = dirname(artifact.filePath);
|
|
671
|
+
for (const link of section.links) {
|
|
672
|
+
if (isAnchorLink(link.target)) {
|
|
587
673
|
violations.push({
|
|
588
|
-
file: filePath,
|
|
589
|
-
message: `Link in "${
|
|
590
|
-
line:
|
|
674
|
+
file: artifact.filePath,
|
|
675
|
+
message: `Link in "${sectionLabel}" must point to a principle file, not an anchor: "${link.target}"`,
|
|
676
|
+
line: link.line
|
|
591
677
|
});
|
|
592
|
-
match = linkPattern.exec(line);
|
|
593
678
|
continue;
|
|
594
679
|
}
|
|
595
|
-
if (
|
|
680
|
+
if (isExternalLink(link.target)) {
|
|
596
681
|
violations.push({
|
|
597
|
-
file: filePath,
|
|
598
|
-
message: `Link in "${
|
|
599
|
-
line:
|
|
682
|
+
file: artifact.filePath,
|
|
683
|
+
message: `Link in "${sectionLabel}" must point to a principle file, not an external URL: "${link.target}"`,
|
|
684
|
+
line: link.line
|
|
600
685
|
});
|
|
601
|
-
match = linkPattern.exec(line);
|
|
602
686
|
continue;
|
|
603
687
|
}
|
|
604
|
-
const targetPath =
|
|
688
|
+
const targetPath = link.target.split("#")[0];
|
|
605
689
|
const resolvedPath = resolve(fileDir, targetPath);
|
|
606
690
|
if (!resolvedPath.includes("/.dust/principles/")) {
|
|
607
691
|
violations.push({
|
|
608
|
-
file: filePath,
|
|
609
|
-
message: `Link in "${
|
|
610
|
-
line:
|
|
692
|
+
file: artifact.filePath,
|
|
693
|
+
message: `Link in "${sectionLabel}" must point to a principle file: "${link.target}"`,
|
|
694
|
+
line: link.line
|
|
611
695
|
});
|
|
612
696
|
}
|
|
613
|
-
match = linkPattern.exec(line);
|
|
614
697
|
}
|
|
615
698
|
}
|
|
616
699
|
return violations;
|
|
@@ -618,58 +701,46 @@ function validatePrincipleHierarchyLinks(filePath, content) {
|
|
|
618
701
|
|
|
619
702
|
// lib/lint/validators/principle-hierarchy.ts
|
|
620
703
|
import { dirname as dirname2, resolve as resolve2 } from "node:path";
|
|
621
|
-
var REQUIRED_PRINCIPLE_HEADINGS = ["
|
|
622
|
-
function validatePrincipleHierarchySections(
|
|
704
|
+
var REQUIRED_PRINCIPLE_HEADINGS = ["Parent Principle", "Sub-Principles"];
|
|
705
|
+
function validatePrincipleHierarchySections(artifact) {
|
|
623
706
|
const violations = [];
|
|
707
|
+
const sectionHeadings = new Set(artifact.sections.map((s) => s.heading));
|
|
624
708
|
for (const heading of REQUIRED_PRINCIPLE_HEADINGS) {
|
|
625
|
-
if (!
|
|
709
|
+
if (!sectionHeadings.has(heading)) {
|
|
626
710
|
violations.push({
|
|
627
|
-
file: filePath,
|
|
628
|
-
message: `Missing required heading: "${heading}"`
|
|
711
|
+
file: artifact.filePath,
|
|
712
|
+
message: `Missing required heading: "## ${heading}"`
|
|
629
713
|
});
|
|
630
714
|
}
|
|
631
715
|
}
|
|
632
716
|
return violations;
|
|
633
717
|
}
|
|
634
|
-
function
|
|
635
|
-
const
|
|
636
|
-
|
|
637
|
-
|
|
718
|
+
function isLocalPrincipleLink(target, resolvedPath) {
|
|
719
|
+
const isLocalLink = !target.startsWith("#") && !target.startsWith("http://") && !target.startsWith("https://");
|
|
720
|
+
return isLocalLink && resolvedPath.includes("/.dust/principles/");
|
|
721
|
+
}
|
|
722
|
+
function extractPrincipleRelationships(artifact) {
|
|
723
|
+
const fileDir = dirname2(artifact.filePath);
|
|
638
724
|
const parentPrinciples = [];
|
|
639
725
|
const subPrinciples = [];
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
if (line.startsWith("## ")) {
|
|
643
|
-
currentSection = line;
|
|
726
|
+
for (const section of artifact.sections) {
|
|
727
|
+
if (section.heading !== "Parent Principle" && section.heading !== "Sub-Principles") {
|
|
644
728
|
continue;
|
|
645
729
|
}
|
|
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];
|
|
730
|
+
for (const link of section.links) {
|
|
731
|
+
const targetPath = link.target.split("#")[0];
|
|
659
732
|
const resolvedPath = resolve2(fileDir, targetPath);
|
|
660
|
-
if (!resolvedPath
|
|
661
|
-
match = linkPattern.exec(line);
|
|
733
|
+
if (!isLocalPrincipleLink(link.target, resolvedPath)) {
|
|
662
734
|
continue;
|
|
663
735
|
}
|
|
664
|
-
if (
|
|
736
|
+
if (section.heading === "Parent Principle") {
|
|
665
737
|
parentPrinciples.push(resolvedPath);
|
|
666
738
|
} else {
|
|
667
739
|
subPrinciples.push(resolvedPath);
|
|
668
740
|
}
|
|
669
|
-
match = linkPattern.exec(line);
|
|
670
741
|
}
|
|
671
742
|
}
|
|
672
|
-
return { filePath, parentPrinciples, subPrinciples };
|
|
743
|
+
return { filePath: artifact.filePath, parentPrinciples, subPrinciples };
|
|
673
744
|
}
|
|
674
745
|
function validateBidirectionalLinks(allPrincipleRelationships) {
|
|
675
746
|
const violations = [];
|
|
@@ -732,70 +803,133 @@ function validateNoCycles(allPrincipleRelationships) {
|
|
|
732
803
|
return violations;
|
|
733
804
|
}
|
|
734
805
|
|
|
735
|
-
// lib/validation/
|
|
736
|
-
function
|
|
737
|
-
const
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
806
|
+
// lib/validation/validation-pipeline.ts
|
|
807
|
+
async function parseArtifacts(fileSystem, dustPath) {
|
|
808
|
+
const artifacts = new Map;
|
|
809
|
+
const byType = {
|
|
810
|
+
facts: [],
|
|
811
|
+
ideas: [],
|
|
812
|
+
principles: [],
|
|
813
|
+
tasks: []
|
|
814
|
+
};
|
|
815
|
+
const rootFiles = [];
|
|
816
|
+
const violations = [];
|
|
817
|
+
let rootEntries;
|
|
818
|
+
try {
|
|
819
|
+
rootEntries = await fileSystem.readdir(dustPath);
|
|
820
|
+
} catch (error) {
|
|
821
|
+
if (error.code === "ENOENT") {
|
|
822
|
+
rootEntries = [];
|
|
823
|
+
} else {
|
|
824
|
+
throw error;
|
|
744
825
|
}
|
|
745
826
|
}
|
|
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;
|
|
827
|
+
for (const entry of rootEntries) {
|
|
828
|
+
if (!entry.endsWith(".md"))
|
|
829
|
+
continue;
|
|
830
|
+
const filePath = `${dustPath}/${entry}`;
|
|
831
|
+
let content;
|
|
832
|
+
try {
|
|
833
|
+
content = await fileSystem.readFile(filePath);
|
|
834
|
+
} catch (error) {
|
|
835
|
+
if (error.code === "ENOENT") {
|
|
836
|
+
continue;
|
|
764
837
|
}
|
|
765
|
-
|
|
766
|
-
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
838
|
+
throw error;
|
|
839
|
+
}
|
|
840
|
+
const artifact = parseArtifact(filePath, content);
|
|
841
|
+
artifacts.set(filePath, artifact);
|
|
842
|
+
rootFiles.push(artifact);
|
|
843
|
+
}
|
|
844
|
+
for (const dir of ARTIFACT_TYPES) {
|
|
845
|
+
const dirPath = `${dustPath}/${dir}`;
|
|
846
|
+
violations.push(...await validateContentDirectoryFiles(dirPath, fileSystem));
|
|
847
|
+
let entries;
|
|
848
|
+
try {
|
|
849
|
+
entries = await fileSystem.readdir(dirPath);
|
|
850
|
+
} catch (error) {
|
|
851
|
+
if (error.code === "ENOENT") {
|
|
852
|
+
continue;
|
|
776
853
|
}
|
|
854
|
+
throw error;
|
|
855
|
+
}
|
|
856
|
+
for (const entry of entries) {
|
|
857
|
+
if (!entry.endsWith(".md"))
|
|
858
|
+
continue;
|
|
859
|
+
const filePath = `${dirPath}/${entry}`;
|
|
860
|
+
let content;
|
|
777
861
|
try {
|
|
778
|
-
|
|
779
|
-
for (const entry of baseEntries) {
|
|
780
|
-
const entryPath = `${path}/${entry}`;
|
|
781
|
-
if (!isDeleted(entryPath)) {
|
|
782
|
-
entries.add(entry);
|
|
783
|
-
}
|
|
784
|
-
}
|
|
862
|
+
content = await fileSystem.readFile(filePath);
|
|
785
863
|
} catch (error) {
|
|
786
|
-
if (error.code
|
|
787
|
-
|
|
864
|
+
if (error.code === "ENOENT") {
|
|
865
|
+
continue;
|
|
788
866
|
}
|
|
867
|
+
throw error;
|
|
789
868
|
}
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
if (isDeleted(path))
|
|
794
|
-
return false;
|
|
795
|
-
return patchDirs.has(path) || base.isDirectory(path);
|
|
869
|
+
const artifact = parseArtifact(filePath, content);
|
|
870
|
+
artifacts.set(filePath, artifact);
|
|
871
|
+
byType[dir].push(artifact);
|
|
796
872
|
}
|
|
873
|
+
}
|
|
874
|
+
return {
|
|
875
|
+
context: {
|
|
876
|
+
artifacts,
|
|
877
|
+
byType,
|
|
878
|
+
rootFiles,
|
|
879
|
+
dustPath,
|
|
880
|
+
fileSystem
|
|
881
|
+
},
|
|
882
|
+
violations
|
|
797
883
|
};
|
|
798
884
|
}
|
|
885
|
+
function validateArtifacts(context) {
|
|
886
|
+
const violations = [];
|
|
887
|
+
const { byType, rootFiles, dustPath, fileSystem } = context;
|
|
888
|
+
const ideasPath = `${dustPath}/ideas`;
|
|
889
|
+
for (const artifact of rootFiles) {
|
|
890
|
+
violations.push(...validateLinks(artifact, fileSystem));
|
|
891
|
+
}
|
|
892
|
+
for (const artifacts of Object.values(byType)) {
|
|
893
|
+
for (const artifact of artifacts) {
|
|
894
|
+
const openingSentenceViolation = validateOpeningSentence(artifact);
|
|
895
|
+
if (openingSentenceViolation)
|
|
896
|
+
violations.push(openingSentenceViolation);
|
|
897
|
+
const lengthViolation = validateOpeningSentenceLength(artifact);
|
|
898
|
+
if (lengthViolation)
|
|
899
|
+
violations.push(lengthViolation);
|
|
900
|
+
const titleViolation = validateTitleFilenameMatch(artifact);
|
|
901
|
+
if (titleViolation)
|
|
902
|
+
violations.push(titleViolation);
|
|
903
|
+
violations.push(...validateLinks(artifact, fileSystem));
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
for (const artifact of byType.ideas) {
|
|
907
|
+
violations.push(...validateIdeaOpenQuestions(artifact));
|
|
908
|
+
}
|
|
909
|
+
for (const artifact of byType.tasks) {
|
|
910
|
+
const filenameViolation = validateFilename(artifact.filePath);
|
|
911
|
+
if (filenameViolation)
|
|
912
|
+
violations.push(filenameViolation);
|
|
913
|
+
violations.push(...validateTaskHeadings(artifact));
|
|
914
|
+
violations.push(...validateSemanticLinks(artifact));
|
|
915
|
+
const imperativeViolation = validateImperativeOpeningSentence(artifact);
|
|
916
|
+
if (imperativeViolation)
|
|
917
|
+
violations.push(imperativeViolation);
|
|
918
|
+
const ideaTransitionViolation = validateIdeaTransitionTitle(artifact, ideasPath, fileSystem);
|
|
919
|
+
if (ideaTransitionViolation)
|
|
920
|
+
violations.push(ideaTransitionViolation);
|
|
921
|
+
violations.push(...validateWorkflowTaskBodySection(artifact, ideasPath, fileSystem));
|
|
922
|
+
}
|
|
923
|
+
const allPrincipleRelationships = [];
|
|
924
|
+
for (const artifact of byType.principles) {
|
|
925
|
+
violations.push(...validatePrincipleHierarchySections(artifact));
|
|
926
|
+
violations.push(...validatePrincipleHierarchyLinks(artifact));
|
|
927
|
+
allPrincipleRelationships.push(extractPrincipleRelationships(artifact));
|
|
928
|
+
}
|
|
929
|
+
violations.push(...validateBidirectionalLinks(allPrincipleRelationships));
|
|
930
|
+
violations.push(...validateNoCycles(allPrincipleRelationships));
|
|
931
|
+
return violations;
|
|
932
|
+
}
|
|
799
933
|
|
|
800
934
|
// lib/validation/index.ts
|
|
801
935
|
var ALLOWED_ROOT_DIRECTORIES = [
|
|
@@ -855,36 +989,6 @@ function relativizeViolations(violations, cwd) {
|
|
|
855
989
|
file: relativizeViolationFilePath(violation.file, cwd)
|
|
856
990
|
}));
|
|
857
991
|
}
|
|
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
992
|
function parsePatchFiles(dustPath, patch) {
|
|
889
993
|
const absolutePatchFiles = new Map;
|
|
890
994
|
const deletedPaths = new Set;
|
|
@@ -898,75 +1002,15 @@ function parsePatchFiles(dustPath, patch) {
|
|
|
898
1002
|
}
|
|
899
1003
|
return { absolutePatchFiles, deletedPaths };
|
|
900
1004
|
}
|
|
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
1005
|
async function validatePatch(fileSystem, dustPath, patch, options = {}) {
|
|
934
1006
|
const cwd = options.cwd ?? process.cwd();
|
|
935
1007
|
const { absolutePatchFiles, deletedPaths } = parsePatchFiles(dustPath, patch);
|
|
936
1008
|
const overlayFs = createOverlayFileSystem(fileSystem, absolutePatchFiles, deletedPaths);
|
|
937
1009
|
const violations = [];
|
|
938
1010
|
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
|
-
}
|
|
1011
|
+
const { context, violations: parseViolations } = await parseArtifacts(overlayFs, dustPath);
|
|
1012
|
+
violations.push(...parseViolations);
|
|
1013
|
+
violations.push(...validateArtifacts(context));
|
|
970
1014
|
return {
|
|
971
1015
|
valid: violations.length === 0,
|
|
972
1016
|
violations: relativizeViolations(violations, cwd)
|