@joshski/dust 0.1.96 → 0.1.97

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.
@@ -1,9 +1,9 @@
1
1
  import type { spawn as nodeSpawn } from 'node:child_process';
2
- type GitPullResult = {
2
+ type PullResult = {
3
3
  success: true;
4
4
  } | {
5
5
  success: false;
6
6
  message: string;
7
7
  };
8
- export declare function gitPull(cwd: string, spawn: typeof nodeSpawn): Promise<GitPullResult>;
8
+ export declare function gitPull(cwd: string, spawn: typeof nodeSpawn): Promise<PullResult>;
9
9
  export {};
package/dist/types.d.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  * the event protocol, workflow tasks, and idea structures.
6
6
  */
7
7
  export type { AgentSessionEvent, EventMessage } from './agent-events';
8
- export type { Idea, IdeaOpenQuestion, IdeaOption } from './artifacts/ideas';
8
+ export type { Idea, IdeaOpenQuestion, IdeaOption, ParsedIdeaContent, } from './artifacts/ideas';
9
9
  export type { ArtifactType, TaskGraph, TaskGraphNode } from './artifacts/index';
10
10
  export type { Task } from './artifacts/tasks';
11
11
  export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, IdeaInProgress, OpenQuestionResponse, ParsedCaptureIdeaTask, WorkflowTaskMatch, WorkflowTaskType, } from './artifacts/workflow-tasks';
@@ -7,50 +7,68 @@ function extractTitle(content) {
7
7
  return match ? match[1].trim() : null;
8
8
  }
9
9
  var MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/;
