@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.
@@ -14,6 +14,11 @@ export interface Idea {
14
14
  content: string;
15
15
  openQuestions: IdeaOpenQuestion[];
16
16
  }
17
+ export interface ParsedIdeaContent {
18
+ title: string | null;
19
+ body: string;
20
+ openQuestions: IdeaOpenQuestion[];
21
+ }
17
22
  /**
18
23
  * Parses the ## Open Questions section from idea markdown content.
19
24
  * Extracts each ### question heading and its #### option children.
@@ -23,3 +28,15 @@ export declare function parseOpenQuestions(content: string): IdeaOpenQuestion[];
23
28
  * Parses an idea markdown file into a structured Idea object.
24
29
  */
25
30
  export declare function parseIdea(fileSystem: ReadableFileSystem, dustPath: string, slug: string): Promise<Idea>;
31
+ /**
32
+ * Parses idea markdown into a structured object that can be bound to a UI
33
+ * and serialized back to markdown.
34
+ */
35
+ export declare function parseIdeaContent(markdown: string): ParsedIdeaContent;
36
+ /**
37
+ * Serializes a ParsedIdeaContent back to markdown.
38
+ * Open Questions are appended as the last section.
39
+ */
40
+ export declare function ideaContentToMarkdown(content: ParsedIdeaContent, options?: {
41
+ includeOpenQuestions?: boolean;
42
+ }): string;
@@ -1,10 +1,11 @@
1
1
  import type { FileSystem, ReadableFileSystem } from '../filesystem/types';
2
2
  import { type Fact } from './facts';
3
- import { type Idea, type IdeaOpenQuestion, type IdeaOption, parseOpenQuestions } from './ideas';
3
+ import { type Idea, type IdeaOpenQuestion, type IdeaOption, type ParsedIdeaContent, ideaContentToMarkdown, parseIdeaContent, parseOpenQuestions } from './ideas';
4
+ import { extractTitle } from '../markdown/markdown-utilities';
4
5
  import { type Principle } from './principles';
5
6
  import { type Task } from './tasks';
6
7
  import { type AllWorkflowTasks, CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllWorkflowTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, parseResolvedQuestions, type WorkflowTaskMatch, type WorkflowTaskType } from './workflow-tasks';
7
- export type { AllWorkflowTasks, CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, WorkflowTaskType, };
8
+ export type { AllWorkflowTasks, CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, ParsedIdeaContent, Principle, Task, WorkflowTaskMatch, WorkflowTaskType, };
8
9
  export interface TaskGraphNode {
9
10
  task: Task;
10
11
  workflowType: WorkflowTaskType | null;
@@ -16,7 +17,7 @@ export interface TaskGraph {
16
17
  to: string;
17
18
  }>;
18
19
  }
19
- export { CAPTURE_IDEA_PREFIX, findAllWorkflowTasks, parseOpenQuestions, parseResolvedQuestions, };
20
+ export { CAPTURE_IDEA_PREFIX, extractTitle, findAllWorkflowTasks, ideaContentToMarkdown, parseIdeaContent, parseOpenQuestions, parseResolvedQuestions, };
20
21
  export type { IdeaInProgress };
21
22
  export type ArtifactType = 'ideas' | 'tasks' | 'principles' | 'facts';
22
23
  export interface ReadOnlyArtifactsRepository {
package/dist/artifacts.js CHANGED
@@ -4,50 +4,68 @@ function extractTitle(content) {
4
4
  return match ? match[1].trim() : null;
5
5
  }
6
6
  var MARKDOWN_LINK_PATTERN = /\[([^\]]+)\]\(([^)]+)\)/;
