@joshski/dust 0.1.64 → 0.1.67
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/biome/dust-no-abbreviated-names.grit +58 -0
- package/dist/artifacts.js +68 -36
- package/dist/audits/stock-audits.d.ts +2 -1
- package/dist/audits.js +65 -9
- package/dist/biome/index.d.ts +16 -0
- package/dist/biome.js +8 -0
- package/dist/cli/types.d.ts +1 -1
- package/dist/dust.js +212 -26
- package/package.json +10 -6
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
language js
|
|
2
|
+
|
|
3
|
+
or {
|
|
4
|
+
`ctx` where {
|
|
5
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'ctx'. Use 'context' instead.")
|
|
6
|
+
},
|
|
7
|
+
`deps` where {
|
|
8
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'deps'. Use 'dependencies' instead.")
|
|
9
|
+
},
|
|
10
|
+
`fs` where {
|
|
11
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'fs'. Use 'fileSystem' instead.")
|
|
12
|
+
},
|
|
13
|
+
`args` where {
|
|
14
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'args'. Use 'arguments' instead.")
|
|
15
|
+
},
|
|
16
|
+
`req` where {
|
|
17
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'req'. Use 'request' instead.")
|
|
18
|
+
},
|
|
19
|
+
`res` where {
|
|
20
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'res'. Use 'response' instead.")
|
|
21
|
+
},
|
|
22
|
+
`err` where {
|
|
23
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'err'. Use 'error' instead.")
|
|
24
|
+
},
|
|
25
|
+
`cb` where {
|
|
26
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'cb'. Use 'callback' instead.")
|
|
27
|
+
},
|
|
28
|
+
`fn` where {
|
|
29
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'fn'. Use a descriptive name instead.")
|
|
30
|
+
},
|
|
31
|
+
`opts` where {
|
|
32
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'opts'. Use 'options' instead.")
|
|
33
|
+
},
|
|
34
|
+
`params` where {
|
|
35
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'params'. Use 'parameters' instead.")
|
|
36
|
+
},
|
|
37
|
+
`obj` where {
|
|
38
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'obj'. Use a descriptive name instead.")
|
|
39
|
+
},
|
|
40
|
+
`val` where {
|
|
41
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'val'. Use 'value' instead.")
|
|
42
|
+
},
|
|
43
|
+
`idx` where {
|
|
44
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'idx'. Use 'index' instead.")
|
|
45
|
+
},
|
|
46
|
+
`len` where {
|
|
47
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'len'. Use 'length' instead.")
|
|
48
|
+
},
|
|
49
|
+
`tmp` where {
|
|
50
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'tmp'. Use a descriptive name instead.")
|
|
51
|
+
},
|
|
52
|
+
`str` where {
|
|
53
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'str'. Use 'string' or a descriptive name instead.")
|
|
54
|
+
},
|
|
55
|
+
`num` where {
|
|
56
|
+
register_diagnostic(span=$match, message="Avoid abbreviated name 'num'. Use 'number' or a descriptive name instead.")
|
|
57
|
+
}
|
|
58
|
+
}
|
package/dist/artifacts.js
CHANGED
|
@@ -302,19 +302,53 @@ async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
|
|
|
302
302
|
function titleToFilename(title) {
|
|
303
303
|
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
304
304
|
}
|
|
305
|
-
var
|
|
306
|
-
{ type: "refine",
|
|
307
|
-
{ type: "decompose-idea",
|
|
308
|
-
{ type: "shelve",
|
|
305
|
+
var WORKFLOW_SECTION_HEADINGS = [
|
|
306
|
+
{ type: "refine", heading: "Refines Idea" },
|
|
307
|
+
{ type: "decompose-idea", heading: "Decomposes Idea" },
|
|
308
|
+
{ type: "shelve", heading: "Shelves Idea" }
|
|
309
309
|
];
|
|
310
|
+
function extractIdeaSlugFromSection(content, sectionHeading) {
|
|
311
|
+
const lines = content.split(`
|
|
312
|
+
`);
|
|
313
|
+
let inSection = false;
|
|
314
|
+
for (const line of lines) {
|
|
315
|
+
if (line.startsWith("## ")) {
|
|
316
|
+
inSection = line.trimEnd() === `## ${sectionHeading}`;
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
if (!inSection)
|
|
320
|
+
continue;
|
|
321
|
+
if (line.startsWith("# "))
|
|
322
|
+
break;
|
|
323
|
+
const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
|
|
324
|
+
if (linkMatch) {
|
|
325
|
+
const target = linkMatch[2];
|
|
326
|
+
const slugMatch = target.match(/([^/]+)\.md$/);
|
|
327
|
+
if (slugMatch) {
|
|
328
|
+
return slugMatch[1];
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
310
334
|
async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
335
|
+
const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
|
|
336
|
+
if (!fileSystem.exists(ideaPath)) {
|
|
337
|
+
throw new Error(`Idea not found: "${ideaSlug}" (expected file at ${ideaPath})`);
|
|
338
|
+
}
|
|
339
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
340
|
+
if (!fileSystem.exists(tasksPath)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
const files = await fileSystem.readdir(tasksPath);
|
|
344
|
+
for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
|
|
345
|
+
const content = await fileSystem.readFile(`${tasksPath}/${file}`);
|
|
346
|
+
for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
|
|
347
|
+
const linkedSlug = extractIdeaSlugFromSection(content, heading);
|
|
348
|
+
if (linkedSlug === ideaSlug) {
|
|
349
|
+
const taskSlug = file.replace(/\.md$/, "");
|
|
350
|
+
return { type, ideaSlug, taskSlug };
|
|
351
|
+
}
|
|
318
352
|
}
|
|
319
353
|
}
|
|
320
354
|
return null;
|
|
@@ -342,22 +376,26 @@ ${sections.join(`
|
|
|
342
376
|
`)}
|
|
343
377
|
`;
|
|
344
378
|
}
|
|
345
|
-
function
|
|
379
|
+
function renderIdeaSection(ideaSection) {
|
|
380
|
+
return `## ${ideaSection.heading}
|
|
381
|
+
|
|
382
|
+
- [${ideaSection.ideaTitle}](../ideas/${ideaSection.ideaSlug}.md)
|
|
383
|
+
`;
|
|
384
|
+
}
|
|
385
|
+
function renderTask(title, openingSentence, definitionOfDone, ideaSection, options) {
|
|
346
386
|
const descriptionParagraph = options?.description !== undefined ? `
|
|
347
387
|
${options.description}
|
|
348
388
|
` : "";
|
|
349
389
|
const resolvedSection = options?.resolvedQuestions && options.resolvedQuestions.length > 0 ? `
|
|
350
390
|
${renderResolvedQuestions(options.resolvedQuestions)}
|
|
351
391
|
` : "";
|
|
392
|
+
const ideaSectionContent = `
|
|
393
|
+
${renderIdeaSection(ideaSection)}
|
|
394
|
+
`;
|
|
352
395
|
return `# ${title}
|
|
353
396
|
|
|
354
397
|
${openingSentence}
|
|
355
|
-
${descriptionParagraph}${resolvedSection}
|
|
356
|
-
## Principles
|
|
357
|
-
|
|
358
|
-
(none)
|
|
359
|
-
|
|
360
|
-
## Blocked By
|
|
398
|
+
${descriptionParagraph}${resolvedSection}${ideaSectionContent}## Blocked By
|
|
361
399
|
|
|
362
400
|
(none)
|
|
363
401
|
|
|
@@ -367,13 +405,17 @@ ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
|
|
|
367
405
|
`)}
|
|
368
406
|
`;
|
|
369
407
|
}
|
|
370
|
-
async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, taskOptions) {
|
|
408
|
+
async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, ideaSectionHeading, taskOptions) {
|
|
371
409
|
const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
|
|
372
410
|
const taskTitle = `${prefix}${ideaTitle}`;
|
|
373
411
|
const filename = titleToFilename(taskTitle);
|
|
374
412
|
const filePath = `${dustPath}/tasks/${filename}`;
|
|
375
413
|
const openingSentence = openingSentenceTemplate(ideaTitle);
|
|
376
|
-
const
|
|
414
|
+
const ideaSection = { heading: ideaSectionHeading, ideaTitle, ideaSlug };
|
|
415
|
+
const content = renderTask(taskTitle, openingSentence, definitionOfDone, ideaSection, {
|
|
416
|
+
description: taskOptions?.description,
|
|
417
|
+
resolvedQuestions: taskOptions?.resolvedQuestions
|
|
418
|
+
});
|
|
377
419
|
await fileSystem.writeFile(filePath, content);
|
|
378
420
|
return { filePath };
|
|
379
421
|
}
|
|
@@ -383,20 +425,20 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description)
|
|
|
383
425
|
"Open questions are added for any ambiguous or underspecified aspects",
|
|
384
426
|
"Open questions follow the required heading format and focus on high-value decisions",
|
|
385
427
|
"Idea file is updated with findings"
|
|
386
|
-
], { description });
|
|
428
|
+
], "Refines Idea", { description });
|
|
387
429
|
}
|
|
388
430
|
async function decomposeIdea(fileSystem, dustPath, options) {
|
|
389
431
|
return createIdeaTask(fileSystem, dustPath, "Decompose Idea: ", options.ideaSlug, (ideaTitle) => `Create one or more well-defined tasks from this idea. Prefer smaller, narrowly scoped tasks that each deliver a thin but complete vertical slice of working software -- a path through the system that can be tested end-to-end -- rather than component-oriented tasks (like "add schema" or "build endpoint") that only work once all tasks are done. Split the idea into multiple tasks if it covers more than one logical change. Review \`.dust/principles/\` to link relevant principles and \`.dust/facts/\` for design decisions that should inform the task. See [${ideaTitle}](../ideas/${options.ideaSlug}.md).`, [
|
|
390
432
|
"One or more new tasks are created in .dust/tasks/",
|
|
391
433
|
"Task's Principles section links to relevant principles from .dust/principles/",
|
|
392
434
|
"The original idea is deleted or updated to reflect remaining scope"
|
|
393
|
-
], {
|
|
435
|
+
], "Decomposes Idea", {
|
|
394
436
|
description: options.description,
|
|
395
437
|
resolvedQuestions: options.openQuestionResponses
|
|
396
438
|
});
|
|
397
439
|
}
|
|
398
440
|
async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
|
|
399
|
-
return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], { description });
|
|
441
|
+
return createIdeaTask(fileSystem, dustPath, "Shelve Idea: ", ideaSlug, (ideaTitle) => `Archive this idea and remove it from the active backlog. See [${ideaTitle}](../ideas/${ideaSlug}.md).`, ["Idea file is deleted", "Rationale is recorded in the commit message"], "Shelves Idea", { description });
|
|
400
442
|
}
|
|
401
443
|
async function createCaptureIdeaTask(fileSystem, dustPath, options) {
|
|
402
444
|
const { title, description, buildItNow } = options;
|
|
@@ -418,10 +460,6 @@ Research this idea thoroughly, then create one or more narrowly-scoped task file
|
|
|
418
460
|
|
|
419
461
|
${description}
|
|
420
462
|
|
|
421
|
-
## Principles
|
|
422
|
-
|
|
423
|
-
(none)
|
|
424
|
-
|
|
425
463
|
## Blocked By
|
|
426
464
|
|
|
427
465
|
(none)
|
|
@@ -438,28 +476,22 @@ ${description}
|
|
|
438
476
|
const taskTitle = `${CAPTURE_IDEA_PREFIX}${title}`;
|
|
439
477
|
const filename = titleToFilename(taskTitle);
|
|
440
478
|
const filePath = `${dustPath}/tasks/${filename}`;
|
|
441
|
-
const ideaFilename = titleToFilename(title);
|
|
442
|
-
const ideaPath = `.dust/ideas/${ideaFilename}`;
|
|
443
479
|
const content = `# ${taskTitle}
|
|
444
480
|
|
|
445
|
-
Research this idea thoroughly, then create
|
|
481
|
+
Research this idea thoroughly, then create one or more idea files in \`.dust/ideas/\`. Read the codebase for relevant context, flesh out the description, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. If you add open questions, use \`## Open Questions\` with \`### Question?\` headings and one or more \`#### Option\` headings beneath each question, and only add questions that are meaningful decisions worth asking. Review \`.dust/principles/\` and \`.dust/facts/\` for relevant context.
|
|
446
482
|
|
|
447
483
|
## Idea Description
|
|
448
484
|
|
|
449
485
|
${description}
|
|
450
486
|
|
|
451
|
-
## Principles
|
|
452
|
-
|
|
453
|
-
(none)
|
|
454
|
-
|
|
455
487
|
## Blocked By
|
|
456
488
|
|
|
457
489
|
(none)
|
|
458
490
|
|
|
459
491
|
## Definition of Done
|
|
460
492
|
|
|
461
|
-
- [ ]
|
|
462
|
-
- [ ]
|
|
493
|
+
- [ ] One or more idea files are created in \`.dust/ideas/\`
|
|
494
|
+
- [ ] Each idea file has an H1 title matching its content
|
|
463
495
|
- [ ] Idea includes relevant context from codebase exploration
|
|
464
496
|
- [ ] Open questions are added for any ambiguous or underspecified aspects
|
|
465
497
|
- [ ] Open questions follow the required heading format and focus on high-value decisions
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* Users can override any of these by placing a file with the same name
|
|
5
5
|
* in .dust/config/audits/.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
interface StockAudit {
|
|
8
8
|
name: string;
|
|
9
9
|
description: string;
|
|
10
10
|
template: string;
|
|
11
11
|
}
|
|
12
12
|
export declare function loadStockAudits(): StockAudit[];
|
|
13
|
+
export {};
|
package/dist/audits.js
CHANGED
|
@@ -63,11 +63,50 @@ function dedent(strings, ...values) {
|
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
// lib/audits/stock-audits.ts
|
|
66
|
+
var ideasHint = "Review existing ideas in `./.dust/ideas/` to understand what has been proposed or considered historically, then create new idea files in `./.dust/ideas/` for any issues you identify, avoiding duplication.";
|
|
67
|
+
function componentReuse() {
|
|
68
|
+
return dedent`
|
|
69
|
+
# Component Reuse
|
|
70
|
+
|
|
71
|
+
Find repeated patterns and code that could be extracted into reusable components.
|
|
72
|
+
|
|
73
|
+
${ideasHint}
|
|
74
|
+
|
|
75
|
+
## Scope
|
|
76
|
+
|
|
77
|
+
Focus on these areas:
|
|
78
|
+
|
|
79
|
+
1. **Repeated patterns** - Similar code blocks that appear multiple times
|
|
80
|
+
2. **Copy-pasted code** - Near-identical logic across different files
|
|
81
|
+
3. **Parallel structures** - Code that handles similar cases with minor variations
|
|
82
|
+
4. **Extraction opportunities** - Logic that could be unified without forcing unrelated concepts together
|
|
83
|
+
|
|
84
|
+
## Principles
|
|
85
|
+
|
|
86
|
+
- [Reasonably DRY](../principles/reasonably-dry.md)
|
|
87
|
+
- [Decoupled Code](../principles/decoupled-code.md)
|
|
88
|
+
- [Maintainable Codebase](../principles/maintainable-codebase.md)
|
|
89
|
+
|
|
90
|
+
## Blocked By
|
|
91
|
+
|
|
92
|
+
(none)
|
|
93
|
+
|
|
94
|
+
## Definition of Done
|
|
95
|
+
|
|
96
|
+
- [ ] Searched for repeated patterns across the codebase
|
|
97
|
+
- [ ] Identified copy-pasted or near-duplicate code
|
|
98
|
+
- [ ] Evaluated each case for whether extraction would be beneficial
|
|
99
|
+
- [ ] Considered whether similar code serves different purposes that may evolve independently
|
|
100
|
+
- [ ] Proposed ideas only for extractions where duplication is truly about the same concept
|
|
101
|
+
`;
|
|
102
|
+
}
|
|
66
103
|
function agentDeveloperExperience() {
|
|
67
104
|
return dedent`
|
|
68
105
|
# Agent Developer Experience
|
|
69
106
|
|
|
70
|
-
Review the codebase to ensure agents have everything they need to operate effectively.
|
|
107
|
+
Review the codebase to ensure agents have everything they need to operate effectively.
|
|
108
|
+
|
|
109
|
+
${ideasHint}
|
|
71
110
|
|
|
72
111
|
## Scope
|
|
73
112
|
|
|
@@ -100,7 +139,9 @@ function deadCode() {
|
|
|
100
139
|
return dedent`
|
|
101
140
|
# Dead Code
|
|
102
141
|
|
|
103
|
-
Find and remove unused code to improve maintainability and reduce bundle size.
|
|
142
|
+
Find and remove unused code to improve maintainability and reduce bundle size.
|
|
143
|
+
|
|
144
|
+
${ideasHint}
|
|
104
145
|
|
|
105
146
|
## Scope
|
|
106
147
|
|
|
@@ -135,7 +176,9 @@ function factsVerification() {
|
|
|
135
176
|
return dedent`
|
|
136
177
|
# Facts Verification
|
|
137
178
|
|
|
138
|
-
Review \`.dust/facts/\` to ensure documented facts match current reality.
|
|
179
|
+
Review \`.dust/facts/\` to ensure documented facts match current reality.
|
|
180
|
+
|
|
181
|
+
${ideasHint}
|
|
139
182
|
|
|
140
183
|
## Scope
|
|
141
184
|
|
|
@@ -168,7 +211,9 @@ function ideasFromCommits() {
|
|
|
168
211
|
return dedent`
|
|
169
212
|
# Ideas from Commits
|
|
170
213
|
|
|
171
|
-
Review recent commit history to identify follow-up improvement ideas.
|
|
214
|
+
Review recent commit history to identify follow-up improvement ideas.
|
|
215
|
+
|
|
216
|
+
${ideasHint}
|
|
172
217
|
|
|
173
218
|
## Scope
|
|
174
219
|
|
|
@@ -200,7 +245,9 @@ function ideasFromPrinciples() {
|
|
|
200
245
|
return dedent`
|
|
201
246
|
# Ideas from Principles
|
|
202
247
|
|
|
203
|
-
Review \`.dust/principles/\` to generate new improvement ideas.
|
|
248
|
+
Review \`.dust/principles/\` to generate new improvement ideas.
|
|
249
|
+
|
|
250
|
+
${ideasHint}
|
|
204
251
|
|
|
205
252
|
## Scope
|
|
206
253
|
|
|
@@ -231,7 +278,9 @@ function performanceReview() {
|
|
|
231
278
|
return dedent`
|
|
232
279
|
# Performance Review
|
|
233
280
|
|
|
234
|
-
Review the application for performance issues and optimization opportunities.
|
|
281
|
+
Review the application for performance issues and optimization opportunities.
|
|
282
|
+
|
|
283
|
+
${ideasHint}
|
|
235
284
|
|
|
236
285
|
## Scope
|
|
237
286
|
|
|
@@ -264,7 +313,9 @@ function securityReview() {
|
|
|
264
313
|
return dedent`
|
|
265
314
|
# Security Review
|
|
266
315
|
|
|
267
|
-
Review the codebase for common security vulnerabilities and misconfigurations.
|
|
316
|
+
Review the codebase for common security vulnerabilities and misconfigurations.
|
|
317
|
+
|
|
318
|
+
${ideasHint}
|
|
268
319
|
|
|
269
320
|
## Scope
|
|
270
321
|
|
|
@@ -299,7 +350,9 @@ function staleIdeas() {
|
|
|
299
350
|
return dedent`
|
|
300
351
|
# Stale Ideas
|
|
301
352
|
|
|
302
|
-
Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant.
|
|
353
|
+
Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant.
|
|
354
|
+
|
|
355
|
+
${ideasHint}
|
|
303
356
|
|
|
304
357
|
## Scope
|
|
305
358
|
|
|
@@ -331,7 +384,9 @@ function testCoverage() {
|
|
|
331
384
|
return dedent`
|
|
332
385
|
# Test Coverage
|
|
333
386
|
|
|
334
|
-
Identify untested code paths and areas that need additional test coverage.
|
|
387
|
+
Identify untested code paths and areas that need additional test coverage.
|
|
388
|
+
|
|
389
|
+
${ideasHint}
|
|
335
390
|
|
|
336
391
|
## Scope
|
|
337
392
|
|
|
@@ -361,6 +416,7 @@ function testCoverage() {
|
|
|
361
416
|
}
|
|
362
417
|
var stockAuditFunctions = {
|
|
363
418
|
"agent-developer-experience": agentDeveloperExperience,
|
|
419
|
+
"component-reuse": componentReuse,
|
|
364
420
|
"dead-code": deadCode,
|
|
365
421
|
"facts-verification": factsVerification,
|
|
366
422
|
"ideas-from-commits": ideasFromCommits,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Biome path export
|
|
3
|
+
*
|
|
4
|
+
* Provides the path to the biome directory containing custom GritQL lint rules.
|
|
5
|
+
* Downstream users can reference this path in their biome.json plugins array.
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Absolute path to the biome directory containing custom GritQL lint rules.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* import { biomePath } from "@joshski/dust/biome";
|
|
13
|
+
* // Returns: "/path/to/node_modules/@joshski/dust/biome"
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export declare const biomePath: string;
|
package/dist/biome.js
ADDED
package/dist/cli/types.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Common types for CLI commands
|
|
3
3
|
*/
|
|
4
4
|
import type { FileSystem, GlobScanner } from '../filesystem/types';
|
|
5
|
-
export type { FileSystem, GlobScanner, ReadableFileSystem,
|
|
5
|
+
export type { FileSystem, GlobScanner, ReadableFileSystem, } from '../filesystem/types';
|
|
6
6
|
export interface CommandContext {
|
|
7
7
|
cwd: string;
|
|
8
8
|
stdout: (message: string) => void;
|
package/dist/dust.js
CHANGED
|
@@ -275,7 +275,25 @@ async function loadSettings(cwd, fileSystem) {
|
|
|
275
275
|
}
|
|
276
276
|
|
|
277
277
|
// lib/version.ts
|
|
278
|
-
var DUST_VERSION = "0.1.
|
|
278
|
+
var DUST_VERSION = "0.1.67";
|
|
279
|
+
|
|
280
|
+
// lib/session.ts
|
|
281
|
+
var DUST_UNATTENDED = "DUST_UNATTENDED";
|
|
282
|
+
var DUST_SKIP_AGENT = "DUST_SKIP_AGENT";
|
|
283
|
+
var DUST_REPOSITORY_ID = "DUST_REPOSITORY_ID";
|
|
284
|
+
function isUnattended(env = process.env) {
|
|
285
|
+
return !!env[DUST_UNATTENDED];
|
|
286
|
+
}
|
|
287
|
+
function buildUnattendedEnv(options) {
|
|
288
|
+
const env = {
|
|
289
|
+
[DUST_UNATTENDED]: "1",
|
|
290
|
+
[DUST_SKIP_AGENT]: "1"
|
|
291
|
+
};
|
|
292
|
+
if (options?.repositoryId) {
|
|
293
|
+
env[DUST_REPOSITORY_ID] = options.repositoryId;
|
|
294
|
+
}
|
|
295
|
+
return env;
|
|
296
|
+
}
|
|
279
297
|
|
|
280
298
|
// lib/cli/dedent.ts
|
|
281
299
|
function dedent(strings, ...values) {
|
|
@@ -524,7 +542,7 @@ ${vars.agentInstructions}` : "";
|
|
|
524
542
|
}
|
|
525
543
|
async function agent(dependencies, env = process.env) {
|
|
526
544
|
const { context, fileSystem, settings } = dependencies;
|
|
527
|
-
if (env
|
|
545
|
+
if (env[DUST_SKIP_AGENT] === "1") {
|
|
528
546
|
context.stdout("You're running in an automated loop - proceeding to implement the assigned task.");
|
|
529
547
|
return { exitCode: 0 };
|
|
530
548
|
}
|
|
@@ -590,11 +608,50 @@ function extractOpeningSentence(content) {
|
|
|
590
608
|
}
|
|
591
609
|
|
|
592
610
|
// lib/audits/stock-audits.ts
|
|
611
|
+
var ideasHint = "Review existing ideas in `./.dust/ideas/` to understand what has been proposed or considered historically, then create new idea files in `./.dust/ideas/` for any issues you identify, avoiding duplication.";
|
|
612
|
+
function componentReuse() {
|
|
613
|
+
return dedent`
|
|
614
|
+
# Component Reuse
|
|
615
|
+
|
|
616
|
+
Find repeated patterns and code that could be extracted into reusable components.
|
|
617
|
+
|
|
618
|
+
${ideasHint}
|
|
619
|
+
|
|
620
|
+
## Scope
|
|
621
|
+
|
|
622
|
+
Focus on these areas:
|
|
623
|
+
|
|
624
|
+
1. **Repeated patterns** - Similar code blocks that appear multiple times
|
|
625
|
+
2. **Copy-pasted code** - Near-identical logic across different files
|
|
626
|
+
3. **Parallel structures** - Code that handles similar cases with minor variations
|
|
627
|
+
4. **Extraction opportunities** - Logic that could be unified without forcing unrelated concepts together
|
|
628
|
+
|
|
629
|
+
## Principles
|
|
630
|
+
|
|
631
|
+
- [Reasonably DRY](../principles/reasonably-dry.md)
|
|
632
|
+
- [Decoupled Code](../principles/decoupled-code.md)
|
|
633
|
+
- [Maintainable Codebase](../principles/maintainable-codebase.md)
|
|
634
|
+
|
|
635
|
+
## Blocked By
|
|
636
|
+
|
|
637
|
+
(none)
|
|
638
|
+
|
|
639
|
+
## Definition of Done
|
|
640
|
+
|
|
641
|
+
- [ ] Searched for repeated patterns across the codebase
|
|
642
|
+
- [ ] Identified copy-pasted or near-duplicate code
|
|
643
|
+
- [ ] Evaluated each case for whether extraction would be beneficial
|
|
644
|
+
- [ ] Considered whether similar code serves different purposes that may evolve independently
|
|
645
|
+
- [ ] Proposed ideas only for extractions where duplication is truly about the same concept
|
|
646
|
+
`;
|
|
647
|
+
}
|
|
593
648
|
function agentDeveloperExperience() {
|
|
594
649
|
return dedent`
|
|
595
650
|
# Agent Developer Experience
|
|
596
651
|
|
|
597
|
-
Review the codebase to ensure agents have everything they need to operate effectively.
|
|
652
|
+
Review the codebase to ensure agents have everything they need to operate effectively.
|
|
653
|
+
|
|
654
|
+
${ideasHint}
|
|
598
655
|
|
|
599
656
|
## Scope
|
|
600
657
|
|
|
@@ -627,7 +684,9 @@ function deadCode() {
|
|
|
627
684
|
return dedent`
|
|
628
685
|
# Dead Code
|
|
629
686
|
|
|
630
|
-
Find and remove unused code to improve maintainability and reduce bundle size.
|
|
687
|
+
Find and remove unused code to improve maintainability and reduce bundle size.
|
|
688
|
+
|
|
689
|
+
${ideasHint}
|
|
631
690
|
|
|
632
691
|
## Scope
|
|
633
692
|
|
|
@@ -662,7 +721,9 @@ function factsVerification() {
|
|
|
662
721
|
return dedent`
|
|
663
722
|
# Facts Verification
|
|
664
723
|
|
|
665
|
-
Review \`.dust/facts/\` to ensure documented facts match current reality.
|
|
724
|
+
Review \`.dust/facts/\` to ensure documented facts match current reality.
|
|
725
|
+
|
|
726
|
+
${ideasHint}
|
|
666
727
|
|
|
667
728
|
## Scope
|
|
668
729
|
|
|
@@ -695,7 +756,9 @@ function ideasFromCommits() {
|
|
|
695
756
|
return dedent`
|
|
696
757
|
# Ideas from Commits
|
|
697
758
|
|
|
698
|
-
Review recent commit history to identify follow-up improvement ideas.
|
|
759
|
+
Review recent commit history to identify follow-up improvement ideas.
|
|
760
|
+
|
|
761
|
+
${ideasHint}
|
|
699
762
|
|
|
700
763
|
## Scope
|
|
701
764
|
|
|
@@ -727,7 +790,9 @@ function ideasFromPrinciples() {
|
|
|
727
790
|
return dedent`
|
|
728
791
|
# Ideas from Principles
|
|
729
792
|
|
|
730
|
-
Review \`.dust/principles/\` to generate new improvement ideas.
|
|
793
|
+
Review \`.dust/principles/\` to generate new improvement ideas.
|
|
794
|
+
|
|
795
|
+
${ideasHint}
|
|
731
796
|
|
|
732
797
|
## Scope
|
|
733
798
|
|
|
@@ -758,7 +823,9 @@ function performanceReview() {
|
|
|
758
823
|
return dedent`
|
|
759
824
|
# Performance Review
|
|
760
825
|
|
|
761
|
-
Review the application for performance issues and optimization opportunities.
|
|
826
|
+
Review the application for performance issues and optimization opportunities.
|
|
827
|
+
|
|
828
|
+
${ideasHint}
|
|
762
829
|
|
|
763
830
|
## Scope
|
|
764
831
|
|
|
@@ -791,7 +858,9 @@ function securityReview() {
|
|
|
791
858
|
return dedent`
|
|
792
859
|
# Security Review
|
|
793
860
|
|
|
794
|
-
Review the codebase for common security vulnerabilities and misconfigurations.
|
|
861
|
+
Review the codebase for common security vulnerabilities and misconfigurations.
|
|
862
|
+
|
|
863
|
+
${ideasHint}
|
|
795
864
|
|
|
796
865
|
## Scope
|
|
797
866
|
|
|
@@ -826,7 +895,9 @@ function staleIdeas() {
|
|
|
826
895
|
return dedent`
|
|
827
896
|
# Stale Ideas
|
|
828
897
|
|
|
829
|
-
Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant.
|
|
898
|
+
Review \`.dust/ideas/\` to identify ideas that have become stale or irrelevant.
|
|
899
|
+
|
|
900
|
+
${ideasHint}
|
|
830
901
|
|
|
831
902
|
## Scope
|
|
832
903
|
|
|
@@ -858,7 +929,9 @@ function testCoverage() {
|
|
|
858
929
|
return dedent`
|
|
859
930
|
# Test Coverage
|
|
860
931
|
|
|
861
|
-
Identify untested code paths and areas that need additional test coverage.
|
|
932
|
+
Identify untested code paths and areas that need additional test coverage.
|
|
933
|
+
|
|
934
|
+
${ideasHint}
|
|
862
935
|
|
|
863
936
|
## Scope
|
|
864
937
|
|
|
@@ -888,6 +961,7 @@ function testCoverage() {
|
|
|
888
961
|
}
|
|
889
962
|
var stockAuditFunctions = {
|
|
890
963
|
"agent-developer-experience": agentDeveloperExperience,
|
|
964
|
+
"component-reuse": componentReuse,
|
|
891
965
|
"dead-code": deadCode,
|
|
892
966
|
"facts-verification": factsVerification,
|
|
893
967
|
"ideas-from-commits": ideasFromCommits,
|
|
@@ -2133,13 +2207,7 @@ async function runOneIteration(dependencies, loopDependencies, onLoopEvent, onAg
|
|
|
2133
2207
|
logger = log,
|
|
2134
2208
|
repositoryId
|
|
2135
2209
|
} = options;
|
|
2136
|
-
const baseEnv = {
|
|
2137
|
-
DUST_UNATTENDED: "1",
|
|
2138
|
-
DUST_SKIP_AGENT: "1"
|
|
2139
|
-
};
|
|
2140
|
-
if (repositoryId) {
|
|
2141
|
-
baseEnv.DUST_REPOSITORY_ID = repositoryId;
|
|
2142
|
-
}
|
|
2210
|
+
const baseEnv = buildUnattendedEnv({ repositoryId });
|
|
2143
2211
|
log("syncing with remote");
|
|
2144
2212
|
onLoopEvent({ type: "loop.syncing" });
|
|
2145
2213
|
const pullResult = await gitPull(context.cwd, spawn);
|
|
@@ -2263,6 +2331,10 @@ function parseMaxIterations(commandArguments) {
|
|
|
2263
2331
|
async function loopClaude(dependencies, loopDependencies = createDefaultDependencies()) {
|
|
2264
2332
|
enableFileLogs("loop");
|
|
2265
2333
|
const { context, settings } = dependencies;
|
|
2334
|
+
if (isUnattended()) {
|
|
2335
|
+
context.stderr("dust loop cannot run inside an unattended session (DUST_UNATTENDED is set)");
|
|
2336
|
+
return { exitCode: 1 };
|
|
2337
|
+
}
|
|
2266
2338
|
const { postEvent } = loopDependencies;
|
|
2267
2339
|
const maxIterations = parseMaxIterations(dependencies.arguments);
|
|
2268
2340
|
const eventsUrl = settings.eventsUrl;
|
|
@@ -3446,9 +3518,13 @@ async function resolveToken(authDeps, context) {
|
|
|
3446
3518
|
return null;
|
|
3447
3519
|
}
|
|
3448
3520
|
}
|
|
3449
|
-
async function
|
|
3521
|
+
async function bucketWorker(dependencies, bucketDeps = createDefaultBucketDependencies()) {
|
|
3450
3522
|
enableFileLogs("bucket");
|
|
3451
3523
|
const { context, fileSystem } = dependencies;
|
|
3524
|
+
if (isUnattended()) {
|
|
3525
|
+
context.stderr("dust bucket cannot run inside an unattended session (DUST_UNATTENDED is set)");
|
|
3526
|
+
return { exitCode: 1 };
|
|
3527
|
+
}
|
|
3452
3528
|
const token = await resolveToken(bucketDeps.auth, context);
|
|
3453
3529
|
if (!token) {
|
|
3454
3530
|
return { exitCode: 1 };
|
|
@@ -3740,11 +3816,7 @@ function runBufferedProcess(spawnFn, command, commandArguments, cwd, shell, time
|
|
|
3740
3816
|
import { join as join8 } from "node:path";
|
|
3741
3817
|
|
|
3742
3818
|
// lib/lint/validators/content-validator.ts
|
|
3743
|
-
var REQUIRED_HEADINGS = [
|
|
3744
|
-
"## Principles",
|
|
3745
|
-
"## Blocked By",
|
|
3746
|
-
"## Definition of Done"
|
|
3747
|
-
];
|
|
3819
|
+
var REQUIRED_HEADINGS = ["## Blocked By", "## Definition of Done"];
|
|
3748
3820
|
var MAX_OPENING_SENTENCE_LENGTH = 150;
|
|
3749
3821
|
var NON_IMPERATIVE_STARTERS = new Set([
|
|
3750
3822
|
"the",
|
|
@@ -3912,6 +3984,11 @@ function validateTitleFilenameMatch(filePath, content) {
|
|
|
3912
3984
|
}
|
|
3913
3985
|
|
|
3914
3986
|
// lib/lint/validators/idea-validator.ts
|
|
3987
|
+
var WORKFLOW_PREFIX_TO_SECTION = {
|
|
3988
|
+
"Refine Idea: ": "Refines Idea",
|
|
3989
|
+
"Decompose Idea: ": "Decomposes Idea",
|
|
3990
|
+
"Shelve Idea: ": "Shelves Idea"
|
|
3991
|
+
};
|
|
3915
3992
|
function validateIdeaOpenQuestions(filePath, content) {
|
|
3916
3993
|
const violations = [];
|
|
3917
3994
|
const lines = content.split(`
|
|
@@ -4025,6 +4102,114 @@ function validateIdeaTransitionTitle(filePath, content, ideasPath, fileSystem) {
|
|
|
4025
4102
|
}
|
|
4026
4103
|
return null;
|
|
4027
4104
|
}
|
|
4105
|
+
function extractSectionContent(content, sectionHeading) {
|
|
4106
|
+
const lines = content.split(`
|
|
4107
|
+
`);
|
|
4108
|
+
let inSection = false;
|
|
4109
|
+
let sectionContent = "";
|
|
4110
|
+
let startLine = 0;
|
|
4111
|
+
for (let i = 0;i < lines.length; i++) {
|
|
4112
|
+
const line = lines[i];
|
|
4113
|
+
if (line.startsWith("## ")) {
|
|
4114
|
+
if (inSection)
|
|
4115
|
+
break;
|
|
4116
|
+
if (line.trimEnd() === `## ${sectionHeading}`) {
|
|
4117
|
+
inSection = true;
|
|
4118
|
+
startLine = i + 1;
|
|
4119
|
+
}
|
|
4120
|
+
continue;
|
|
4121
|
+
}
|
|
4122
|
+
if (line.startsWith("# ") && inSection)
|
|
4123
|
+
break;
|
|
4124
|
+
if (inSection) {
|
|
4125
|
+
sectionContent += `${line}
|
|
4126
|
+
`;
|
|
4127
|
+
}
|
|
4128
|
+
}
|
|
4129
|
+
if (!inSection)
|
|
4130
|
+
return null;
|
|
4131
|
+
return { content: sectionContent, startLine };
|
|
4132
|
+
}
|
|
4133
|
+
function validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSystem) {
|
|
4134
|
+
const violations = [];
|
|
4135
|
+
const title = extractTitle(content);
|
|
4136
|
+
if (!title)
|
|
4137
|
+
return violations;
|
|
4138
|
+
let matchedPrefix = null;
|
|
4139
|
+
for (const prefix of IDEA_TRANSITION_PREFIXES) {
|
|
4140
|
+
if (title.startsWith(prefix)) {
|
|
4141
|
+
matchedPrefix = prefix;
|
|
4142
|
+
break;
|
|
4143
|
+
}
|
|
4144
|
+
}
|
|
4145
|
+
if (!matchedPrefix)
|
|
4146
|
+
return violations;
|
|
4147
|
+
const expectedHeading = WORKFLOW_PREFIX_TO_SECTION[matchedPrefix];
|
|
4148
|
+
const section = extractSectionContent(content, expectedHeading);
|
|
4149
|
+
if (!section) {
|
|
4150
|
+
violations.push({
|
|
4151
|
+
file: filePath,
|
|
4152
|
+
message: `Workflow task with "${matchedPrefix.trim()}" prefix is missing required "## ${expectedHeading}" section. Add a section with a link to the idea file, e.g.:
|
|
4153
|
+
|
|
4154
|
+
## ${expectedHeading}
|
|
4155
|
+
|
|
4156
|
+
- [Idea Title](../ideas/idea-slug.md)`
|
|
4157
|
+
});
|
|
4158
|
+
return violations;
|
|
4159
|
+
}
|
|
4160
|
+
const linkRegex = new RegExp(MARKDOWN_LINK_PATTERN.source, "g");
|
|
4161
|
+
const links = [];
|
|
4162
|
+
const sectionLines = section.content.split(`
|
|
4163
|
+
`);
|
|
4164
|
+
for (let i = 0;i < sectionLines.length; i++) {
|
|
4165
|
+
const line = sectionLines[i];
|
|
4166
|
+
let match = linkRegex.exec(line);
|
|
4167
|
+
while (match !== null) {
|
|
4168
|
+
links.push({
|
|
4169
|
+
text: match[1],
|
|
4170
|
+
target: match[2],
|
|
4171
|
+
line: section.startLine + i + 1
|
|
4172
|
+
});
|
|
4173
|
+
match = linkRegex.exec(line);
|
|
4174
|
+
}
|
|
4175
|
+
}
|
|
4176
|
+
if (links.length === 0) {
|
|
4177
|
+
violations.push({
|
|
4178
|
+
file: filePath,
|
|
4179
|
+
message: `"## ${expectedHeading}" section contains no link. Add a markdown link to the idea file, e.g.:
|
|
4180
|
+
|
|
4181
|
+
- [Idea Title](../ideas/idea-slug.md)`,
|
|
4182
|
+
line: section.startLine
|
|
4183
|
+
});
|
|
4184
|
+
return violations;
|
|
4185
|
+
}
|
|
4186
|
+
const ideaLinks = links.filter((l) => l.target.includes("/ideas/") || l.target.startsWith("../ideas/"));
|
|
4187
|
+
if (ideaLinks.length === 0) {
|
|
4188
|
+
violations.push({
|
|
4189
|
+
file: filePath,
|
|
4190
|
+
message: `"## ${expectedHeading}" section contains no link to an idea file. Links must point to a file in ../ideas/, e.g.:
|
|
4191
|
+
|
|
4192
|
+
- [Idea Title](../ideas/idea-slug.md)`,
|
|
4193
|
+
line: section.startLine
|
|
4194
|
+
});
|
|
4195
|
+
return violations;
|
|
4196
|
+
}
|
|
4197
|
+
for (const link of ideaLinks) {
|
|
4198
|
+
const slugMatch = link.target.match(/([^/]+)\.md$/);
|
|
4199
|
+
if (!slugMatch)
|
|
4200
|
+
continue;
|
|
4201
|
+
const ideaSlug = slugMatch[1];
|
|
4202
|
+
const ideaFilePath = `${ideasPath}/${ideaSlug}.md`;
|
|
4203
|
+
if (!fileSystem.exists(ideaFilePath)) {
|
|
4204
|
+
violations.push({
|
|
4205
|
+
file: filePath,
|
|
4206
|
+
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.`,
|
|
4207
|
+
line: link.line
|
|
4208
|
+
});
|
|
4209
|
+
}
|
|
4210
|
+
}
|
|
4211
|
+
return violations;
|
|
4212
|
+
}
|
|
4028
4213
|
|
|
4029
4214
|
// lib/lint/validators/link-validator.ts
|
|
4030
4215
|
import { dirname as dirname3, resolve } from "node:path";
|
|
@@ -4428,6 +4613,7 @@ async function lintMarkdown(dependencies) {
|
|
|
4428
4613
|
if (ideaTransitionViolation) {
|
|
4429
4614
|
violations.push(ideaTransitionViolation);
|
|
4430
4615
|
}
|
|
4616
|
+
violations.push(...validateWorkflowTaskBodySection(filePath, content, ideasPath, fileSystem));
|
|
4431
4617
|
}
|
|
4432
4618
|
}
|
|
4433
4619
|
const principlesPath = `${dustPath}/principles`;
|
|
@@ -5376,7 +5562,7 @@ async function prePush(dependencies, gitRunner = defaultGitRunner, env = process
|
|
|
5376
5562
|
if (agent2.type === "unknown") {
|
|
5377
5563
|
return { exitCode: 0 };
|
|
5378
5564
|
}
|
|
5379
|
-
if (env
|
|
5565
|
+
if (isUnattended(env)) {
|
|
5380
5566
|
const uncommittedFiles = await getUncommittedFiles(context.cwd, gitRunner);
|
|
5381
5567
|
if (uncommittedFiles.length > 0) {
|
|
5382
5568
|
context.stderr("");
|
|
@@ -5446,7 +5632,7 @@ var commandRegistry = {
|
|
|
5446
5632
|
check,
|
|
5447
5633
|
agent,
|
|
5448
5634
|
audit,
|
|
5449
|
-
bucket,
|
|
5635
|
+
"bucket worker": bucketWorker,
|
|
5450
5636
|
"bucket asset upload": bucketAssetUpload,
|
|
5451
5637
|
focus,
|
|
5452
5638
|
"new task": newTask,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@joshski/dust",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.67",
|
|
4
4
|
"description": "Flow state for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -29,12 +29,17 @@
|
|
|
29
29
|
"./filesystem": {
|
|
30
30
|
"types": "./dist/filesystem/types.d.ts"
|
|
31
31
|
},
|
|
32
|
-
"./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs"
|
|
32
|
+
"./istanbul/minimal-reporter": "./lib/istanbul/minimal-reporter.cjs",
|
|
33
|
+
"./biome": {
|
|
34
|
+
"import": "./dist/biome.js",
|
|
35
|
+
"types": "./dist/biome/index.d.ts"
|
|
36
|
+
}
|
|
33
37
|
},
|
|
34
38
|
"files": [
|
|
35
39
|
"dist",
|
|
36
40
|
"bin",
|
|
37
|
-
"lib/istanbul/minimal-reporter.cjs"
|
|
41
|
+
"lib/istanbul/minimal-reporter.cjs",
|
|
42
|
+
"biome"
|
|
38
43
|
],
|
|
39
44
|
"repository": {
|
|
40
45
|
"type": "git",
|
|
@@ -58,9 +63,8 @@
|
|
|
58
63
|
"@biomejs/biome": "^2.3.13",
|
|
59
64
|
"@types/bun": "^1.3.6",
|
|
60
65
|
"@vitest/coverage-v8": "^4.0.18",
|
|
66
|
+
"istanbul-lib-coverage": "^3.2.2",
|
|
67
|
+
"istanbul-lib-report": "^3.0.1",
|
|
61
68
|
"vitest": "^4.0.18"
|
|
62
|
-
},
|
|
63
|
-
"dependencies": {
|
|
64
|
-
"@joshski/dust": "^0.1.49"
|
|
65
69
|
}
|
|
66
70
|
}
|