10
- function extractOpeningSentence(content) {
11
- const lines = content.split(`
12
- `);
13
- let h1Index = -1;
10
+ function findH1Index(lines) {
14
11
  for (let i = 0;i < lines.length; i++) {
15
12
  if (lines[i].match(/^#\s+.+$/)) {
16
- h1Index = i;
17
- break;
13
+ return i;
18
14
  }
19
15
  }
20
- if (h1Index === -1) {
21
- return null;
16
+ return -1;
17
+ }
18
+ function findFirstNonBlankLineAfter(lines, startIndex) {
19
+ for (let i = startIndex + 1;i < lines.length; i++) {
20
+ if (lines[i].trim() !== "") {
21
+ return i;
22
+ }
23
+ }
24
+ return -1;
25
+ }
26
+ var LIST_ITEM_PREFIXES = ["-", "*", "+"];
27
+ var STRUCTURAL_PREFIXES = ["#", "```", ">"];
28
+ function isStructuralElement(line) {
29
+ if (STRUCTURAL_PREFIXES.some((prefix) => line.startsWith(prefix))) {
30
+ return true;
22
31
  }
23
- let paragraphStart = -1;
24
- for (let i = h1Index + 1;i < lines.length; i++) {
32
+ if (LIST_ITEM_PREFIXES.some((prefix) => line.startsWith(prefix))) {
33
+ return true;
34
+ }
35
+ return /^\d+\./.test(line);
36
+ }
37
+ function isBlockBreak(line) {
38
+ return STRUCTURAL_PREFIXES.some((prefix) => line.startsWith(prefix));
39
+ }
40
+ function collectParagraph(lines, startIndex) {
41
+ const parts = [];
42
+ for (let i = startIndex;i < lines.length; i++) {
25
43
  const line = lines[i].trim();
26
- if (line !== "") {
27
- paragraphStart = i;
44
+ if (line === "" || isBlockBreak(line)) {
28
45
  break;
29
46
  }
47
+ parts.push(line);
30
48
  }
31
- if (paragraphStart === -1) {
49
+ return parts.join(" ");
50
+ }
51
+ function extractFirstSentence(paragraph) {
52
+ const match = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
53
+ return match ? match[1] : null;
54
+ }
55
+ function extractOpeningSentence(content) {
56
+ const lines = content.split(`
57
+ `);
58
+ const h1Index = findH1Index(lines);
59
+ if (h1Index === -1) {
32
60
  return null;
33
61
  }
34
- const firstLine = lines[paragraphStart];
35
- const trimmedFirstLine = firstLine.trim();
36
- if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
62
+ const paragraphStart = findFirstNonBlankLineAfter(lines, h1Index);
63
+ if (paragraphStart === -1) {
37
64
  return null;
38
65
  }
39
- let paragraph = "";
40
- for (let i = paragraphStart;i < lines.length; i++) {
41
- const line = lines[i].trim();
42
- if (line === "")
43
- break;
44
- if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
45
- break;
46
- }
47
- paragraph += (paragraph ? " " : "") + line;
48
- }
49
- const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
50
- if (!sentenceMatch) {
66
+ const trimmedFirstLine = lines[paragraphStart].trim();
67
+ if (isStructuralElement(trimmedFirstLine)) {
51
68
  return null;
52
69
  }
53
- return sentenceMatch[1];
70
+ const paragraph = collectParagraph(lines, paragraphStart);
71
+ return extractFirstSentence(paragraph);
54
72
  }
55
73
 
56
74
  // lib/lint/validators/content-validator.ts
@@ -208,6 +226,32 @@ var WORKFLOW_PREFIX_TO_SECTION = {
208
226
  "Shelve Idea: ": "Shelves Idea",
209
227
  "Expedite Idea: ": "Expedites Idea"
210
228
  };
229
+ function validateH2Heading(filePath, line, lineNumber, inOpenQuestions, currentQuestionLine) {
230
+ const violations = [];
231
+ if (inOpenQuestions && currentQuestionLine !== null) {
232
+ violations.push({
233
+ file: filePath,
234
+ message: "Question has no options listed beneath it",
235
+ line: currentQuestionLine
236
+ });
237
+ }
238
+ if (inOpenQuestions && line !== "## Open Questions") {
239
+ violations.push({
240
+ file: filePath,
241
+ message: "Open Questions must be the last section in an idea file. Move this section above ## Open Questions.",
242
+ line: lineNumber
243
+ });
244
+ }
245
+ const headingText = line.slice(3).trimEnd();
246
+ if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
247
+ violations.push({
248
+ file: filePath,
249
+ message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
250
+ line: lineNumber
251
+ });
252
+ }
253
+ return violations;
254
+ }
211
255
  function validateIdeaOpenQuestions(filePath, content) {
212
256
  const violations = [];
213
257
  const lines = content.split(`
@@ -221,22 +265,21 @@ function validateIdeaOpenQuestions(filePath, content) {
221
265
  const line = lines[i];
222
266
  const trimmedLine = line.trimEnd();
223
267
  const nonWhitespaceLine = line.trim();
224
- if (line.startsWith("## ")) {
225
- if (inOpenQuestions && currentQuestionLine !== null) {
226
- violations.push({
227
- file: filePath,
228
- message: "Question has no options listed beneath it",
229
- line: currentQuestionLine
230
- });
231
- }
232
- const headingText = line.slice(3).trimEnd();
233
- if (headingText.toLowerCase() === "open questions" && headingText !== "Open Questions") {
268
+ if (inOpenQuestions && line.startsWith("```")) {
269
+ if (!inOption && !inCodeBlock) {
234
270
  violations.push({
235
271
  file: filePath,
236
- message: `Heading "${line.trimEnd()}" should be "## Open Questions"`,
272
+ message: topLevelStructureMessage,
237
273
  line: i + 1
238
274
  });
239
275
  }
276
+ inCodeBlock = !inCodeBlock;
277
+ continue;
278
+ }
279
+ if (inCodeBlock)
280
+ continue;
281
+ if (line.startsWith("## ")) {
282
+ violations.push(...validateH2Heading(filePath, line, i + 1, inOpenQuestions, currentQuestionLine));
240
283
  inOpenQuestions = line === "## Open Questions";
241
284
  currentQuestionLine = null;
242
285
  inOption = false;
@@ -245,19 +288,6 @@ function validateIdeaOpenQuestions(filePath, content) {
245
288
  }
246
289
  if (!inOpenQuestions)
247
290
  continue;
248
- if (line.startsWith("```")) {
249
- if (!inOption && !inCodeBlock) {
250
- violations.push({
251
- file: filePath,
252
- message: topLevelStructureMessage,
253
- line: i + 1
254
- });
255
- }
256
- inCodeBlock = !inCodeBlock;
257
- continue;
258
- }
259
- if (inCodeBlock)
260
- continue;
261
291
  if (line.startsWith("### ")) {
262
292
  inOption = false;
263
293
  if (currentQuestionLine !== null) {
@@ -825,8 +855,37 @@ function relativizeViolations(violations, cwd) {
825
855
  file: relativizeViolationFilePath(violation.file, cwd)
826
856
  }));
827
857
  }
828
- async function validatePatch(fileSystem, dustPath, patch, options = {}) {
829
- const cwd = options.cwd ?? process.cwd();
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
+ function parsePatchFiles(dustPath, patch) {
830
889
  const absolutePatchFiles = new Map;
831
890
  const deletedPaths = new Set;
832
891
  for (const [relativePath, content] of Object.entries(patch.files)) {
@@ -837,20 +896,51 @@ async function validatePatch(fileSystem, dustPath, patch, options = {}) {
837
896
  absolutePatchFiles.set(absolutePath, content);
838
897
  }
839
898
  }
840
- const overlayFs = createOverlayFileSystem(fileSystem, absolutePatchFiles, deletedPaths);
841
- const violations = [];
842
- violations.push(...validatePatchRootEntries(fileSystem, dustPath, patch));
843
- const contentDirs = ["principles", "facts", "ideas", "tasks"];
899
+ return { absolutePatchFiles, deletedPaths };
900
+ }
901
+ function collectPatchDirs(patch) {
844
902
  const patchDirs = new Set;
845
903
  for (const relativePath of Object.keys(patch.files)) {
846
904
  const dir = relativePath.split("/")[0];
847
- if (contentDirs.includes(dir)) {
905
+ if (CONTENT_DIRS.includes(dir)) {
848
906
  patchDirs.add(dir);
849
907
  }
850
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
+ async function validatePatch(fileSystem, dustPath, patch, options = {}) {
934
+ const cwd = options.cwd ?? process.cwd();
935
+ const { absolutePatchFiles, deletedPaths } = parsePatchFiles(dustPath, patch);
936
+ const overlayFs = createOverlayFileSystem(fileSystem, absolutePatchFiles, deletedPaths);
937
+ const violations = [];
938
+ violations.push(...validatePatchRootEntries(fileSystem, dustPath, patch));
939
+ const patchDirs = collectPatchDirs(patch);
851
940
  for (const dir of patchDirs) {
852
941
  violations.push(...await validateContentDirectoryFiles(`${dustPath}/${dir}`, overlayFs));
853
942
  }
943
+ const ideasPath = `${dustPath}/ideas`;
854
944
  for (const [relativePath, content] of Object.entries(patch.files)) {
855
945
  if (content === null)
856
946
  continue;
@@ -859,34 +949,14 @@ async function validatePatch(fileSystem, dustPath, patch, options = {}) {
859
949
  const filePath = `${dustPath}/${relativePath}`;
860
950
  const dir = relativePath.split("/")[0];
861
951
  violations.push(...validateLinks(filePath, content, overlayFs));
862
- if (contentDirs.includes(dir)) {
863
- const openingSentence = validateOpeningSentence(filePath, content);
864
- if (openingSentence)
865
- violations.push(openingSentence);
866
- const openingSentenceLength = validateOpeningSentenceLength(filePath, content);
867
- if (openingSentenceLength)
868
- violations.push(openingSentenceLength);
869
- const titleFilename = validateTitleFilenameMatch(filePath, content);
870
- if (titleFilename)
871
- violations.push(titleFilename);
952
+ if (CONTENT_DIRS.includes(dir)) {
953
+ violations.push(...validateContentFile(filePath, content));
872
954
  }
873
955
  if (dir === "ideas") {
874
956
  violations.push(...validateIdeaOpenQuestions(filePath, content));
875
957
  }
876
958
  if (dir === "tasks") {
877
- const filenameViolation = validateFilename(filePath);
878
- if (filenameViolation)
879
- violations.push(filenameViolation);
880
- violations.push(...validateTaskHeadings(filePath, content));
881
- violations.push(...validateSemanticLinks(filePath, content));
882
- const imperativeViolation = validateImperativeOpeningSentence(filePath, content);
883
- if (imperativeViolation)
884
- violations.push(imperativeViolation);
885
- const ideasPath = `${dustPath}/ideas`;
886
- const ideaTransition = validateIdeaTransitionTitle(filePath, content, ideasPath, overlayFs);
887
- if (ideaTransition)
888
- violations.push(ideaTransition);
889
- violations.push(...validateWorkflowTaskBodySection(filePath, content, ideasPath, overlayFs));
959
+ violations.push(...validateTaskFile(filePath, content, ideasPath, overlayFs));
890
960
  }
891
961
  if (dir === "principles") {
892
962
  violations.push(...validatePrincipleHierarchySections(filePath, content));
@@ -895,24 +965,7 @@ async function validatePatch(fileSystem, dustPath, patch, options = {}) {
895
965
  }
896
966
  const hasPrinciplePatches = Object.keys(patch.files).some((p) => p.startsWith("principles/"));
897
967
  if (hasPrinciplePatches) {
898
- const allRelationships = [];
899
- const principlesPath = `${dustPath}/principles`;
900
- try {
901
- const existingFiles = await overlayFs.readdir(principlesPath);
902
- for (const file of existingFiles) {
903
- if (!file.endsWith(".md"))
904
- continue;
905
- const filePath = `${principlesPath}/${file}`;
906
- const content = await overlayFs.readFile(filePath);
907
- allRelationships.push(extractPrincipleRelationships(filePath, content));
908
- }
909
- } catch (error) {
910
- if (error.code !== "ENOENT") {
911
- throw error;
912
- }
913
- }
914
- violations.push(...validateBidirectionalLinks(allRelationships));
915
- violations.push(...validateNoCycles(allRelationships));
968
+ violations.push(...await validatePrincipleRelationships(dustPath, overlayFs));
916
969
  }
917
970
  return {
918
971
  valid: violations.length === 0,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@joshski/dust",
3
- "version": "0.1.96",
3
+ "version": "0.1.97",
4
4
  "description": "Flow state for AI coding agents",
5
5
  "type": "module",
6
6
  "bin": {