@joshski/dust 0.1.109 → 0.1.111
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/README.md +4 -0
- package/dist/artifacts/facts.d.ts +2 -5
- package/dist/artifacts/ideas.d.ts +2 -15
- package/dist/artifacts/index.d.ts +2 -38
- package/dist/artifacts/principles.d.ts +2 -7
- package/dist/artifacts/repository-principle-hierarchy.d.ts +16 -0
- package/dist/artifacts/tasks.d.ts +2 -8
- package/dist/artifacts/types.d.ts +98 -0
- package/dist/artifacts/workflow-tasks.d.ts +2 -16
- package/dist/artifacts.js +40 -4
- package/dist/audits.js +163 -0
- package/dist/bucket/repository-loop.d.ts +1 -1
- package/dist/bucket/repository-types.d.ts +58 -0
- package/dist/bucket/repository.d.ts +2 -52
- package/dist/bucket/server-messages.d.ts +8 -2
- package/dist/dust.js +644 -75
- package/dist/patch.js +5 -4
- package/dist/validation.js +5 -4
- package/package.json +1 -1
package/dist/dust.js
CHANGED
|
@@ -7,7 +7,7 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
|
7
7
|
var require_package = __commonJS((exports, module) => {
|
|
8
8
|
module.exports = {
|
|
9
9
|
name: "@joshski/dust",
|
|
10
|
-
version: "0.1.
|
|
10
|
+
version: "0.1.111",
|
|
11
11
|
description: "Flow state for AI coding agents",
|
|
12
12
|
type: "module",
|
|
13
13
|
bin: {
|
|
@@ -721,7 +721,7 @@ async function loadSettings(cwd, fileSystem, runtime) {
|
|
|
721
721
|
}
|
|
722
722
|
|
|
723
723
|
// lib/version.ts
|
|
724
|
-
var DUST_VERSION = "0.1.
|
|
724
|
+
var DUST_VERSION = "0.1.111";
|
|
725
725
|
|
|
726
726
|
// lib/cli/middleware.ts
|
|
727
727
|
function applyMiddleware(middlewares, execute) {
|
|
@@ -2895,6 +2895,168 @@ function slowTests() {
|
|
|
2895
2895
|
- No changes to files outside \`.dust/\`
|
|
2896
2896
|
`;
|
|
2897
2897
|
}
|
|
2898
|
+
function overAbstraction() {
|
|
2899
|
+
return dedent`
|
|
2900
|
+
# Over-Abstraction
|
|
2901
|
+
|
|
2902
|
+
Identify violations of the "reasonably-dry" principle where code has been over-engineered with excessive abstraction.
|
|
2903
|
+
|
|
2904
|
+
${ideasHint}
|
|
2905
|
+
|
|
2906
|
+
## Scope
|
|
2907
|
+
|
|
2908
|
+
Detect these over-abstraction patterns:
|
|
2909
|
+
|
|
2910
|
+
1. **Single-use abstractions** - Interfaces, base classes, or utility functions used in only one place
|
|
2911
|
+
2. **Deep inheritance hierarchies** - Classes extending more than 2 levels deep
|
|
2912
|
+
3. **Premature generalization** - Parameters always used with the same value, unused options/flags
|
|
2913
|
+
4. **Excessive indirection** - Multiple layers of wrappers adding no value
|
|
2914
|
+
|
|
2915
|
+
## Analysis Steps
|
|
2916
|
+
|
|
2917
|
+
### 1. Find Single-Use Abstractions
|
|
2918
|
+
|
|
2919
|
+
Search for abstractions that are only used once:
|
|
2920
|
+
|
|
2921
|
+
1. **Interfaces with one implementation**
|
|
2922
|
+
- Search for \`interface\` declarations
|
|
2923
|
+
- Check if each interface has only one implementing class
|
|
2924
|
+
- Flag interfaces that exist solely for testing (can be replaced with the concrete type)
|
|
2925
|
+
|
|
2926
|
+
2. **Base classes with one subclass**
|
|
2927
|
+
- Search for \`abstract class\` or classes used as base classes
|
|
2928
|
+
- Count implementations extending each base class
|
|
2929
|
+
- Flag base classes with only one subclass
|
|
2930
|
+
|
|
2931
|
+
3. **Utility functions called once**
|
|
2932
|
+
- Search for exported utility functions
|
|
2933
|
+
- Check call sites - if only called from one location, it's over-abstraction
|
|
2934
|
+
- Consider inlining single-use utilities
|
|
2935
|
+
|
|
2936
|
+
4. **Generic types with one concrete usage**
|
|
2937
|
+
- Find generic type parameters: \`<T>\`, \`<TData>\`, etc.
|
|
2938
|
+
- Check if T is always the same type at all call sites
|
|
2939
|
+
- Flag generics that could be concrete types
|
|
2940
|
+
|
|
2941
|
+
### 2. Detect Deep Inheritance Hierarchies
|
|
2942
|
+
|
|
2943
|
+
Find inheritance chains longer than 2 levels:
|
|
2944
|
+
|
|
2945
|
+
1. Search for \`extends\` keywords in class declarations
|
|
2946
|
+
2. Build inheritance tree for each class
|
|
2947
|
+
3. Flag chains deeper than 2 (A extends B extends C extends D...)
|
|
2948
|
+
4. Respect framework conventions (don't flag React.Component, etc.)
|
|
2949
|
+
|
|
2950
|
+
### 3. Identify Premature Generalization
|
|
2951
|
+
|
|
2952
|
+
Look for flexibility that's never used:
|
|
2953
|
+
|
|
2954
|
+
1. **Always-same parameter values**
|
|
2955
|
+
- Find function parameters
|
|
2956
|
+
- Check all call sites - if always the same value, it's not needed
|
|
2957
|
+
- Flag parameters that could be constants or removed
|
|
2958
|
+
|
|
2959
|
+
2. **Unused configuration options**
|
|
2960
|
+
- Search for configuration objects/interfaces
|
|
2961
|
+
- Check which options are actually used
|
|
2962
|
+
- Flag options that are never set or always default
|
|
2963
|
+
|
|
2964
|
+
3. **Unused function parameters**
|
|
2965
|
+
- Find parameters that aren't referenced in function bodies
|
|
2966
|
+
- Flag as candidates for removal
|
|
2967
|
+
|
|
2968
|
+
### 4. Find Excessive Indirection
|
|
2969
|
+
|
|
2970
|
+
Detect wrapper chains that add no value:
|
|
2971
|
+
|
|
2972
|
+
1. **Delegation chains**
|
|
2973
|
+
- Search for functions that only call another function
|
|
2974
|
+
- Flag wrappers that don't add logic, just forward calls
|
|
2975
|
+
- Example: \`function foo(x) { return bar(x) }\`
|
|
2976
|
+
|
|
2977
|
+
2. **Proxy patterns without behavior**
|
|
2978
|
+
- Find classes that wrap another class
|
|
2979
|
+
- Check if wrapper adds any logic beyond forwarding
|
|
2980
|
+
- Flag pure proxies
|
|
2981
|
+
|
|
2982
|
+
3. **Middleware without transformation**
|
|
2983
|
+
- Look for middleware/interceptor patterns
|
|
2984
|
+
- Check if they modify data or just pass through
|
|
2985
|
+
- Flag pass-through middleware
|
|
2986
|
+
|
|
2987
|
+
## Output Format
|
|
2988
|
+
|
|
2989
|
+
For each over-abstraction found, create an idea file in \`.dust/ideas/\` with:
|
|
2990
|
+
|
|
2991
|
+
\`\`\`markdown
|
|
2992
|
+
# Over-Abstraction: [Type] in [Location]
|
|
2993
|
+
|
|
2994
|
+
## Type
|
|
2995
|
+
|
|
2996
|
+
[Single-use | Deep hierarchy | Premature generalization | Excessive indirection]
|
|
2997
|
+
|
|
2998
|
+
## Location
|
|
2999
|
+
|
|
3000
|
+
\`\`\`
|
|
3001
|
+
[file path]:[line number]
|
|
3002
|
+
\`\`\`
|
|
3003
|
+
|
|
3004
|
+
## Description
|
|
3005
|
+
|
|
3006
|
+
[What the abstraction is]
|
|
3007
|
+
|
|
3008
|
+
## Problem
|
|
3009
|
+
|
|
3010
|
+
[Why this is over-abstraction - complexity without benefit]
|
|
3011
|
+
|
|
3012
|
+
## Usage Analysis
|
|
3013
|
+
|
|
3014
|
+
- **Times used**: [count]
|
|
3015
|
+
- **Variation in usage**: [how different are the use cases]
|
|
3016
|
+
- **Complexity cost**: [lines of code, indirection levels, etc.]
|
|
3017
|
+
|
|
3018
|
+
## Suggested Simplification
|
|
3019
|
+
|
|
3020
|
+
[How to remove or reduce this abstraction]
|
|
3021
|
+
|
|
3022
|
+
## Impact
|
|
3023
|
+
|
|
3024
|
+
[Lines of code saved, reduced complexity, improved clarity]
|
|
3025
|
+
\`\`\`
|
|
3026
|
+
|
|
3027
|
+
## Special Considerations
|
|
3028
|
+
|
|
3029
|
+
1. **Framework conventions** - Don't flag patterns mandated by frameworks:
|
|
3030
|
+
- React: Component base classes, hooks patterns
|
|
3031
|
+
- Express: Middleware signatures
|
|
3032
|
+
- Testing: Test base classes, fixture patterns
|
|
3033
|
+
|
|
3034
|
+
2. **Library boundaries** - Public API abstractions may be justified even if internal usage is simple
|
|
3035
|
+
|
|
3036
|
+
3. **Test code** - Apply the same standards to test code as production code
|
|
3037
|
+
|
|
3038
|
+
4. **Context depth thresholds**:
|
|
3039
|
+
- Deep hierarchies (>2 levels) make understanding difficult
|
|
3040
|
+
- Wrapper chains (>2 levels) obscure actual behavior
|
|
3041
|
+
- Generic parameters should have multiple concrete usages
|
|
3042
|
+
|
|
3043
|
+
## Blocked By
|
|
3044
|
+
|
|
3045
|
+
(none)
|
|
3046
|
+
|
|
3047
|
+
## Definition of Done
|
|
3048
|
+
|
|
3049
|
+
- Searched for single-use interfaces, base classes, and utility functions
|
|
3050
|
+
- Identified deep inheritance hierarchies (>2 levels)
|
|
3051
|
+
- Found parameters always used with the same value
|
|
3052
|
+
- Detected unused configuration options
|
|
3053
|
+
- Located excessive wrapper chains and delegation
|
|
3054
|
+
- Respected framework conventions (didn't flag framework-mandated patterns)
|
|
3055
|
+
- Created idea files for each over-abstraction found
|
|
3056
|
+
- Each idea includes usage analysis and simplification suggestions
|
|
3057
|
+
- No changes to files outside \`.dust/\`
|
|
3058
|
+
`;
|
|
3059
|
+
}
|
|
2898
3060
|
function primitiveObsession() {
|
|
2899
3061
|
return dedent`
|
|
2900
3062
|
# Primitive Obsession
|
|
@@ -4270,6 +4432,7 @@ var stockAuditFunctions = {
|
|
|
4270
4432
|
"idiomatic-style": idiomaticStyle,
|
|
4271
4433
|
"incidental-test-details": incidentalTestDetails,
|
|
4272
4434
|
"logging-and-traceability": loggingAndTraceability,
|
|
4435
|
+
"over-abstraction": overAbstraction,
|
|
4273
4436
|
"primitive-obsession": primitiveObsession,
|
|
4274
4437
|
"repository-context": repositoryContext,
|
|
4275
4438
|
"security-review": securityReview,
|
|
@@ -5754,6 +5917,8 @@ function buildImplementationInstructions(bin, hooksInstalled, taskTitle, taskPat
|
|
|
5754
5917
|
steps.push(`${step}. Run \`${bin} check\` to verify the project is in a good state`);
|
|
5755
5918
|
step++;
|
|
5756
5919
|
}
|
|
5920
|
+
steps.push(`${step}. If the task file contains Principles and Guidance sections, read and follow them before implementing changes`);
|
|
5921
|
+
step++;
|
|
5757
5922
|
steps.push(`${step}. Implement the task`);
|
|
5758
5923
|
step++;
|
|
5759
5924
|
if (!hooksInstalled) {
|
|
@@ -6081,6 +6246,55 @@ async function findAllWorkflowTasks(fileSystem, dustPath) {
|
|
|
6081
6246
|
}
|
|
6082
6247
|
return { captureIdeaTasks, workflowTasksByIdeaSlug };
|
|
6083
6248
|
}
|
|
6249
|
+
async function findWorkflowTaskForIdea(fileSystem, dustPath, ideaSlug) {
|
|
6250
|
+
const ideaPath = `${dustPath}/ideas/${ideaSlug}.md`;
|
|
6251
|
+
if (!fileSystem.exists(ideaPath)) {
|
|
6252
|
+
throw new Error(`Idea not found: "${ideaSlug}" (expected file at ${ideaPath})`);
|
|
6253
|
+
}
|
|
6254
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
6255
|
+
if (!fileSystem.exists(tasksPath)) {
|
|
6256
|
+
return null;
|
|
6257
|
+
}
|
|
6258
|
+
const files = await fileSystem.readdir(tasksPath);
|
|
6259
|
+
for (const file of files.filter((f) => f.endsWith(".md")).toSorted()) {
|
|
6260
|
+
const content = await fileSystem.readFile(`${tasksPath}/${file}`);
|
|
6261
|
+
const taskSlug = file.replace(/\.md$/, "");
|
|
6262
|
+
const match = findWorkflowMatch(content, ideaSlug, taskSlug);
|
|
6263
|
+
if (match) {
|
|
6264
|
+
return match;
|
|
6265
|
+
}
|
|
6266
|
+
}
|
|
6267
|
+
return null;
|
|
6268
|
+
}
|
|
6269
|
+
function findWorkflowMatch(content, ideaSlug, taskSlug) {
|
|
6270
|
+
const taskType = parseTaskType(content);
|
|
6271
|
+
if (taskType) {
|
|
6272
|
+
const heading = WORKFLOW_SECTION_HEADINGS.find((h) => h.type === taskType)?.heading;
|
|
6273
|
+
if (heading) {
|
|
6274
|
+
const linkedSlug = extractIdeaSlugFromSection(content, heading);
|
|
6275
|
+
if (linkedSlug === ideaSlug) {
|
|
6276
|
+
return {
|
|
6277
|
+
type: taskType,
|
|
6278
|
+
ideaSlug,
|
|
6279
|
+
taskSlug,
|
|
6280
|
+
resolvedQuestions: parseResolvedQuestions(content)
|
|
6281
|
+
};
|
|
6282
|
+
}
|
|
6283
|
+
}
|
|
6284
|
+
}
|
|
6285
|
+
for (const { type, heading } of WORKFLOW_SECTION_HEADINGS) {
|
|
6286
|
+
const linkedSlug = extractIdeaSlugFromSection(content, heading);
|
|
6287
|
+
if (linkedSlug === ideaSlug) {
|
|
6288
|
+
return {
|
|
6289
|
+
type,
|
|
6290
|
+
ideaSlug,
|
|
6291
|
+
taskSlug,
|
|
6292
|
+
resolvedQuestions: parseResolvedQuestions(content)
|
|
6293
|
+
};
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
return null;
|
|
6297
|
+
}
|
|
6084
6298
|
function parseResolvedQuestions(content) {
|
|
6085
6299
|
const lines = content.split(`
|
|
6086
6300
|
`);
|
|
@@ -6121,6 +6335,42 @@ function parseResolvedQuestions(content) {
|
|
|
6121
6335
|
}
|
|
6122
6336
|
return results;
|
|
6123
6337
|
}
|
|
6338
|
+
async function parseCaptureIdeaTask(fileSystem, dustPath, taskSlug) {
|
|
6339
|
+
const filePath = `${dustPath}/tasks/${taskSlug}.md`;
|
|
6340
|
+
if (!fileSystem.exists(filePath)) {
|
|
6341
|
+
return null;
|
|
6342
|
+
}
|
|
6343
|
+
const content = await fileSystem.readFile(filePath);
|
|
6344
|
+
const titleMatch = content.match(/^#\s+(.+)$/m);
|
|
6345
|
+
if (!titleMatch) {
|
|
6346
|
+
return null;
|
|
6347
|
+
}
|
|
6348
|
+
const title = titleMatch[1].trim();
|
|
6349
|
+
const taskType = parseTaskType(content);
|
|
6350
|
+
let ideaTitle;
|
|
6351
|
+
let expedite;
|
|
6352
|
+
if (taskType === "implement") {
|
|
6353
|
+
expedite = true;
|
|
6354
|
+
ideaTitle = title.startsWith(EXPEDITE_IDEA_PREFIX) ? title.slice(EXPEDITE_IDEA_PREFIX.length) : title;
|
|
6355
|
+
} else if (taskType === "capture") {
|
|
6356
|
+
expedite = false;
|
|
6357
|
+
ideaTitle = title.startsWith(CAPTURE_IDEA_PREFIX) ? title.slice(CAPTURE_IDEA_PREFIX.length) : title;
|
|
6358
|
+
} else if (title.startsWith(EXPEDITE_IDEA_PREFIX)) {
|
|
6359
|
+
ideaTitle = title.slice(EXPEDITE_IDEA_PREFIX.length);
|
|
6360
|
+
expedite = true;
|
|
6361
|
+
} else if (title.startsWith(CAPTURE_IDEA_PREFIX)) {
|
|
6362
|
+
ideaTitle = title.slice(CAPTURE_IDEA_PREFIX.length);
|
|
6363
|
+
expedite = false;
|
|
6364
|
+
} else {
|
|
6365
|
+
return null;
|
|
6366
|
+
}
|
|
6367
|
+
const descriptionMatch = content.match(/^## Idea Description\n\n([\s\S]*?)\n\n## /m);
|
|
6368
|
+
if (!descriptionMatch) {
|
|
6369
|
+
return null;
|
|
6370
|
+
}
|
|
6371
|
+
const ideaDescription = descriptionMatch[1];
|
|
6372
|
+
return { ideaTitle, ideaDescription, expedite };
|
|
6373
|
+
}
|
|
6124
6374
|
|
|
6125
6375
|
// lib/lint/validators/content-validator.ts
|
|
6126
6376
|
var REQUIRED_TASK_HEADINGS = ["Task Type", "Blocked By", "Definition of Done"];
|
|
@@ -9261,6 +9511,34 @@ function executeMessageEffects(effects, dependencies) {
|
|
|
9261
9511
|
}
|
|
9262
9512
|
}
|
|
9263
9513
|
|
|
9514
|
+
// lib/bucket/bucket-dependencies.ts
|
|
9515
|
+
function createAuthFileSystem(dependencies) {
|
|
9516
|
+
return {
|
|
9517
|
+
exists: (path3) => {
|
|
9518
|
+
try {
|
|
9519
|
+
dependencies.accessSync(path3);
|
|
9520
|
+
return true;
|
|
9521
|
+
} catch {
|
|
9522
|
+
return false;
|
|
9523
|
+
}
|
|
9524
|
+
},
|
|
9525
|
+
isDirectory: (path3) => {
|
|
9526
|
+
try {
|
|
9527
|
+
return dependencies.statSync(path3).isDirectory();
|
|
9528
|
+
} catch {
|
|
9529
|
+
return false;
|
|
9530
|
+
}
|
|
9531
|
+
},
|
|
9532
|
+
getFileCreationTime: (path3) => dependencies.statSync(path3).birthtimeMs,
|
|
9533
|
+
readFile: (path3) => dependencies.readFile(path3, "utf8"),
|
|
9534
|
+
writeFile: (path3, content) => dependencies.writeFile(path3, content, "utf8"),
|
|
9535
|
+
mkdir: (path3, options) => dependencies.mkdir(path3, options).then(() => {}),
|
|
9536
|
+
readdir: dependencies.readdir.bind(dependencies),
|
|
9537
|
+
chmod: dependencies.chmod.bind(dependencies),
|
|
9538
|
+
rename: dependencies.rename.bind(dependencies)
|
|
9539
|
+
};
|
|
9540
|
+
}
|
|
9541
|
+
|
|
9264
9542
|
// lib/bucket/native-io.ts
|
|
9265
9543
|
import { spawn as nodeSpawn4 } from "node:child_process";
|
|
9266
9544
|
import { EventEmitter } from "node:events";
|
|
@@ -9732,32 +10010,6 @@ function findRepoPathByRepositoryId(repositories, repositoryId) {
|
|
|
9732
10010
|
return;
|
|
9733
10011
|
}
|
|
9734
10012
|
var DEFAULT_DUSTBUCKET_WS_URL = "wss://dustbucket.com/agent/connect";
|
|
9735
|
-
function createAuthFileSystem(dependencies) {
|
|
9736
|
-
return {
|
|
9737
|
-
exists: (path3) => {
|
|
9738
|
-
try {
|
|
9739
|
-
dependencies.accessSync(path3);
|
|
9740
|
-
return true;
|
|
9741
|
-
} catch {
|
|
9742
|
-
return false;
|
|
9743
|
-
}
|
|
9744
|
-
},
|
|
9745
|
-
isDirectory: (path3) => {
|
|
9746
|
-
try {
|
|
9747
|
-
return dependencies.statSync(path3).isDirectory();
|
|
9748
|
-
} catch {
|
|
9749
|
-
return false;
|
|
9750
|
-
}
|
|
9751
|
-
},
|
|
9752
|
-
getFileCreationTime: (path3) => dependencies.statSync(path3).birthtimeMs,
|
|
9753
|
-
readFile: (path3) => dependencies.readFile(path3, "utf8"),
|
|
9754
|
-
writeFile: (path3, content) => dependencies.writeFile(path3, content, "utf8"),
|
|
9755
|
-
mkdir: (path3, options) => dependencies.mkdir(path3, options).then(() => {}),
|
|
9756
|
-
readdir: dependencies.readdir.bind(dependencies),
|
|
9757
|
-
chmod: dependencies.chmod.bind(dependencies),
|
|
9758
|
-
rename: dependencies.rename.bind(dependencies)
|
|
9759
|
-
};
|
|
9760
|
-
}
|
|
9761
10013
|
function createInitialState() {
|
|
9762
10014
|
const sessionId = crypto.randomUUID();
|
|
9763
10015
|
const systemBuffer = createLogBuffer();
|
|
@@ -10567,6 +10819,273 @@ Run \`dust bucket tool ${toolName}\` to see available operations.`);
|
|
|
10567
10819
|
// lib/cli/commands/lint-markdown.ts
|
|
10568
10820
|
import { isAbsolute, join as join11, relative, sep } from "node:path";
|
|
10569
10821
|
|
|
10822
|
+
// lib/artifacts/facts.ts
|
|
10823
|
+
async function parseFact(fileSystem, dustPath, slug) {
|
|
10824
|
+
const factPath = `${dustPath}/facts/${slug}.md`;
|
|
10825
|
+
if (!fileSystem.exists(factPath)) {
|
|
10826
|
+
throw new Error(`Fact not found: "${slug}" (expected file at ${factPath})`);
|
|
10827
|
+
}
|
|
10828
|
+
const content = await fileSystem.readFile(factPath);
|
|
10829
|
+
const title = extractTitle(content);
|
|
10830
|
+
if (!title) {
|
|
10831
|
+
throw new Error(`Fact file has no title: ${factPath}`);
|
|
10832
|
+
}
|
|
10833
|
+
return {
|
|
10834
|
+
slug,
|
|
10835
|
+
title,
|
|
10836
|
+
content
|
|
10837
|
+
};
|
|
10838
|
+
}
|
|
10839
|
+
|
|
10840
|
+
// lib/artifacts/ideas.ts
|
|
10841
|
+
function parseOpenQuestions(content) {
|
|
10842
|
+
const lines = content.split(`
|
|
10843
|
+
`);
|
|
10844
|
+
const questions = [];
|
|
10845
|
+
let inOpenQuestions = false;
|
|
10846
|
+
let currentQuestion = null;
|
|
10847
|
+
let currentOption = null;
|
|
10848
|
+
let descriptionLines = [];
|
|
10849
|
+
function flushOption() {
|
|
10850
|
+
if (currentOption) {
|
|
10851
|
+
currentOption.description = descriptionLines.join(`
|
|
10852
|
+
`).trim();
|
|
10853
|
+
descriptionLines = [];
|
|
10854
|
+
currentOption = null;
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
function flushQuestion() {
|
|
10858
|
+
flushOption();
|
|
10859
|
+
if (currentQuestion) {
|
|
10860
|
+
questions.push(currentQuestion);
|
|
10861
|
+
currentQuestion = null;
|
|
10862
|
+
}
|
|
10863
|
+
}
|
|
10864
|
+
let inCodeFence = false;
|
|
10865
|
+
for (const line of lines) {
|
|
10866
|
+
if (line.startsWith("```")) {
|
|
10867
|
+
inCodeFence = !inCodeFence;
|
|
10868
|
+
if (currentOption) {
|
|
10869
|
+
descriptionLines.push(line);
|
|
10870
|
+
}
|
|
10871
|
+
continue;
|
|
10872
|
+
}
|
|
10873
|
+
if (inCodeFence) {
|
|
10874
|
+
if (currentOption) {
|
|
10875
|
+
descriptionLines.push(line);
|
|
10876
|
+
}
|
|
10877
|
+
continue;
|
|
10878
|
+
}
|
|
10879
|
+
if (line.startsWith("## ")) {
|
|
10880
|
+
if (inOpenQuestions) {
|
|
10881
|
+
flushQuestion();
|
|
10882
|
+
}
|
|
10883
|
+
inOpenQuestions = line.trimEnd() === "## Open Questions";
|
|
10884
|
+
continue;
|
|
10885
|
+
}
|
|
10886
|
+
if (!inOpenQuestions)
|
|
10887
|
+
continue;
|
|
10888
|
+
if (line.startsWith("### ")) {
|
|
10889
|
+
flushQuestion();
|
|
10890
|
+
currentQuestion = {
|
|
10891
|
+
question: line.slice(4).trim(),
|
|
10892
|
+
options: []
|
|
10893
|
+
};
|
|
10894
|
+
continue;
|
|
10895
|
+
}
|
|
10896
|
+
if (line.startsWith("#### ")) {
|
|
10897
|
+
flushOption();
|
|
10898
|
+
currentOption = {
|
|
10899
|
+
name: line.slice(5).trim(),
|
|
10900
|
+
description: ""
|
|
10901
|
+
};
|
|
10902
|
+
if (currentQuestion) {
|
|
10903
|
+
currentQuestion.options.push(currentOption);
|
|
10904
|
+
}
|
|
10905
|
+
continue;
|
|
10906
|
+
}
|
|
10907
|
+
if (currentOption) {
|
|
10908
|
+
descriptionLines.push(line);
|
|
10909
|
+
}
|
|
10910
|
+
}
|
|
10911
|
+
flushQuestion();
|
|
10912
|
+
return questions;
|
|
10913
|
+
}
|
|
10914
|
+
async function parseIdea(fileSystem, dustPath, slug) {
|
|
10915
|
+
const ideaPath = `${dustPath}/ideas/${slug}.md`;
|
|
10916
|
+
if (!fileSystem.exists(ideaPath)) {
|
|
10917
|
+
throw new Error(`Idea not found: "${slug}" (expected file at ${ideaPath})`);
|
|
10918
|
+
}
|
|
10919
|
+
const content = await fileSystem.readFile(ideaPath);
|
|
10920
|
+
const title = extractTitle(content);
|
|
10921
|
+
if (!title) {
|
|
10922
|
+
throw new Error(`Idea file has no title: ${ideaPath}`);
|
|
10923
|
+
}
|
|
10924
|
+
const openingSentence = extractOpeningSentence(content);
|
|
10925
|
+
const openQuestions = parseOpenQuestions(content);
|
|
10926
|
+
return {
|
|
10927
|
+
slug,
|
|
10928
|
+
title,
|
|
10929
|
+
openingSentence,
|
|
10930
|
+
content,
|
|
10931
|
+
openQuestions
|
|
10932
|
+
};
|
|
10933
|
+
}
|
|
10934
|
+
|
|
10935
|
+
// lib/artifacts/principles.ts
|
|
10936
|
+
function extractLinksFromSection(content, sectionHeading) {
|
|
10937
|
+
const lines = content.split(`
|
|
10938
|
+
`);
|
|
10939
|
+
const links = [];
|
|
10940
|
+
let inSection = false;
|
|
10941
|
+
for (const line of lines) {
|
|
10942
|
+
if (line.startsWith("## ")) {
|
|
10943
|
+
inSection = line.trimEnd() === `## ${sectionHeading}`;
|
|
10944
|
+
continue;
|
|
10945
|
+
}
|
|
10946
|
+
if (!inSection)
|
|
10947
|
+
continue;
|
|
10948
|
+
if (line.startsWith("# "))
|
|
10949
|
+
break;
|
|
10950
|
+
const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
|
|
10951
|
+
if (linkMatch) {
|
|
10952
|
+
const target = linkMatch[2];
|
|
10953
|
+
const slugMatch = target.match(/([^/]+)\.md$/);
|
|
10954
|
+
if (slugMatch) {
|
|
10955
|
+
links.push(slugMatch[1]);
|
|
10956
|
+
}
|
|
10957
|
+
}
|
|
10958
|
+
}
|
|
10959
|
+
return links;
|
|
10960
|
+
}
|
|
10961
|
+
function extractSingleLinkFromSection(content, sectionHeading) {
|
|
10962
|
+
const links = extractLinksFromSection(content, sectionHeading);
|
|
10963
|
+
return links.length === 1 ? links[0] : null;
|
|
10964
|
+
}
|
|
10965
|
+
async function parsePrinciple(fileSystem, dustPath, slug) {
|
|
10966
|
+
const principlePath = `${dustPath}/principles/${slug}.md`;
|
|
10967
|
+
if (!fileSystem.exists(principlePath)) {
|
|
10968
|
+
throw new Error(`Principle not found: "${slug}" (expected file at ${principlePath})`);
|
|
10969
|
+
}
|
|
10970
|
+
const content = await fileSystem.readFile(principlePath);
|
|
10971
|
+
const title = extractTitle(content) || slug;
|
|
10972
|
+
const parentPrinciple = extractSingleLinkFromSection(content, "Parent Principle");
|
|
10973
|
+
const subPrinciples = extractLinksFromSection(content, "Sub-Principles");
|
|
10974
|
+
return {
|
|
10975
|
+
slug,
|
|
10976
|
+
title,
|
|
10977
|
+
content,
|
|
10978
|
+
parentPrinciple,
|
|
10979
|
+
subPrinciples
|
|
10980
|
+
};
|
|
10981
|
+
}
|
|
10982
|
+
|
|
10983
|
+
// lib/artifacts/tasks.ts
|
|
10984
|
+
function extractLinksFromSection2(content, sectionHeading) {
|
|
10985
|
+
const lines = content.split(`
|
|
10986
|
+
`);
|
|
10987
|
+
const links = [];
|
|
10988
|
+
let inSection = false;
|
|
10989
|
+
for (const line of lines) {
|
|
10990
|
+
if (line.startsWith("## ")) {
|
|
10991
|
+
inSection = line.trimEnd() === `## ${sectionHeading}`;
|
|
10992
|
+
continue;
|
|
10993
|
+
}
|
|
10994
|
+
if (!inSection)
|
|
10995
|
+
continue;
|
|
10996
|
+
if (line.startsWith("# "))
|
|
10997
|
+
break;
|
|
10998
|
+
const linkMatch = line.match(MARKDOWN_LINK_PATTERN);
|
|
10999
|
+
if (linkMatch) {
|
|
11000
|
+
const target = linkMatch[2];
|
|
11001
|
+
const slugMatch = target.match(/([^/]+)\.md$/);
|
|
11002
|
+
if (slugMatch) {
|
|
11003
|
+
links.push(slugMatch[1]);
|
|
11004
|
+
}
|
|
11005
|
+
}
|
|
11006
|
+
}
|
|
11007
|
+
return links;
|
|
11008
|
+
}
|
|
11009
|
+
function extractDefinitionOfDone(content) {
|
|
11010
|
+
const lines = content.split(`
|
|
11011
|
+
`);
|
|
11012
|
+
const items = [];
|
|
11013
|
+
let inSection = false;
|
|
11014
|
+
for (const line of lines) {
|
|
11015
|
+
if (line.startsWith("## ")) {
|
|
11016
|
+
inSection = line.trimEnd() === "## Definition of Done";
|
|
11017
|
+
continue;
|
|
11018
|
+
}
|
|
11019
|
+
if (!inSection)
|
|
11020
|
+
continue;
|
|
11021
|
+
if (line.startsWith("# "))
|
|
11022
|
+
break;
|
|
11023
|
+
const listMatch = line.match(/^-\s+(.+)$/);
|
|
11024
|
+
if (listMatch) {
|
|
11025
|
+
items.push(listMatch[1].trim());
|
|
11026
|
+
}
|
|
11027
|
+
}
|
|
11028
|
+
return items;
|
|
11029
|
+
}
|
|
11030
|
+
async function parseTask(fileSystem, dustPath, slug) {
|
|
11031
|
+
const taskPath = `${dustPath}/tasks/${slug}.md`;
|
|
11032
|
+
if (!fileSystem.exists(taskPath)) {
|
|
11033
|
+
throw new Error(`Task not found: "${slug}" (expected file at ${taskPath})`);
|
|
11034
|
+
}
|
|
11035
|
+
const content = await fileSystem.readFile(taskPath);
|
|
11036
|
+
const title = extractTitle(content);
|
|
11037
|
+
if (!title) {
|
|
11038
|
+
throw new Error(`Task file has no title: ${taskPath}`);
|
|
11039
|
+
}
|
|
11040
|
+
const principles = extractLinksFromSection2(content, "Principles");
|
|
11041
|
+
const blockedBy = extractLinksFromSection2(content, "Blocked By");
|
|
11042
|
+
const definitionOfDone = extractDefinitionOfDone(content);
|
|
11043
|
+
return {
|
|
11044
|
+
slug,
|
|
11045
|
+
title,
|
|
11046
|
+
content,
|
|
11047
|
+
principles,
|
|
11048
|
+
blockedBy,
|
|
11049
|
+
definitionOfDone
|
|
11050
|
+
};
|
|
11051
|
+
}
|
|
11052
|
+
|
|
11053
|
+
// lib/artifacts/repository-principle-hierarchy.ts
|
|
11054
|
+
function sortNodes(nodes) {
|
|
11055
|
+
nodes.sort((a, b) => a.title.localeCompare(b.title));
|
|
11056
|
+
for (const node of nodes) {
|
|
11057
|
+
sortNodes(node.children);
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
async function getRepositoryPrincipleHierarchy(repository) {
|
|
11061
|
+
const slugs = await repository.listPrinciples();
|
|
11062
|
+
if (slugs.length === 0) {
|
|
11063
|
+
return [];
|
|
11064
|
+
}
|
|
11065
|
+
const principles = await Promise.all(slugs.map((slug) => repository.parsePrinciple({ slug })));
|
|
11066
|
+
const principleSet = new Set(slugs);
|
|
11067
|
+
const nodeBySlug = new Map;
|
|
11068
|
+
for (const p of principles) {
|
|
11069
|
+
nodeBySlug.set(p.slug, {
|
|
11070
|
+
slug: p.slug,
|
|
11071
|
+
title: p.title,
|
|
11072
|
+
children: []
|
|
11073
|
+
});
|
|
11074
|
+
}
|
|
11075
|
+
const roots = [];
|
|
11076
|
+
for (const p of principles) {
|
|
11077
|
+
const node = nodeBySlug.get(p.slug);
|
|
11078
|
+
const parentSlug = p.parentPrinciple;
|
|
11079
|
+
if (!parentSlug || !principleSet.has(parentSlug)) {
|
|
11080
|
+
roots.push(node);
|
|
11081
|
+
} else {
|
|
11082
|
+
nodeBySlug.get(parentSlug).children.push(node);
|
|
11083
|
+
}
|
|
11084
|
+
}
|
|
11085
|
+
sortNodes(roots);
|
|
11086
|
+
return roots;
|
|
11087
|
+
}
|
|
11088
|
+
|
|
10570
11089
|
// lib/artifacts/index.ts
|
|
10571
11090
|
var ARTIFACT_TYPES = [
|
|
10572
11091
|
"facts",
|
|
@@ -10574,6 +11093,90 @@ var ARTIFACT_TYPES = [
|
|
|
10574
11093
|
"principles",
|
|
10575
11094
|
"tasks"
|
|
10576
11095
|
];
|
|
11096
|
+
function buildReadOperations(fileSystem, dustPath) {
|
|
11097
|
+
return {
|
|
11098
|
+
artifactPath(type, slug) {
|
|
11099
|
+
return `${dustPath}/${type}/${slug}.md`;
|
|
11100
|
+
},
|
|
11101
|
+
async parseIdea(options) {
|
|
11102
|
+
return parseIdea(fileSystem, dustPath, options.slug);
|
|
11103
|
+
},
|
|
11104
|
+
async listIdeas() {
|
|
11105
|
+
const ideasPath = `${dustPath}/ideas`;
|
|
11106
|
+
if (!fileSystem.exists(ideasPath)) {
|
|
11107
|
+
return [];
|
|
11108
|
+
}
|
|
11109
|
+
const files = await fileSystem.readdir(ideasPath);
|
|
11110
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).toSorted();
|
|
11111
|
+
},
|
|
11112
|
+
async parsePrinciple(options) {
|
|
11113
|
+
return parsePrinciple(fileSystem, dustPath, options.slug);
|
|
11114
|
+
},
|
|
11115
|
+
async listPrinciples() {
|
|
11116
|
+
const principlesPath = `${dustPath}/principles`;
|
|
11117
|
+
if (!fileSystem.exists(principlesPath)) {
|
|
11118
|
+
return [];
|
|
11119
|
+
}
|
|
11120
|
+
const files = await fileSystem.readdir(principlesPath);
|
|
11121
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).toSorted();
|
|
11122
|
+
},
|
|
11123
|
+
async parseFact(options) {
|
|
11124
|
+
return parseFact(fileSystem, dustPath, options.slug);
|
|
11125
|
+
},
|
|
11126
|
+
async listFacts() {
|
|
11127
|
+
const factsPath = `${dustPath}/facts`;
|
|
11128
|
+
if (!fileSystem.exists(factsPath)) {
|
|
11129
|
+
return [];
|
|
11130
|
+
}
|
|
11131
|
+
const files = await fileSystem.readdir(factsPath);
|
|
11132
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).toSorted();
|
|
11133
|
+
},
|
|
11134
|
+
async parseTask(options) {
|
|
11135
|
+
return parseTask(fileSystem, dustPath, options.slug);
|
|
11136
|
+
},
|
|
11137
|
+
async listTasks() {
|
|
11138
|
+
const tasksPath = `${dustPath}/tasks`;
|
|
11139
|
+
if (!fileSystem.exists(tasksPath)) {
|
|
11140
|
+
return [];
|
|
11141
|
+
}
|
|
11142
|
+
const files = await fileSystem.readdir(tasksPath);
|
|
11143
|
+
return files.filter((f) => f.endsWith(".md")).map((f) => f.replace(/\.md$/, "")).toSorted();
|
|
11144
|
+
},
|
|
11145
|
+
async findWorkflowTaskForIdea(options) {
|
|
11146
|
+
return findWorkflowTaskForIdea(fileSystem, dustPath, options.ideaSlug);
|
|
11147
|
+
},
|
|
11148
|
+
async parseCaptureIdeaTask(options) {
|
|
11149
|
+
return parseCaptureIdeaTask(fileSystem, dustPath, options.taskSlug);
|
|
11150
|
+
},
|
|
11151
|
+
async buildTaskGraph() {
|
|
11152
|
+
const taskSlugs = await this.listTasks();
|
|
11153
|
+
const allWorkflowTasks = await findAllWorkflowTasks(fileSystem, dustPath);
|
|
11154
|
+
const workflowTypeByTaskSlug = new Map;
|
|
11155
|
+
for (const match of allWorkflowTasks.workflowTasksByIdeaSlug.values()) {
|
|
11156
|
+
workflowTypeByTaskSlug.set(match.taskSlug, match.type);
|
|
11157
|
+
}
|
|
11158
|
+
const nodes = [];
|
|
11159
|
+
const edges = [];
|
|
11160
|
+
for (const slug of taskSlugs) {
|
|
11161
|
+
const task = await this.parseTask({ slug });
|
|
11162
|
+
nodes.push({
|
|
11163
|
+
task,
|
|
11164
|
+
workflowType: workflowTypeByTaskSlug.get(slug) ?? null
|
|
11165
|
+
});
|
|
11166
|
+
for (const blockerSlug of task.blockedBy) {
|
|
11167
|
+
edges.push({ from: blockerSlug, to: slug });
|
|
11168
|
+
}
|
|
11169
|
+
}
|
|
11170
|
+
return { nodes, edges };
|
|
11171
|
+
},
|
|
11172
|
+
async getRepositoryPrincipleHierarchy() {
|
|
11173
|
+
return getRepositoryPrincipleHierarchy(this);
|
|
11174
|
+
}
|
|
11175
|
+
};
|
|
11176
|
+
}
|
|
11177
|
+
function buildReadOnlyArtifactsRepository(fileSystem, dustPath) {
|
|
11178
|
+
return buildReadOperations(fileSystem, dustPath);
|
|
11179
|
+
}
|
|
10577
11180
|
|
|
10578
11181
|
// lib/lint/validators/directory-validator.ts
|
|
10579
11182
|
var EXPECTED_DIRECTORIES = [...ARTIFACT_TYPES, "config"];
|
|
@@ -12916,10 +13519,10 @@ Surprising behavior erodes trust and slows people down. Unsurprising behavior le
|
|
|
12916
13519
|
];
|
|
12917
13520
|
|
|
12918
13521
|
// lib/artifacts/core-principles.ts
|
|
12919
|
-
function
|
|
13522
|
+
function sortNodes2(nodes) {
|
|
12920
13523
|
nodes.sort((a, b) => a.title.localeCompare(b.title));
|
|
12921
13524
|
for (const node of nodes) {
|
|
12922
|
-
|
|
13525
|
+
sortNodes2(node.children);
|
|
12923
13526
|
}
|
|
12924
13527
|
}
|
|
12925
13528
|
function isInternalPrinciple(principleContent) {
|
|
@@ -12963,12 +13566,12 @@ function getCorePrincipleTree(allPrinciples, config) {
|
|
|
12963
13566
|
nodeBySlug.get(parentSlug).children.push(node);
|
|
12964
13567
|
}
|
|
12965
13568
|
}
|
|
12966
|
-
|
|
13569
|
+
sortNodes2(roots);
|
|
12967
13570
|
return roots;
|
|
12968
13571
|
}
|
|
12969
13572
|
|
|
12970
13573
|
// lib/core-principles.ts
|
|
12971
|
-
function
|
|
13574
|
+
function extractLinksFromSection3(content, sectionHeading) {
|
|
12972
13575
|
const lines = content.split(`
|
|
12973
13576
|
`);
|
|
12974
13577
|
const links = [];
|
|
@@ -12993,8 +13596,8 @@ function extractLinksFromSection(content, sectionHeading) {
|
|
|
12993
13596
|
}
|
|
12994
13597
|
return links;
|
|
12995
13598
|
}
|
|
12996
|
-
function
|
|
12997
|
-
const links =
|
|
13599
|
+
function extractSingleLinkFromSection2(content, sectionHeading) {
|
|
13600
|
+
const links = extractLinksFromSection3(content, sectionHeading);
|
|
12998
13601
|
return links.length === 1 ? links[0] : null;
|
|
12999
13602
|
}
|
|
13000
13603
|
function parsePrincipleContent(slug, content) {
|
|
@@ -13002,8 +13605,8 @@ function parsePrincipleContent(slug, content) {
|
|
|
13002
13605
|
if (!title) {
|
|
13003
13606
|
throw new Error(`Principle has no title: ${slug}`);
|
|
13004
13607
|
}
|
|
13005
|
-
const parentPrinciple =
|
|
13006
|
-
const subPrinciples =
|
|
13608
|
+
const parentPrinciple = extractSingleLinkFromSection2(content, "Parent Principle");
|
|
13609
|
+
const subPrinciples = extractLinksFromSection3(content, "Sub-Principles");
|
|
13007
13610
|
return {
|
|
13008
13611
|
slug,
|
|
13009
13612
|
title,
|
|
@@ -13212,7 +13815,6 @@ async function init(dependencies) {
|
|
|
13212
13815
|
}
|
|
13213
13816
|
|
|
13214
13817
|
// lib/cli/commands/list.ts
|
|
13215
|
-
import { basename as basename3 } from "node:path";
|
|
13216
13818
|
function workflowTypeToStatus(type) {
|
|
13217
13819
|
switch (type) {
|
|
13218
13820
|
case "refine":
|
|
@@ -13282,41 +13884,7 @@ function formatPrinciplesSection(header, entries) {
|
|
|
13282
13884
|
}
|
|
13283
13885
|
return lines;
|
|
13284
13886
|
}
|
|
13285
|
-
|
|
13286
|
-
const files = await fileSystem.readdir(principlesPath);
|
|
13287
|
-
const mdFiles = files.filter((f) => f.endsWith(".md"));
|
|
13288
|
-
const relationships = [];
|
|
13289
|
-
const titleMap = new Map;
|
|
13290
|
-
for (const file of mdFiles) {
|
|
13291
|
-
const filePath = `${principlesPath}/${file}`;
|
|
13292
|
-
const content = await fileSystem.readFile(filePath);
|
|
13293
|
-
const artifact = parseArtifact(filePath, content);
|
|
13294
|
-
relationships.push(extractPrincipleRelationships(artifact));
|
|
13295
|
-
const title = extractTitle(content) || basename3(file, ".md");
|
|
13296
|
-
titleMap.set(filePath, title);
|
|
13297
|
-
}
|
|
13298
|
-
const relMap = new Map;
|
|
13299
|
-
for (const rel of relationships) {
|
|
13300
|
-
relMap.set(rel.filePath, rel);
|
|
13301
|
-
}
|
|
13302
|
-
const rootPrinciples = relationships.filter((rel) => rel.parentPrinciples.length === 0);
|
|
13303
|
-
function buildNode(filePath) {
|
|
13304
|
-
const rel = relMap.get(filePath);
|
|
13305
|
-
const children = [];
|
|
13306
|
-
if (rel) {
|
|
13307
|
-
for (const childPath of rel.subPrinciples) {
|
|
13308
|
-
children.push(buildNode(childPath));
|
|
13309
|
-
}
|
|
13310
|
-
}
|
|
13311
|
-
return {
|
|
13312
|
-
filePath,
|
|
13313
|
-
title: titleMap.get(filePath) || basename3(filePath, ".md"),
|
|
13314
|
-
children
|
|
13315
|
-
};
|
|
13316
|
-
}
|
|
13317
|
-
return rootPrinciples.map((rel) => buildNode(rel.filePath));
|
|
13318
|
-
}
|
|
13319
|
-
function renderHierarchy(nodes, output, prefix = "") {
|
|
13887
|
+
function renderRepositoryPrincipleHierarchy(nodes, output, prefix = "") {
|
|
13320
13888
|
for (let i = 0;i < nodes.length; i++) {
|
|
13321
13889
|
const node = nodes[i];
|
|
13322
13890
|
const isLastNode = i === nodes.length - 1;
|
|
@@ -13324,7 +13892,7 @@ function renderHierarchy(nodes, output, prefix = "") {
|
|
|
13324
13892
|
const childPrefix = isLastNode ? " " : "│ ";
|
|
13325
13893
|
output(`${prefix}${connector}${node.title}`);
|
|
13326
13894
|
if (node.children.length > 0) {
|
|
13327
|
-
|
|
13895
|
+
renderRepositoryPrincipleHierarchy(node.children, output, prefix + childPrefix);
|
|
13328
13896
|
}
|
|
13329
13897
|
}
|
|
13330
13898
|
}
|
|
@@ -13434,9 +14002,10 @@ async function processPrinciplesList(context) {
|
|
|
13434
14002
|
stdout("");
|
|
13435
14003
|
}
|
|
13436
14004
|
if (hasLocalPrinciples) {
|
|
13437
|
-
const
|
|
14005
|
+
const repository = buildReadOnlyArtifactsRepository(fileSystem, dustPath);
|
|
14006
|
+
const localHierarchy = await repository.getRepositoryPrincipleHierarchy();
|
|
13438
14007
|
stdout(`${colors.bold}Local${colors.reset}`);
|
|
13439
|
-
|
|
14008
|
+
renderRepositoryPrincipleHierarchy(localHierarchy, (line) => stdout(line));
|
|
13440
14009
|
stdout("");
|
|
13441
14010
|
const collectedItems = [];
|
|
13442
14011
|
for (const file of localMdFiles) {
|