7
- function extractOpeningSentence(content) {
8
- const lines = content.split(`
9
- `);
10
- let h1Index = -1;
7
+ function findH1Index(lines) {
11
8
  for (let i = 0;i < lines.length; i++) {
12
9
  if (lines[i].match(/^#\s+.+$/)) {
13
- h1Index = i;
14
- break;
10
+ return i;
15
11
  }
16
12
  }
17
- if (h1Index === -1) {
18
- return null;
13
+ return -1;
14
+ }
15
+ function findFirstNonBlankLineAfter(lines, startIndex) {
16
+ for (let i = startIndex + 1;i < lines.length; i++) {
17
+ if (lines[i].trim() !== "") {
18
+ return i;
19
+ }
20
+ }
21
+ return -1;
22
+ }
23
+ var LIST_ITEM_PREFIXES = ["-", "*", "+"];
24
+ var STRUCTURAL_PREFIXES = ["#", "```", ">"];
25
+ function isStructuralElement(line) {
26
+ if (STRUCTURAL_PREFIXES.some((prefix) => line.startsWith(prefix))) {
27
+ return true;
28
+ }
29
+ if (LIST_ITEM_PREFIXES.some((prefix) => line.startsWith(prefix))) {
30
+ return true;
19
31
  }
20
- let paragraphStart = -1;
21
- for (let i = h1Index + 1;i < lines.length; i++) {
32
+ return /^\d+\./.test(line);
33
+ }
34
+ function isBlockBreak(line) {
35
+ return STRUCTURAL_PREFIXES.some((prefix) => line.startsWith(prefix));
36
+ }
37
+ function collectParagraph(lines, startIndex) {
38
+ const parts = [];
39
+ for (let i = startIndex;i < lines.length; i++) {
22
40
  const line = lines[i].trim();
23
- if (line !== "") {
24
- paragraphStart = i;
41
+ if (line === "" || isBlockBreak(line)) {
25
42
  break;
26
43
  }
44
+ parts.push(line);
27
45
  }
28
- if (paragraphStart === -1) {
46
+ return parts.join(" ");
47
+ }
48
+ function extractFirstSentence(paragraph) {
49
+ const match = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
50
+ return match ? match[1] : null;
51
+ }
52
+ function extractOpeningSentence(content) {
53
+ const lines = content.split(`
54
+ `);
55
+ const h1Index = findH1Index(lines);
56
+ if (h1Index === -1) {
29
57
  return null;
30
58
  }
31
- const firstLine = lines[paragraphStart];
32
- const trimmedFirstLine = firstLine.trim();
33
- if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
59
+ const paragraphStart = findFirstNonBlankLineAfter(lines, h1Index);
60
+ if (paragraphStart === -1) {
34
61
  return null;
35
62
  }
36
- let paragraph = "";
37
- for (let i = paragraphStart;i < lines.length; i++) {
38
- const line = lines[i].trim();
39
- if (line === "")
40
- break;
41
- if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
42
- break;
43
- }
44
- paragraph += (paragraph ? " " : "") + line;
45
- }
46
- const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
47
- if (!sentenceMatch) {
63
+ const trimmedFirstLine = lines[paragraphStart].trim();
64
+ if (isStructuralElement(trimmedFirstLine)) {
48
65
  return null;
49
66
  }
50
- return sentenceMatch[1];
67
+ const paragraph = collectParagraph(lines, paragraphStart);
68
+ return extractFirstSentence(paragraph);
51
69
  }
52
70
 
53
71
  // lib/artifacts/facts.ts
@@ -162,6 +180,83 @@ async function parseIdea(fileSystem, dustPath, slug) {
162
180
  openQuestions
163
181
  };
164
182
  }
183
+ function stripOpenQuestionsSection(content) {
184
+ const lines = content.split(`
185
+ `);
186
+ const result = [];
187
+ let inOpenQuestions = false;
188
+ let inCodeFence = false;
189
+ for (const line of lines) {
190
+ if (line.startsWith("```")) {
191
+ inCodeFence = !inCodeFence;
192
+ if (!inOpenQuestions)
193
+ result.push(line);
194
+ continue;
195
+ }
196
+ if (inCodeFence) {
197
+ if (!inOpenQuestions)
198
+ result.push(line);
199
+ continue;
200
+ }
201
+ if (line.startsWith("## ")) {
202
+ inOpenQuestions = line.trimEnd() === "## Open Questions";
203
+ if (!inOpenQuestions)
204
+ result.push(line);
205
+ continue;
206
+ }
207
+ if (!inOpenQuestions) {
208
+ result.push(line);
209
+ }
210
+ }
211
+ while (result.length > 0 && result[result.length - 1].trim() === "") {
212
+ result.pop();
213
+ }
214
+ return result.join(`
215
+ `) + `
216
+ `;
217
+ }
218
+ function stripTitle(content) {
219
+ const match = content.match(/^#\s+.+\n+/);
220
+ if (!match)
221
+ return content;
222
+ return content.slice(match[0].length);
223
+ }
224
+ function parseIdeaContent(markdown) {
225
+ const title = extractTitle(markdown);
226
+ const openQuestions = parseOpenQuestions(markdown);
227
+ const body = stripTitle(stripOpenQuestionsSection(markdown));
228
+ return { title, body, openQuestions };
229
+ }
230
+ function ideaContentToMarkdown(content, options) {
231
+ const includeOQ = options?.includeOpenQuestions ?? true;
232
+ const parts = [];
233
+ if (content.title) {
234
+ parts.push(`# ${content.title}`);
235
+ parts.push("");
236
+ }
237
+ if (content.body) {
238
+ parts.push(content.body.trimEnd());
239
+ parts.push("");
240
+ }
241
+ if (includeOQ && content.openQuestions.length > 0) {
242
+ parts.push("## Open Questions");
243
+ parts.push("");
244
+ for (const q of content.openQuestions) {
245
+ parts.push(`### ${q.question}`);
246
+ parts.push("");
247
+ for (const o of q.options) {
248
+ parts.push(`#### ${o.name}`);
249
+ parts.push("");
250
+ if (o.description) {
251
+ parts.push(o.description);
252
+ parts.push("");
253
+ }
254
+ }
255
+ }
256
+ }
257
+ return parts.join(`
258
+ `);
259
+ }
165
260
 
166
261
  // lib/artifacts/principles.ts
167
262
  function extractLinksFromSection(content, sectionHeading) {
@@ -765,7 +860,10 @@ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
765
860
  export {
766
861
  parseResolvedQuestions,
767
862
  parseOpenQuestions,
863
+ parseIdeaContent,
864
+ ideaContentToMarkdown,
768
865
  findAllWorkflowTasks,
866
+ extractTitle,
769
867
  buildReadOnlyArtifactsRepository,
770
868
  buildArtifactsRepository,
771
869
  CAPTURE_IDEA_PREFIX
package/dist/audits.js CHANGED
@@ -6,50 +6,68 @@ function extractTitle(content) {
6
6
  const match = content.match(/^#\s+(.+)$/m);
7
7
  return match ? match[1].trim() : null;
8
8
  }
9
- function extractOpeningSentence(content) {
10
- const lines = content.split(`
11
- `);
12
- let h1Index = -1;
9
+ function findH1Index(lines) {
13
10
  for (let i = 0;i < lines.length; i++) {
14
11
  if (lines[i].match(/^#\s+.+$/)) {
15
- h1Index = i;
16
- break;
12
+ return i;
17
13
  }
18
14
  }
19
- if (h1Index === -1) {
20
- return null;
15
+ return -1;
16
+ }
17
+ function findFirstNonBlankLineAfter(lines, startIndex) {
18
+ for (let i = startIndex + 1;i < lines.length; i++) {
19
+ if (lines[i].trim() !== "") {
20
+ return i;
21
+ }
21
22
  }
22
- let paragraphStart = -1;
23
- for (let i = h1Index + 1;i < lines.length; i++) {
23
+ return -1;
24
+ }
25
+ var LIST_ITEM_PREFIXES = ["-", "*", "+"];
26
+ var STRUCTURAL_PREFIXES = ["#", "```", ">"];
27
+ function isStructuralElement(line) {
28
+ if (STRUCTURAL_PREFIXES.some((prefix) => line.startsWith(prefix))) {
29
+ return true;
30
+ }
31
+ if (LIST_ITEM_PREFIXES.some((prefix) => line.startsWith(prefix))) {
32
+ return true;
33
+ }
34
+ return /^\d+\./.test(line);
35
+ }
36
+ function isBlockBreak(line) {
37
+ return STRUCTURAL_PREFIXES.some((prefix) => line.startsWith(prefix));
38
+ }
39
+ function collectParagraph(lines, startIndex) {
40
+ const parts = [];
41
+ for (let i = startIndex;i < lines.length; i++) {
24
42
  const line = lines[i].trim();
25
- if (line !== "") {
26
- paragraphStart = i;
43
+ if (line === "" || isBlockBreak(line)) {
27
44
  break;
28
45
  }
46
+ parts.push(line);
29
47
  }
30
- if (paragraphStart === -1) {
48
+ return parts.join(" ");
49
+ }
50
+ function extractFirstSentence(paragraph) {
51
+ const match = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
52
+ return match ? match[1] : null;
53
+ }
54
+ function extractOpeningSentence(content) {
55
+ const lines = content.split(`
56
+ `);
57
+ const h1Index = findH1Index(lines);
58
+ if (h1Index === -1) {
31
59
  return null;
32
60
  }
33
- const firstLine = lines[paragraphStart];
34
- const trimmedFirstLine = firstLine.trim();
35
- if (trimmedFirstLine.startsWith("#") || trimmedFirstLine.startsWith("-") || trimmedFirstLine.startsWith("*") || trimmedFirstLine.startsWith("+") || trimmedFirstLine.match(/^\d+\./) || trimmedFirstLine.startsWith("```") || trimmedFirstLine.startsWith(">")) {
61
+ const paragraphStart = findFirstNonBlankLineAfter(lines, h1Index);
62
+ if (paragraphStart === -1) {
36
63
  return null;
37
64
  }
38
- let paragraph = "";
39
- for (let i = paragraphStart;i < lines.length; i++) {
40
- const line = lines[i].trim();
41
- if (line === "")
42
- break;
43
- if (line.startsWith("#") || line.startsWith("```") || line.startsWith(">")) {
44
- break;
45
- }
46
- paragraph += (paragraph ? " " : "") + line;
47
- }
48
- const sentenceMatch = paragraph.match(/^(.+?[.?!])(?:\s|$)/);
49
- if (!sentenceMatch) {
65
+ const trimmedFirstLine = lines[paragraphStart].trim();
66
+ if (isStructuralElement(trimmedFirstLine)) {
50
67
  return null;
51
68
  }
52
- return sentenceMatch[1];
69
+ const paragraph = collectParagraph(lines, paragraphStart);
70
+ return extractFirstSentence(paragraph);
53
71
  }
54
72
 
55
73
  // lib/cli/dedent.ts
@@ -214,19 +232,33 @@ function coverageExclusions() {
214
232
  return dedent`
215
233
  # Coverage Exclusions
216
234
 
217
- Review coverage exclusion configuration to identify opportunities for removal through refactoring.
235
+ Audit all coverage exclusions to identify opportunities for removal through refactoring.
218
236
 
219
237
  ${ideasHint}
220
238
 
221
239
  ## Scope
222
240
 
223
- Focus on these areas:
241
+ Search for exclusions in both configuration and source code:
242
+
243
+ 1. **Configuration-level exclusions** - Review test framework configuration for file/directory exclusion patterns
244
+ 2. **Inline escape directives** - Search for comments that exclude code from coverage (e.g., ignore directives in source files)
245
+ 3. **Test files** - Include test code in the search; inline directives may hide flaky or unclear test logic
246
+
247
+ ## Analysis
248
+
249
+ For each exclusion found:
224
250
 
225
- 1. **Current exclusions** - Review all exclusions in \`vitest.config.ts\` or equivalent
226
- 2. **Justification** - Is each exclusion still necessary?
227
- 3. **Tooling limitations** - Can workarounds be found for coverage tool issues?
228
- 4. **Decoupling opportunities** - Can excluded code be restructured to enable testing?
229
- 5. **Entry point patterns** - Can hard-to-test entry points be decoupled from logic?
251
+ 1. **Document the location** - File path and line number (or config section)
252
+ 2. **Identify the reason** - Why was this exclusion added?
253
+ 3. **Categorize by justification**:
254
+ - Native wrapper (code that wraps platform APIs with no testable logic)
255
+ - Defensive guard (unreachable error handling for type safety)
256
+ - Integration boundary (code that requires external systems)
257
+ - Tooling limitation (coverage tool bug or limitation)
258
+ - Technical debt (code that should be testable but isn't)
259
+ - Unknown (no clear justification found)
260
+ 4. **Label justification quality** - Is the justification well-documented, reasonable, or questionable?
261
+ 5. **Evaluate removal potential** - Can the exclusion be removed through decoupling or refactoring?
230
262
 
231
263
  ## Principles
232
264
 
@@ -241,9 +273,11 @@ function coverageExclusions() {
241
273
 
242
274
  ## Definition of Done
243
275
 
244
- - Identified all coverage exclusions in the project
276
+ - Identified all configuration-level coverage exclusions
277
+ - Searched source and test files for inline escape directives
245
278
  - Documented the reason each exclusion exists
246
- - Evaluated whether each exclusion is still necessary
279
+ - Categorized each exclusion by justification type
280
+ - Labeled justification quality for visibility
247
281
  - Identified exclusions that could be removed through decoupling
248
282
  - Proposed ideas for refactoring where feasible
249
283
  `;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Repository lifecycle state type for dust bucket.
3
+ *
4
+ * Represents all valid states in the repository lifecycle as a discriminated union,
5
+ * making invalid state combinations unrepresentable.
6
+ */
7
+ export type RepositoryLifecycleState = {
8
+ type: 'idle';
9
+ } | {
10
+ type: 'starting';
11
+ } | {
12
+ type: 'running';
13
+ loopPromise: Promise<void>;
14
+ cancel: () => void;
15
+ } | {
16
+ type: 'stopping';
17
+ } | {
18
+ type: 'stopped';
19
+ };
20
+ export type LifecycleAction = {
21
+ type: 'start';
22
+ } | {
23
+ type: 'started';
24
+ loopPromise: Promise<void>;
25
+ cancel: () => void;
26
+ } | {
27
+ type: 'stop';
28
+ } | {
29
+ type: 'stopped';
30
+ };
31
+ export type TransitionResult = {
32
+ ok: true;
33
+ state: RepositoryLifecycleState;
34
+ } | {
35
+ ok: false;
36
+ error: string;
37
+ };
38
+ export declare function transition(current: RepositoryLifecycleState, action: LifecycleAction): TransitionResult;
@@ -12,9 +12,11 @@ import { type AuthConfig, type RuntimeConfig, type SessionConfig } from '../env-
12
12
  import type { ToolExecutionRequest, ToolExecutionResult } from './command-events-proxy';
13
13
  import { type BucketEmitFn, type SendEventFn } from './events';
14
14
  import { type LogBuffer } from './log-buffer';
15
+ import { type RepositoryLifecycleState } from './repository-lifecycle';
15
16
  import type { ToolDefinition } from './server-messages';
16
17
  export { cloneRepository, getRepoPath, removeRepository, } from './repository-git';
17
18
  export { runRepositoryLoop } from './repository-loop';
19
+ export type { RepositoryLifecycleState } from './repository-lifecycle';
18
20
  export interface Repository {
19
21
  name: string;
20
22
  gitUrl: string;
@@ -26,13 +28,11 @@ export interface Repository {
26
28
  export interface RepositoryState {
27
29
  repository: Repository;
28
30
  path: string;
29
- loopPromise: Promise<void> | null;
30
- stopRequested: boolean;
31
31
  logBuffer: LogBuffer;
32
+ lifecycle: RepositoryLifecycleState;
32
33
  agentStatus: 'idle' | 'busy';
33
34
  wakeUp?: () => void;
34
35
  taskAvailablePending?: boolean;
35
- cancelCurrentIteration?: () => void;
36
36
  }
37
37
  /**
38
38
  * Interface for the subset of bucket state needed by repository management.
@@ -66,7 +66,7 @@ export interface RepositoryDependencies {
66
66
  revealFamily?: (familyName: string) => void;
67
67
  }
68
68
  /**
69
- * Start (or restart) the per-repository loop and keep loopPromise state accurate.
69
+ * Start (or restart) the per-repository loop and keep lifecycle state accurate.
70
70
  */
71
71
  export declare function startRepositoryLoop(repoState: RepositoryState, repoDeps: RepositoryDependencies, sendEvent?: SendEventFn, sessionId?: string): void;
72
72
  export declare function createDefaultRepositoryDependencies(fileSystem: FileSystem): RepositoryDependencies;