@joshski/dust 0.1.69 → 0.1.71
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/artifacts/index.d.ts +5 -5
- package/dist/artifacts/workflow-tasks.d.ts +9 -4
- package/dist/artifacts.js +59 -19
- package/dist/audits.js +57 -0
- package/dist/dust.js +73 -11
- package/package.json +1 -1
|
@@ -3,9 +3,9 @@ import { type Fact } from './facts';
|
|
|
3
3
|
import { type Idea, type IdeaOpenQuestion, type IdeaOption, parseOpenQuestions } from './ideas';
|
|
4
4
|
import { type Principle } from './principles';
|
|
5
5
|
import { type Task } from './tasks';
|
|
6
|
-
import { CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllCaptureIdeaTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch } from './workflow-tasks';
|
|
7
|
-
export type { CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, };
|
|
8
|
-
export { CAPTURE_IDEA_PREFIX, findAllCaptureIdeaTasks, parseOpenQuestions };
|
|
6
|
+
import { type AllWorkflowTasks, CAPTURE_IDEA_PREFIX, type CreateIdeaTransitionTaskResult, type DecomposeIdeaOptions, findAllCaptureIdeaTasks, findAllWorkflowTasks, type IdeaInProgress, type OpenQuestionResponse, type ParsedCaptureIdeaTask, type WorkflowTaskMatch } from './workflow-tasks';
|
|
7
|
+
export type { AllWorkflowTasks, CreateIdeaTransitionTaskResult, DecomposeIdeaOptions, Fact, Idea, IdeaOpenQuestion, IdeaOption, OpenQuestionResponse, ParsedCaptureIdeaTask, Principle, Task, WorkflowTaskMatch, };
|
|
8
|
+
export { CAPTURE_IDEA_PREFIX, findAllCaptureIdeaTasks, findAllWorkflowTasks, parseOpenQuestions, };
|
|
9
9
|
export type { IdeaInProgress };
|
|
10
10
|
export interface ArtifactsRepository {
|
|
11
11
|
parseIdea(options: {
|
|
@@ -33,10 +33,10 @@ export interface ArtifactsRepository {
|
|
|
33
33
|
ideaSlug: string;
|
|
34
34
|
description?: string;
|
|
35
35
|
}): Promise<CreateIdeaTransitionTaskResult>;
|
|
36
|
-
|
|
36
|
+
createIdeaTask(options: {
|
|
37
37
|
title: string;
|
|
38
38
|
description: string;
|
|
39
|
-
|
|
39
|
+
expedite?: boolean;
|
|
40
40
|
}): Promise<CreateIdeaTransitionTaskResult>;
|
|
41
41
|
findWorkflowTaskForIdea(options: {
|
|
42
42
|
ideaSlug: string;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FileSystem, ReadableFileSystem } from '../filesystem/types';
|
|
2
2
|
export declare const IDEA_TRANSITION_PREFIXES: string[];
|
|
3
3
|
export declare const CAPTURE_IDEA_PREFIX = "Add Idea: ";
|
|
4
|
-
export declare const
|
|
4
|
+
export declare const EXPEDITE_IDEA_PREFIX = "Expedite Idea: ";
|
|
5
5
|
export interface IdeaInProgress {
|
|
6
6
|
taskSlug: string;
|
|
7
7
|
ideaTitle: string;
|
|
@@ -9,7 +9,7 @@ export interface IdeaInProgress {
|
|
|
9
9
|
export interface ParsedCaptureIdeaTask {
|
|
10
10
|
ideaTitle: string;
|
|
11
11
|
ideaDescription: string;
|
|
12
|
-
|
|
12
|
+
expedite: boolean;
|
|
13
13
|
}
|
|
14
14
|
export declare function findAllCaptureIdeaTasks(fileSystem: ReadableFileSystem, dustPath: string): Promise<IdeaInProgress[]>;
|
|
15
15
|
/**
|
|
@@ -28,6 +28,11 @@ export interface WorkflowTaskMatch {
|
|
|
28
28
|
ideaSlug: string;
|
|
29
29
|
taskSlug: string;
|
|
30
30
|
}
|
|
31
|
+
export interface AllWorkflowTasks {
|
|
32
|
+
captureIdeaTasks: IdeaInProgress[];
|
|
33
|
+
workflowTasksByIdeaSlug: Map<string, WorkflowTaskMatch>;
|
|
34
|
+
}
|
|
35
|
+
export declare function findAllWorkflowTasks(fileSystem: ReadableFileSystem, dustPath: string): Promise<AllWorkflowTasks>;
|
|
31
36
|
export declare function findWorkflowTaskForIdea(fileSystem: ReadableFileSystem, dustPath: string, ideaSlug: string): Promise<WorkflowTaskMatch | null>;
|
|
32
37
|
export interface CreateIdeaTransitionTaskResult {
|
|
33
38
|
filePath: string;
|
|
@@ -44,9 +49,9 @@ export interface DecomposeIdeaOptions {
|
|
|
44
49
|
export declare function createRefineIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string): Promise<CreateIdeaTransitionTaskResult>;
|
|
45
50
|
export declare function decomposeIdea(fileSystem: FileSystem, dustPath: string, options: DecomposeIdeaOptions): Promise<CreateIdeaTransitionTaskResult>;
|
|
46
51
|
export declare function createShelveIdeaTask(fileSystem: FileSystem, dustPath: string, ideaSlug: string, description?: string): Promise<CreateIdeaTransitionTaskResult>;
|
|
47
|
-
export declare function
|
|
52
|
+
export declare function createIdeaTask(fileSystem: FileSystem, dustPath: string, options: {
|
|
48
53
|
title: string;
|
|
49
54
|
description: string;
|
|
50
|
-
|
|
55
|
+
expedite?: boolean;
|
|
51
56
|
}): Promise<CreateIdeaTransitionTaskResult>;
|
|
52
57
|
export declare function parseCaptureIdeaTask(fileSystem: ReadableFileSystem, dustPath: string, taskSlug: string): Promise<ParsedCaptureIdeaTask | null>;
|
package/dist/artifacts.js
CHANGED
|
@@ -272,7 +272,7 @@ async function parseTask(fileSystem, dustPath, slug) {
|
|
|
272
272
|
|
|
273
273
|
// lib/artifacts/workflow-tasks.ts
|
|
274
274
|
var CAPTURE_IDEA_PREFIX = "Add Idea: ";
|
|
275
|
-
var
|
|
275
|
+
var EXPEDITE_IDEA_PREFIX = "Expedite Idea: ";
|
|
276
276
|
async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
|
|
277
277
|
const tasksPath = `${dustPath}/tasks`;
|
|
278
278
|
if (!fileSystem.exists(tasksPath))
|
|
@@ -290,10 +290,10 @@ async function findAllCaptureIdeaTasks(fileSystem, dustPath) {
|
|
|
290
290
|
taskSlug: file.replace(/\.md$/, ""),
|
|
291
291
|
ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
|
|
292
292
|
});
|
|
293
|
-
} else if (title.startsWith(
|
|
293
|
+
} else if (title.startsWith(EXPEDITE_IDEA_PREFIX)) {
|
|
294
294
|
results.push({
|
|
295
295
|
taskSlug: file.replace(/\.md$/, ""),
|
|
296
|
-
ideaTitle: title.slice(
|
|
296
|
+
ideaTitle: title.slice(EXPEDITE_IDEA_PREFIX.length)
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
}
|
|
@@ -331,6 +331,45 @@ function extractIdeaSlugFromSection(content, sectionHeading) {
|
|
|
331
331
|
}
|
|
332
332
|
return null;
|
|
333
333
|
}
|
|
334
|
+
async function findAllWorkflowTasks(fileSystem, dustPath) {
|
|
335
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
336
|
+
const captureIdeaTasks = [];
|
|
337
|
+
const workflowTasksByIdeaSlug = new Map;
|
|
338
|
+
if (!fileSystem.exists(tasksPath)) {
|
|
339
|
+
return { captureIdeaTasks, workflowTasksByIdeaSlug };
|
|
340
|
+
}
|
|
341
|
+
const files = await fileSystem.readdir(tasksPath);
|
|
342
|
+
for (const file of files.filter((f) => f.endsWith(".md")).sort()) {
|
|
343
|
+
const content = await fileSystem.readFile(`${tasksPath}/${file}`);
|
|
344
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
345
|
+
if (!titleMatch)
|
|
346
|
+
continue;
|
|
347
|
+
const title = titleMatch[1].trim();
|
|
348
|
+
const taskSlug = file.replace(/\.md$/, "");
|
|
349
|
+
if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
|
|
350
|
+
captureIdeaTasks.push({
|
|
351
|
+
taskSlug,
|
|
352
|
+
ideaTitle: title.slice(CAPTURE_IDEA_PREFIX.length)
|
|
353
|
+
});
|
|
354
|
+
} else if (title.startsWith(EXPEDITE_IDEA_PREFIX)) {
|
|
355
|
+
captureIdeaTasks.push({
|
|
356
|
+
taskSlug,
|
|
357
|
+
ideaTitle: title.slice(EXPEDITE_IDEA_PREFIX.length)
|
|
358
|
+
});
|
|
359
|
+
}
|
|
360
|
+
for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
|
|
361
|
+
const linkedSlug = extractIdeaSlugFromSection(content, heading);
|
|
362
|
+
if (linkedSlug) {
|
|
363
|
+
workflowTasksByIdeaSlug.set(linkedSlug, {
|
|
364
|
+
type,
|
|
365
|
+
ideaSlug: linkedSlug,
|
|
366
|
+
taskSlug
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return { captureIdeaTasks, workflowTasksByIdeaSlug };
|
|
372
|
+
}
|
|
334
373
|
async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
|
|
335
374
|
const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
|
|
336
375
|
if (!fileSystem.exists(ideaPath)) {
|
|
@@ -405,7 +444,7 @@ ${definitionOfDone.map((item) => `- [ ] ${item}`).join(`
|
|
|
405
444
|
`)}
|
|
406
445
|
`;
|
|
407
446
|
}
|
|
408
|
-
async function
|
|
447
|
+
async function createIdeaTransitionTask(fileSystem, dustPath, prefix, ideaSlug, openingSentenceTemplate, definitionOfDone, ideaSectionHeading, taskOptions) {
|
|
409
448
|
const ideaTitle = await readIdeaTitle(fileSystem, dustPath, ideaSlug);
|
|
410
449
|
const taskTitle = `${prefix}${ideaTitle}`;
|
|
411
450
|
const filename = titleToFilename(taskTitle);
|
|
@@ -420,7 +459,7 @@ async function createIdeaTask(fileSystem, dustPath, prefix, ideaSlug, openingSen
|
|
|
420
459
|
return { filePath };
|
|
421
460
|
}
|
|
422
461
|
async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description) {
|
|
423
|
-
return
|
|
462
|
+
return createIdeaTransitionTask(fileSystem, dustPath, "Refine Idea: ", ideaSlug, (ideaTitle) => `Thoroughly research this idea and refine it into a well-defined proposal. Read the idea file, explore the codebase for relevant context, and identify any ambiguity. Where aspects are unclear or could go multiple ways, add open questions to the idea file. Review \`.dust/principles/\` for alignment and \`.dust/facts/\` for relevant design decisions. See [${ideaTitle}](../ideas/${ideaSlug}.md). 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.`, [
|
|
424
463
|
"Idea is thoroughly researched with relevant codebase context",
|
|
425
464
|
"Open questions are added for any ambiguous or underspecified aspects",
|
|
426
465
|
"Open questions follow the required heading format and focus on high-value decisions",
|
|
@@ -428,7 +467,7 @@ async function createRefineIdeaTask(fileSystem, dustPath, ideaSlug, description)
|
|
|
428
467
|
], "Refines Idea", { description });
|
|
429
468
|
}
|
|
430
469
|
async function decomposeIdea(fileSystem, dustPath, options) {
|
|
431
|
-
return
|
|
470
|
+
return createIdeaTransitionTask(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).`, [
|
|
432
471
|
"One or more new tasks are created in .dust/tasks/",
|
|
433
472
|
"Task's Principles section links to relevant principles from .dust/principles/",
|
|
434
473
|
"The original idea is deleted or updated to reflect remaining scope"
|
|
@@ -438,18 +477,18 @@ async function decomposeIdea(fileSystem, dustPath, options) {
|
|
|
438
477
|
});
|
|
439
478
|
}
|
|
440
479
|
async function createShelveIdeaTask(fileSystem, dustPath, ideaSlug, description) {
|
|
441
|
-
return
|
|
480
|
+
return createIdeaTransitionTask(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 });
|
|
442
481
|
}
|
|
443
|
-
async function
|
|
444
|
-
const { title, description,
|
|
482
|
+
async function createIdeaTask(fileSystem, dustPath, options) {
|
|
483
|
+
const { title, description, expedite } = options;
|
|
445
484
|
if (!title || !title.trim()) {
|
|
446
485
|
throw new Error("title is required and must not be whitespace-only");
|
|
447
486
|
}
|
|
448
487
|
if (!description || !description.trim()) {
|
|
449
488
|
throw new Error("description is required and must not be whitespace-only");
|
|
450
489
|
}
|
|
451
|
-
if (
|
|
452
|
-
const taskTitle2 = `${
|
|
490
|
+
if (expedite) {
|
|
491
|
+
const taskTitle2 = `${EXPEDITE_IDEA_PREFIX}${title}`;
|
|
453
492
|
const filename2 = titleToFilename(taskTitle2);
|
|
454
493
|
const filePath2 = `${dustPath}/tasks/${filename2}`;
|
|
455
494
|
const content2 = `# ${taskTitle2}
|
|
@@ -511,13 +550,13 @@ async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
|
|
|
511
550
|
}
|
|
512
551
|
const title = titleMatch[1].trim();
|
|
513
552
|
let ideaTitle;
|
|
514
|
-
let
|
|
515
|
-
if (title.startsWith(
|
|
516
|
-
ideaTitle = title.slice(
|
|
517
|
-
|
|
553
|
+
let expedite;
|
|
554
|
+
if (title.startsWith(EXPEDITE_IDEA_PREFIX)) {
|
|
555
|
+
ideaTitle = title.slice(EXPEDITE_IDEA_PREFIX.length);
|
|
556
|
+
expedite = true;
|
|
518
557
|
} else if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
|
|
519
558
|
ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
|
|
520
|
-
|
|
559
|
+
expedite = false;
|
|
521
560
|
} else {
|
|
522
561
|
return null;
|
|
523
562
|
}
|
|
@@ -526,7 +565,7 @@ async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
|
|
|
526
565
|
return null;
|
|
527
566
|
}
|
|
528
567
|
const ideaDescription = descriptionMatch[1];
|
|
529
|
-
return { ideaTitle, ideaDescription,
|
|
568
|
+
return { ideaTitle, ideaDescription, expedite };
|
|
530
569
|
}
|
|
531
570
|
|
|
532
571
|
// lib/artifacts/index.ts
|
|
@@ -585,8 +624,8 @@ function buildArtifactsRepository(fileSystem, dustPath) {
|
|
|
585
624
|
async createShelveIdeaTask(options) {
|
|
586
625
|
return createShelveIdeaTask(fileSystem, dustPath, options.ideaSlug, options.description);
|
|
587
626
|
},
|
|
588
|
-
async
|
|
589
|
-
return
|
|
627
|
+
async createIdeaTask(options) {
|
|
628
|
+
return createIdeaTask(fileSystem, dustPath, options);
|
|
590
629
|
},
|
|
591
630
|
async findWorkflowTaskForIdea(options) {
|
|
592
631
|
return findWorkflowTaskForIdea(fileSystem, dustPath, options.ideaSlug);
|
|
@@ -652,6 +691,7 @@ function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
|
|
|
652
691
|
}
|
|
653
692
|
export {
|
|
654
693
|
parseOpenQuestions,
|
|
694
|
+
findAllWorkflowTasks,
|
|
655
695
|
findAllCaptureIdeaTasks,
|
|
656
696
|
buildReadOnlyArtifactsRepository,
|
|
657
697
|
buildArtifactsRepository,
|
package/dist/audits.js
CHANGED
|
@@ -614,6 +614,62 @@ function errorHandling() {
|
|
|
614
614
|
- [ ] Proposed ideas for any error handling improvements identified
|
|
615
615
|
`;
|
|
616
616
|
}
|
|
617
|
+
function globalState() {
|
|
618
|
+
return dedent`
|
|
619
|
+
# Global State
|
|
620
|
+
|
|
621
|
+
Find global state and singletons that introduce hidden coupling and hurt testability.
|
|
622
|
+
|
|
623
|
+
${ideasHint}
|
|
624
|
+
|
|
625
|
+
## Scope
|
|
626
|
+
|
|
627
|
+
Focus on these patterns:
|
|
628
|
+
|
|
629
|
+
1. **Module-level mutable variables** - Variables declared outside functions that can be modified
|
|
630
|
+
2. **Singleton patterns** - Classes or objects that enforce a single instance (getInstance, static instance fields)
|
|
631
|
+
3. **Global registries** - Maps, arrays, or sets that accumulate state across module loads
|
|
632
|
+
4. **Implicit dependencies** - Functions that read from or write to module-level state instead of using parameters
|
|
633
|
+
5. **Shared configuration objects** - Mutable config objects imported and modified by multiple modules
|
|
634
|
+
6. **Lazy initialization with caching** - Values computed once and stored at module level
|
|
635
|
+
|
|
636
|
+
## Analysis Steps
|
|
637
|
+
|
|
638
|
+
1. Search for \`let\` and \`var\` declarations at module level (outside functions/classes)
|
|
639
|
+
2. Look for singleton patterns: \`getInstance\`, \`static instance\`, \`export const instance\`
|
|
640
|
+
3. Find module-level \`Map\`, \`Set\`, \`Array\`, or \`Object\` that gets modified
|
|
641
|
+
4. Identify functions that reference module-level variables not passed as parameters
|
|
642
|
+
5. Check for \`process.env\` access scattered throughout the codebase instead of centralized
|
|
643
|
+
|
|
644
|
+
## Output
|
|
645
|
+
|
|
646
|
+
For each global state instance identified, provide:
|
|
647
|
+
- **Location** - File path and line number
|
|
648
|
+
- **Pattern** - Which category (mutable variable, singleton, registry, etc.)
|
|
649
|
+
- **Impact** - How this affects testability or coupling (e.g., "Tests must reset this state", "Cannot run tests in parallel")
|
|
650
|
+
- **Suggestion** - How to refactor (e.g., "Pass as dependency", "Use factory function", "Move to function scope")
|
|
651
|
+
|
|
652
|
+
## Principles
|
|
653
|
+
|
|
654
|
+
- [Dependency Injection](../principles/dependency-injection.md) - Dependencies should be passed in, not accessed globally
|
|
655
|
+
- [Decoupled Code](../principles/decoupled-code.md) - Code should be organized into independent units
|
|
656
|
+
- [Test Isolation](../principles/test-isolation.md) - Tests should not affect each other
|
|
657
|
+
|
|
658
|
+
## Blocked By
|
|
659
|
+
|
|
660
|
+
(none)
|
|
661
|
+
|
|
662
|
+
## Definition of Done
|
|
663
|
+
|
|
664
|
+
- [ ] Searched for module-level mutable variables (let/var outside functions)
|
|
665
|
+
- [ ] Identified singleton patterns and getInstance methods
|
|
666
|
+
- [ ] Found global registries (Maps, Sets, Arrays modified at module level)
|
|
667
|
+
- [ ] Located functions with implicit dependencies on module-level state
|
|
668
|
+
- [ ] Checked for scattered process.env access
|
|
669
|
+
- [ ] Documented impact of each global state instance on testing
|
|
670
|
+
- [ ] Proposed ideas for refactoring global state to explicit dependencies
|
|
671
|
+
`;
|
|
672
|
+
}
|
|
617
673
|
function ubiquitousLanguage() {
|
|
618
674
|
return dedent`
|
|
619
675
|
# Ubiquitous Language
|
|
@@ -675,6 +731,7 @@ var stockAuditFunctions = {
|
|
|
675
731
|
"dead-code": deadCode,
|
|
676
732
|
"error-handling": errorHandling,
|
|
677
733
|
"facts-verification": factsVerification,
|
|
734
|
+
"global-state": globalState,
|
|
678
735
|
"ideas-from-commits": ideasFromCommits,
|
|
679
736
|
"ideas-from-principles": ideasFromPrinciples,
|
|
680
737
|
"performance-review": performanceReview,
|
package/dist/dust.js
CHANGED
|
@@ -275,7 +275,7 @@ 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.71";
|
|
279
279
|
|
|
280
280
|
// lib/session.ts
|
|
281
281
|
var DUST_UNATTENDED = "DUST_UNATTENDED";
|
|
@@ -537,6 +537,8 @@ ${vars.agentInstructions}` : "";
|
|
|
537
537
|
6. **Unclear** → \`${vars.bin} help\`
|
|
538
538
|
If none of the above clearly apply, run this to see all available commands.
|
|
539
539
|
|
|
540
|
+
Note: "tasks" here refers to dust task files in \`.dust/tasks/\`, not internal task tracking tools.
|
|
541
|
+
|
|
540
542
|
Do NOT proceed without running one of these commands.${instructions}
|
|
541
543
|
`;
|
|
542
544
|
}
|
|
@@ -1159,6 +1161,62 @@ function errorHandling() {
|
|
|
1159
1161
|
- [ ] Proposed ideas for any error handling improvements identified
|
|
1160
1162
|
`;
|
|
1161
1163
|
}
|
|
1164
|
+
function globalState() {
|
|
1165
|
+
return dedent`
|
|
1166
|
+
# Global State
|
|
1167
|
+
|
|
1168
|
+
Find global state and singletons that introduce hidden coupling and hurt testability.
|
|
1169
|
+
|
|
1170
|
+
${ideasHint}
|
|
1171
|
+
|
|
1172
|
+
## Scope
|
|
1173
|
+
|
|
1174
|
+
Focus on these patterns:
|
|
1175
|
+
|
|
1176
|
+
1. **Module-level mutable variables** - Variables declared outside functions that can be modified
|
|
1177
|
+
2. **Singleton patterns** - Classes or objects that enforce a single instance (getInstance, static instance fields)
|
|
1178
|
+
3. **Global registries** - Maps, arrays, or sets that accumulate state across module loads
|
|
1179
|
+
4. **Implicit dependencies** - Functions that read from or write to module-level state instead of using parameters
|
|
1180
|
+
5. **Shared configuration objects** - Mutable config objects imported and modified by multiple modules
|
|
1181
|
+
6. **Lazy initialization with caching** - Values computed once and stored at module level
|
|
1182
|
+
|
|
1183
|
+
## Analysis Steps
|
|
1184
|
+
|
|
1185
|
+
1. Search for \`let\` and \`var\` declarations at module level (outside functions/classes)
|
|
1186
|
+
2. Look for singleton patterns: \`getInstance\`, \`static instance\`, \`export const instance\`
|
|
1187
|
+
3. Find module-level \`Map\`, \`Set\`, \`Array\`, or \`Object\` that gets modified
|
|
1188
|
+
4. Identify functions that reference module-level variables not passed as parameters
|
|
1189
|
+
5. Check for \`process.env\` access scattered throughout the codebase instead of centralized
|
|
1190
|
+
|
|
1191
|
+
## Output
|
|
1192
|
+
|
|
1193
|
+
For each global state instance identified, provide:
|
|
1194
|
+
- **Location** - File path and line number
|
|
1195
|
+
- **Pattern** - Which category (mutable variable, singleton, registry, etc.)
|
|
1196
|
+
- **Impact** - How this affects testability or coupling (e.g., "Tests must reset this state", "Cannot run tests in parallel")
|
|
1197
|
+
- **Suggestion** - How to refactor (e.g., "Pass as dependency", "Use factory function", "Move to function scope")
|
|
1198
|
+
|
|
1199
|
+
## Principles
|
|
1200
|
+
|
|
1201
|
+
- [Dependency Injection](../principles/dependency-injection.md) - Dependencies should be passed in, not accessed globally
|
|
1202
|
+
- [Decoupled Code](../principles/decoupled-code.md) - Code should be organized into independent units
|
|
1203
|
+
- [Test Isolation](../principles/test-isolation.md) - Tests should not affect each other
|
|
1204
|
+
|
|
1205
|
+
## Blocked By
|
|
1206
|
+
|
|
1207
|
+
(none)
|
|
1208
|
+
|
|
1209
|
+
## Definition of Done
|
|
1210
|
+
|
|
1211
|
+
- [ ] Searched for module-level mutable variables (let/var outside functions)
|
|
1212
|
+
- [ ] Identified singleton patterns and getInstance methods
|
|
1213
|
+
- [ ] Found global registries (Maps, Sets, Arrays modified at module level)
|
|
1214
|
+
- [ ] Located functions with implicit dependencies on module-level state
|
|
1215
|
+
- [ ] Checked for scattered process.env access
|
|
1216
|
+
- [ ] Documented impact of each global state instance on testing
|
|
1217
|
+
- [ ] Proposed ideas for refactoring global state to explicit dependencies
|
|
1218
|
+
`;
|
|
1219
|
+
}
|
|
1162
1220
|
function ubiquitousLanguage() {
|
|
1163
1221
|
return dedent`
|
|
1164
1222
|
# Ubiquitous Language
|
|
@@ -1220,6 +1278,7 @@ var stockAuditFunctions = {
|
|
|
1220
1278
|
"dead-code": deadCode,
|
|
1221
1279
|
"error-handling": errorHandling,
|
|
1222
1280
|
"facts-verification": factsVerification,
|
|
1281
|
+
"global-state": globalState,
|
|
1223
1282
|
"ideas-from-commits": ideasFromCommits,
|
|
1224
1283
|
"ideas-from-principles": ideasFromPrinciples,
|
|
1225
1284
|
"performance-review": performanceReview,
|
|
@@ -2226,7 +2285,7 @@ var IDEA_TRANSITION_PREFIXES = [
|
|
|
2226
2285
|
"Decompose Idea: ",
|
|
2227
2286
|
"Shelve Idea: "
|
|
2228
2287
|
];
|
|
2229
|
-
var
|
|
2288
|
+
var EXPEDITE_IDEA_PREFIX = "Expedite Idea: ";
|
|
2230
2289
|
function titleToFilename(title) {
|
|
2231
2290
|
return `${title.toLowerCase().replace(/\./g, "-").replace(/[^a-z0-9\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "")}.md`;
|
|
2232
2291
|
}
|
|
@@ -2235,7 +2294,7 @@ function titleToFilename(title) {
|
|
|
2235
2294
|
function buildImplementationInstructions(bin, hooksInstalled, taskTitle) {
|
|
2236
2295
|
const steps = [];
|
|
2237
2296
|
let step = 1;
|
|
2238
|
-
const hasIdeaFile = !taskTitle?.startsWith(
|
|
2297
|
+
const hasIdeaFile = !taskTitle?.startsWith(EXPEDITE_IDEA_PREFIX);
|
|
2239
2298
|
steps.push(`Note: Do NOT run \`${bin} agent\`.`, "");
|
|
2240
2299
|
steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
|
|
2241
2300
|
step++;
|
|
@@ -3979,14 +4038,15 @@ function createDefaultUploadDependencies() {
|
|
|
3979
4038
|
const file = Bun.file(path);
|
|
3980
4039
|
return file.exists();
|
|
3981
4040
|
},
|
|
3982
|
-
uploadFile: async (url, token, fileBytes, contentType) => {
|
|
4041
|
+
uploadFile: async (url, token, fileBytes, contentType, fileName) => {
|
|
4042
|
+
const formData = new FormData;
|
|
4043
|
+
formData.append("file", new Blob([fileBytes.buffer], { type: contentType }), fileName);
|
|
3983
4044
|
const response = await fetch(url, {
|
|
3984
4045
|
method: "POST",
|
|
3985
4046
|
headers: {
|
|
3986
|
-
Authorization: `Bearer ${token}
|
|
3987
|
-
"Content-Type": contentType
|
|
4047
|
+
Authorization: `Bearer ${token}`
|
|
3988
4048
|
},
|
|
3989
|
-
body:
|
|
4049
|
+
body: formData
|
|
3990
4050
|
});
|
|
3991
4051
|
if (!response.ok) {
|
|
3992
4052
|
const text = await response.text();
|
|
@@ -4071,9 +4131,10 @@ async function bucketAssetUpload(dependencies, uploadDeps = createDefaultUploadD
|
|
|
4071
4131
|
}
|
|
4072
4132
|
const fileBytes = await uploadDeps.readFileBytes(filePath);
|
|
4073
4133
|
const contentType = getContentType(filePath);
|
|
4134
|
+
const fileName = filePath.split("/").pop();
|
|
4074
4135
|
const uploadUrl = `${getDustbucketHost()}/api/assets?repositoryId=${encodeURIComponent(repositoryId)}`;
|
|
4075
4136
|
try {
|
|
4076
|
-
const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType);
|
|
4137
|
+
const result = await uploadDeps.uploadFile(uploadUrl, token, fileBytes, contentType, fileName);
|
|
4077
4138
|
context.stdout(result.url);
|
|
4078
4139
|
return { exitCode: 0 };
|
|
4079
4140
|
} catch (error) {
|
|
@@ -5018,7 +5079,7 @@ async function runSingleCheck(check, cwd, runner) {
|
|
|
5018
5079
|
output: result.output,
|
|
5019
5080
|
hints: check.hints,
|
|
5020
5081
|
durationMs,
|
|
5021
|
-
timedOut: result.timedOut,
|
|
5082
|
+
timedOut: result.timedOut ?? false,
|
|
5022
5083
|
timeoutSeconds: timeoutMs / 1000
|
|
5023
5084
|
};
|
|
5024
5085
|
}
|
|
@@ -5057,7 +5118,8 @@ async function runValidationCheck(dependencies) {
|
|
|
5057
5118
|
output: outputLines.join(`
|
|
5058
5119
|
`),
|
|
5059
5120
|
isBuiltIn: true,
|
|
5060
|
-
durationMs
|
|
5121
|
+
durationMs,
|
|
5122
|
+
timedOut: false
|
|
5061
5123
|
};
|
|
5062
5124
|
}
|
|
5063
5125
|
function displayResults(results, context) {
|
|
@@ -5067,7 +5129,7 @@ function displayResults(results, context) {
|
|
|
5067
5129
|
if (result.timedOut) {
|
|
5068
5130
|
context.stdout(`✗ ${result.name} [timed out after ${result.timeoutSeconds}s]`);
|
|
5069
5131
|
} else {
|
|
5070
|
-
const timing = result.durationMs
|
|
5132
|
+
const timing = result.durationMs >= 1000 ? ` [${(result.durationMs / 1000).toFixed(1)}s]` : "";
|
|
5071
5133
|
if (result.exitCode === 0) {
|
|
5072
5134
|
context.stdout(`✓ ${result.name}${timing}`);
|
|
5073
5135
|
} else {
|