@akanjs/devkit 3.1.1-rc.9 → 3.1.1
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/CHANGELOG.md +117 -0
- package/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanApp/akanApp.host.test.ts +12 -0
- package/akanApp/akanApp.host.ts +12 -0
- package/akanConfig/akanConfig.test.ts +30 -0
- package/akanConfig/akanConfig.ts +10 -0
- package/akanConfig/productionLockfile.test.ts +31 -0
- package/akanConfig/productionLockfile.ts +2 -0
- package/akanContext.ts +129 -4
- package/applicationBuildRunner.test.ts +17 -0
- package/applicationBuildRunner.ts +3 -1
- package/biome.base.json +4 -1
- package/codeAgent/agent/CodeAgent.ts +4 -0
- package/codeAgent/agent/SubagentPool.ts +5 -0
- package/codeAgent/agent/akanSystemPrompt.ts +1 -1
- package/codeAgent/agent/codeAgent.test.ts +41 -2
- package/codeAgent/feedback/PreviewFeedback.ts +2 -2
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/index.ts +2 -2
- package/codeAgent/skills/akan-agent/SKILL.md +36 -0
- package/codeAgent/skills/akan-code/SKILL.md +1 -0
- package/codeAgent/skills/akan-debug/SKILL.md +40 -6
- package/codeAgent/skills/akan-deps/SKILL.md +1 -2
- package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
- package/codeAgent/skills/akan-page/SKILL.md +44 -0
- package/codeAgent/skills/akan-query/SKILL.md +38 -0
- package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
- package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
- package/codeAgent/skills/akan-transport/SKILL.md +40 -0
- package/codeAgent/skills/akan-validate/SKILL.md +13 -11
- package/codeAgent/tools/AkanCli.ts +2 -1
- package/codeAgent/tools/AkanCodePlugins.ts +5 -1
- package/codeAgent/tools/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/akanCodePlugins.test.ts +60 -58
- package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
- package/commandDecorators/workspaceVersionCheck.ts +4 -1
- package/commentDensityScanner.test.ts +121 -0
- package/commentDensityScanner.ts +101 -0
- package/componentSizeScanner.test.ts +36 -0
- package/componentSizeScanner.ts +52 -0
- package/componentWriteChainScanner.test.ts +137 -0
- package/componentWriteChainScanner.ts +104 -0
- package/conventionSet.test.ts +73 -0
- package/conventionSet.ts +67 -0
- package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
- package/deprecatedCommentScanner.test.ts +54 -0
- package/deprecatedCommentScanner.ts +30 -0
- package/endpointReach.test.ts +58 -0
- package/endpointReach.ts +80 -0
- package/executors.test.ts +0 -4
- package/executors.ts +18 -2
- package/index.ts +1 -0
- package/integration/devStability.integration.test.ts +9 -5
- package/kindBranchScanner.test.ts +89 -0
- package/kindBranchScanner.ts +158 -0
- package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
- package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
- package/lint/no-oversized-list-limit.grit +73 -0
- package/linter.test.ts +12 -0
- package/linter.ts +35 -12
- package/maintainabilityBaseline.test.ts +280 -0
- package/maintainabilityBaseline.ts +220 -0
- package/maintainabilityContract.test.ts +846 -0
- package/maintainabilityContract.ts +404 -0
- package/maintainabilityExceptionScanner.test.ts +90 -0
- package/maintainabilityExceptionScanner.ts +85 -0
- package/maintainabilityLimits.ts +60 -0
- package/maintainabilityReport.test.ts +130 -0
- package/maintainabilityReport.ts +153 -0
- package/mobile/desktopRelease.test.ts +1 -1
- package/mobile/desktopRelease.ts +3 -1
- package/package.json +3 -2
- package/platformTest/LinuxDockerTarget.ts +3 -0
- package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
- package/preview/PreviewView.ts +219 -0
- package/preview/index.ts +2 -0
- package/preview/previewView.test.ts +76 -0
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/routeSourceValidator.test.ts +11 -2
- package/routeSourceValidator.ts +14 -2
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- package/signalMutationIndex.test.ts +15 -0
- package/signalMutationIndex.ts +1 -1
- package/sourceComments.test.ts +45 -0
- package/sourceComments.ts +70 -0
- package/storeAccessScanner.test.ts +84 -0
- package/storeAccessScanner.ts +71 -0
- package/storeSource.ts +159 -0
- package/storeWriteScanner.test.ts +292 -0
- package/storeWriteScanner.ts +50 -0
- package/testOnlyExportScanner.test.ts +83 -0
- package/testOnlyExportScanner.ts +124 -0
- package/typeChecker.ts +32 -0
- package/typecheck/projectTypecheck.test.ts +18 -0
- package/typecheck/projectTypecheck.ts +29 -5
- package/typecheck/typecheck.proc.ts +6 -0
- package/typecheck/typecheckFailure.ts +13 -0
- package/types.ts +1 -0
- package/workflow/artifacts.ts +16 -2
- package/workflow/executor.ts +22 -7
- package/workflow/guardCatalog.test.ts +59 -0
- package/workflow/guardCatalog.ts +108 -0
- package/workflow/index.ts +4 -0
- package/workflow/interactionScaffold.ts +122 -0
- package/workflow/mutationScaffold.ts +519 -0
- package/workflow/mutationSource.test.ts +108 -0
- package/workflow/mutationSource.ts +219 -0
- package/workflow/plan.ts +54 -0
- package/workflow/render.ts +19 -4
- package/workflow/source.test.ts +185 -0
- package/workflow/source.ts +151 -24
- package/workflow/types.ts +18 -1
- package/workspaceLayout.ts +2 -0
- package/writePath.test.ts +109 -0
- package/writePath.ts +219 -0
- package/writePlacementScanner.test.ts +77 -0
- package/writePlacementScanner.ts +50 -0
- package/writingActionIndex.test.ts +99 -0
- package/writingActionIndex.ts +63 -0
- package/codeAgent/feedback/PreviewView.ts +0 -95
- /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
package/workflow/source.ts
CHANGED
|
@@ -41,6 +41,7 @@ export const moduleSourcePaths = (moduleName: string) => {
|
|
|
41
41
|
abstract: `lib/${moduleName}/${moduleName}.abstract.md`,
|
|
42
42
|
constant: `lib/${moduleName}/${moduleName}.constant.ts`,
|
|
43
43
|
dictionary: `lib/${moduleName}/${moduleName}.dictionary.ts`,
|
|
44
|
+
document: `lib/${moduleName}/${moduleName}.document.ts`,
|
|
44
45
|
service: `lib/${moduleName}/${moduleName}.service.ts`,
|
|
45
46
|
signal: `lib/${moduleName}/${moduleName}.signal.ts`,
|
|
46
47
|
store: `lib/${moduleName}/${moduleName}.store.ts`,
|
|
@@ -57,14 +58,12 @@ export const generatedFilesForSync = (sys: Sys, reason = "Generated files may ch
|
|
|
57
58
|
|
|
58
59
|
export const validationCommandsForTarget = (target: string) =>
|
|
59
60
|
[
|
|
60
|
-
{ command: `akan
|
|
61
|
-
{ command: `akan lint ${target}`, reason: "Validate formatting, imports, and static lint rules." },
|
|
61
|
+
{ command: `akan verify ${target}`, reason: "Sync, lint, typecheck and build the target in one run." },
|
|
62
62
|
] satisfies PrimitiveValidationCommand[];
|
|
63
63
|
|
|
64
64
|
export const nextActionsForTarget = (target: string) =>
|
|
65
65
|
[
|
|
66
|
-
{ command: `akan
|
|
67
|
-
{ command: `akan lint ${target}`, reason: "Validate the target after generated files are refreshed." },
|
|
66
|
+
{ command: `akan verify ${target}`, reason: "Refresh generated files and validate the target in one run." },
|
|
68
67
|
] satisfies PrimitiveNextAction[];
|
|
69
68
|
|
|
70
69
|
export const createPrimitiveWriteReport = ({
|
|
@@ -189,20 +188,52 @@ export const normalizeFieldType = (typeName: string) => {
|
|
|
189
188
|
return normalizedTypes[typeName.toLowerCase()] ?? typeName;
|
|
190
189
|
};
|
|
191
190
|
|
|
191
|
+
//? Biome's organizeImports order: case-insensitive, an uppercase name before its lowercase twin.
|
|
192
|
+
const importOrder = (a: string, b: string) => {
|
|
193
|
+
const [lowerA, lowerB] = [a.toLowerCase(), b.toLowerCase()];
|
|
194
|
+
if (lowerA !== lowerB) return lowerA < lowerB ? -1 : 1;
|
|
195
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
196
|
+
};
|
|
197
|
+
|
|
192
198
|
export const ensureNamedImport = (content: string, moduleSpecifier: string, name: string) => {
|
|
193
199
|
const source = sourceFileFor("source.ts", content);
|
|
194
|
-
const namedImport = findNamedImport(source, moduleSpecifier);
|
|
195
|
-
if (namedImport) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
200
|
+
const namedImport = findNamedImport(source, moduleSpecifier, { valueOnly: true });
|
|
201
|
+
if (!namedImport) return ensureImportDeclaration(content, moduleSpecifier, `{ ${name} }`);
|
|
202
|
+
if (namedImport.names.includes(name)) return content;
|
|
203
|
+
const nextElements = [...namedImport.elements, { name, text: name }].sort((a, b) => importOrder(a.name, b.name));
|
|
204
|
+
return spliceText(
|
|
205
|
+
content,
|
|
206
|
+
namedImport.namedBindingsStart,
|
|
207
|
+
namedImport.namedBindingsEnd,
|
|
208
|
+
`{ ${nextElements.map((element) => element.text).join(", ")} }`,
|
|
209
|
+
);
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
export const ensureImportDeclaration = (content: string, moduleSpecifier: string, clause: string) => {
|
|
213
|
+
const line = `import ${clause} from "${moduleSpecifier}";`;
|
|
214
|
+
if (content.includes(line)) return content;
|
|
215
|
+
const source = sourceFileFor("source.ts", content);
|
|
216
|
+
const imports = source.statements
|
|
217
|
+
.filter((statement): statement is ts.ImportDeclaration => ts.isImportDeclaration(statement))
|
|
218
|
+
.flatMap((statement) =>
|
|
219
|
+
ts.isStringLiteral(statement.moduleSpecifier)
|
|
220
|
+
? [{ specifier: statement.moduleSpecifier.text, start: statement.getStart(source), end: statement.getEnd() }]
|
|
221
|
+
: [],
|
|
203
222
|
);
|
|
223
|
+
const isRelative = (specifier: string) => specifier.startsWith(".");
|
|
224
|
+
const peers = imports.filter((entry) => isRelative(entry.specifier) === isRelative(moduleSpecifier));
|
|
225
|
+
const before = peers.find((entry) => importOrder(entry.specifier, moduleSpecifier) > 0);
|
|
226
|
+
if (before) {
|
|
227
|
+
const insertAt = lineStartAt(content, before.start);
|
|
228
|
+
return spliceText(content, insertAt, insertAt, `${line}\n`);
|
|
204
229
|
}
|
|
205
|
-
|
|
230
|
+
const after = peers.at(-1) ?? (isRelative(moduleSpecifier) ? imports.at(-1) : undefined);
|
|
231
|
+
if (after) {
|
|
232
|
+
const insertAt = lineEndAt(content, after.end);
|
|
233
|
+
return spliceText(content, insertAt, insertAt, peers.length ? `${line}\n` : `\n${line}\n`);
|
|
234
|
+
}
|
|
235
|
+
const insertAt = imports[0] ? lineStartAt(content, imports[0].start) : 0;
|
|
236
|
+
return spliceText(content, insertAt, insertAt, `${line}\n\n`);
|
|
206
237
|
};
|
|
207
238
|
|
|
208
239
|
export const ensureBaseImport = (content: string, name: string) => ensureNamedImport(content, "akanjs/base", name);
|
|
@@ -382,6 +413,7 @@ interface ArrayInsertionLocator {
|
|
|
382
413
|
|
|
383
414
|
interface NamedImportLocator {
|
|
384
415
|
names: string[];
|
|
416
|
+
elements: { name: string; text: string }[];
|
|
385
417
|
namedBindingsStart: number;
|
|
386
418
|
namedBindingsEnd: number;
|
|
387
419
|
}
|
|
@@ -499,16 +531,23 @@ const dictionaryChainOrderValid = (chainMethods: readonly string[]) => {
|
|
|
499
531
|
return true;
|
|
500
532
|
};
|
|
501
533
|
|
|
502
|
-
const findDictionaryModelObject = (source: ts.SourceFile, moduleClassName: string)
|
|
534
|
+
const findDictionaryModelObject = (source: ts.SourceFile, moduleClassName: string) =>
|
|
535
|
+
findDictionaryStageObject(source, "model", moduleClassName);
|
|
536
|
+
|
|
537
|
+
const findDictionaryStageObject = (
|
|
538
|
+
source: ts.SourceFile,
|
|
539
|
+
stage: string,
|
|
540
|
+
typeName: string,
|
|
541
|
+
): DictionaryModelLocator | null => {
|
|
503
542
|
let locator: DictionaryModelLocator | null = null;
|
|
504
543
|
const visit = (node: ts.Node) => {
|
|
505
544
|
if (locator) return;
|
|
506
|
-
if (!ts.isCallExpression(node) || callExpressionName(node) !==
|
|
545
|
+
if (!ts.isCallExpression(node) || callExpressionName(node) !== stage) {
|
|
507
546
|
ts.forEachChild(node, visit);
|
|
508
547
|
return;
|
|
509
548
|
}
|
|
510
549
|
const typeArgument = node.typeArguments?.[0];
|
|
511
|
-
if (!typeArgument || typeArgument.getText(source) !==
|
|
550
|
+
if (!typeArgument || typeArgument.getText(source) !== typeName) {
|
|
512
551
|
ts.forEachChild(node, visit);
|
|
513
552
|
return;
|
|
514
553
|
}
|
|
@@ -556,14 +595,20 @@ const findLightProjectionArray = (source: ts.SourceFile, moduleClassName: string
|
|
|
556
595
|
return locator;
|
|
557
596
|
};
|
|
558
597
|
|
|
559
|
-
const findNamedImport = (
|
|
598
|
+
const findNamedImport = (
|
|
599
|
+
source: ts.SourceFile,
|
|
600
|
+
moduleSpecifier: string,
|
|
601
|
+
{ valueOnly = false }: { valueOnly?: boolean } = {},
|
|
602
|
+
): NamedImportLocator | null => {
|
|
560
603
|
for (const statement of source.statements) {
|
|
561
604
|
if (!ts.isImportDeclaration(statement) || !ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
562
605
|
if (statement.moduleSpecifier.text !== moduleSpecifier) continue;
|
|
606
|
+
if (valueOnly && statement.importClause?.isTypeOnly) continue;
|
|
563
607
|
const namedBindings = statement.importClause?.namedBindings;
|
|
564
608
|
if (!namedBindings || !ts.isNamedImports(namedBindings)) continue;
|
|
565
609
|
return {
|
|
566
610
|
names: namedBindings.elements.map((element) => element.name.text),
|
|
611
|
+
elements: namedBindings.elements.map((element) => ({ name: element.name.text, text: element.getText(source) })),
|
|
567
612
|
namedBindingsStart: namedBindings.getStart(source),
|
|
568
613
|
namedBindingsEnd: namedBindings.getEnd(),
|
|
569
614
|
};
|
|
@@ -816,6 +861,27 @@ export const hasDictionaryModelField = (content: string, moduleClassName: string
|
|
|
816
861
|
return findDictionaryModelObject(source, moduleClassName)?.fields.some((field) => field.name === fieldName) ?? false;
|
|
817
862
|
};
|
|
818
863
|
|
|
864
|
+
export const hasDictionaryEndpointEntry = (content: string, moduleClassName: string, entryName: string) => {
|
|
865
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
866
|
+
return (
|
|
867
|
+
findDictionaryStageObject(source, "endpoint", `${moduleClassName}Endpoint`)?.fields.some(
|
|
868
|
+
(field) => field.name === entryName,
|
|
869
|
+
) ?? false
|
|
870
|
+
);
|
|
871
|
+
};
|
|
872
|
+
|
|
873
|
+
export const insertDictionaryEndpointEntry = (
|
|
874
|
+
content: string,
|
|
875
|
+
moduleClassName: string,
|
|
876
|
+
entryName: string,
|
|
877
|
+
entryText: string,
|
|
878
|
+
) => {
|
|
879
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
880
|
+
const locator = findDictionaryStageObject(source, "endpoint", `${moduleClassName}Endpoint`);
|
|
881
|
+
if (!locator) return null;
|
|
882
|
+
return insertOrderedFieldLine(content, locator, entryName, entryText, { fieldIndent: " ", closingIndent: " " });
|
|
883
|
+
};
|
|
884
|
+
|
|
819
885
|
export const ensureConstantTypeImport = (content: string, constantPath: string, typeName: string) => {
|
|
820
886
|
if (new RegExp(`import type \\{[^}]*\\b${typeName}\\b[^}]*\\} from "${constantPath}";`).test(content)) return content;
|
|
821
887
|
const importPattern = new RegExp(`import type \\{ ([^}]+) \\} from "${constantPath}";`);
|
|
@@ -830,17 +896,38 @@ export const ensureConstantTypeImport = (content: string, constantPath: string,
|
|
|
830
896
|
return `import type { ${typeName} } from "${constantPath}";\n${content}`;
|
|
831
897
|
};
|
|
832
898
|
|
|
899
|
+
const dictionaryChainOf = (node: ts.Node): ts.CallExpression | undefined => {
|
|
900
|
+
if (
|
|
901
|
+
ts.isCallExpression(node) &&
|
|
902
|
+
outermostFluentCall(node) === node &&
|
|
903
|
+
protectedDictionaryChainOrder.some((method) => chainMethodsForCall(node).includes(method))
|
|
904
|
+
)
|
|
905
|
+
return node;
|
|
906
|
+
return ts.forEachChild(node, dictionaryChainOf);
|
|
907
|
+
};
|
|
908
|
+
|
|
909
|
+
const chainMethodNames = (call: ts.Expression): ts.Identifier[] => {
|
|
910
|
+
if (!ts.isCallExpression(call) || !ts.isPropertyAccessExpression(call.expression)) return [];
|
|
911
|
+
const name = call.expression.name;
|
|
912
|
+
return [...chainMethodNames(call.expression.expression), ...(ts.isIdentifier(name) ? [name] : [])];
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
//* Located on the parsed chain, never by text search: the sample dictionary's header comment mentions `.error()`.
|
|
833
916
|
export const insertDictionaryEnum = (content: string, enumClassName: string, enumName: string, values: string[]) => {
|
|
834
917
|
if (content.includes(`.enum<${enumClassName}>("${enumName}"`)) return content;
|
|
918
|
+
const source = sourceFileFor("dictionary.ts", content);
|
|
919
|
+
if (hasParseDiagnostics(source)) return null;
|
|
920
|
+
const chain = dictionaryChainOf(source);
|
|
921
|
+
if (!chain) return null;
|
|
835
922
|
const enumBlock = ` .enum<${enumClassName}>("${enumName}", (t) => ({\n${values
|
|
836
923
|
.map((value) => ` ${value}: t([${JSON.stringify(titleize(value))}, ${JSON.stringify(titleize(value))}]),`)
|
|
837
924
|
.join("\n")}\n }))\n`;
|
|
838
|
-
const
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
return
|
|
925
|
+
const later = chainMethodNames(chain).find((name) => name.text === "error" || name.text === "translate");
|
|
926
|
+
if (!later) return spliceText(content, chain.getEnd(), chain.getEnd(), `\n${enumBlock.trimEnd()}`);
|
|
927
|
+
const dot = content.lastIndexOf(".", later.getStart(source));
|
|
928
|
+
const lineStart = lineStartAt(content, dot);
|
|
929
|
+
if (content.slice(lineStart, dot).trim() === "") return spliceText(content, lineStart, lineStart, enumBlock);
|
|
930
|
+
return spliceText(content, dot, dot, `\n${enumBlock}`);
|
|
844
931
|
};
|
|
845
932
|
|
|
846
933
|
export const parseValues = (value: string | null) =>
|
|
@@ -883,6 +970,19 @@ export const hasClassMethod = (content: string, className: string, methodName: s
|
|
|
883
970
|
);
|
|
884
971
|
};
|
|
885
972
|
|
|
973
|
+
export const classMethodParameters = (content: string, className: string, methodName: string) => {
|
|
974
|
+
const source = sourceFileFor("service.ts", content);
|
|
975
|
+
const method = findClassDeclaration(source, className)?.members.find(
|
|
976
|
+
(member): member is ts.MethodDeclaration => ts.isMethodDeclaration(member) && nodeName(member.name) === methodName,
|
|
977
|
+
);
|
|
978
|
+
return (
|
|
979
|
+
method?.parameters.map((parameter) => ({
|
|
980
|
+
name: nodeName(parameter.name),
|
|
981
|
+
optional: !!parameter.questionToken || !!parameter.initializer || !!parameter.dotDotDotToken,
|
|
982
|
+
})) ?? null
|
|
983
|
+
);
|
|
984
|
+
};
|
|
985
|
+
|
|
886
986
|
export const insertClassMethod = (content: string, className: string, methodBlock: string): string | null => {
|
|
887
987
|
const source = sourceFileFor("service.ts", content);
|
|
888
988
|
const node = findClassDeclaration(source, className);
|
|
@@ -976,3 +1076,30 @@ export const insertSignalFactoryEntry = (
|
|
|
976
1076
|
if (withEntry === content) return null;
|
|
977
1077
|
return paramEdit === "unchanged" ? withEntry : spliceText(withEntry, paramEdit.start, paramEdit.end, paramEdit.text);
|
|
978
1078
|
};
|
|
1079
|
+
|
|
1080
|
+
export const appendTopLevelTestCases = (
|
|
1081
|
+
content: string,
|
|
1082
|
+
describeTitle: string,
|
|
1083
|
+
render: (itName: string, indent: string) => string[],
|
|
1084
|
+
) => {
|
|
1085
|
+
const bunImport = findNamedImport(sourceFileFor("test.ts", content), "bun:test", { valueOnly: true });
|
|
1086
|
+
const itName = !bunImport?.names.includes("it") && bunImport?.names.includes("test") ? "test" : "it";
|
|
1087
|
+
const imported = ensureNamedImport(content, "bun:test", itName);
|
|
1088
|
+
const source = sourceFileFor("test.ts", imported);
|
|
1089
|
+
const body = source.statements.flatMap((statement) => {
|
|
1090
|
+
if (!ts.isExpressionStatement(statement) || !ts.isCallExpression(statement.expression)) return [];
|
|
1091
|
+
if (expressionName(statement.expression.expression) !== "describe") return [];
|
|
1092
|
+
const callback = factoryArrowOf(statement.expression);
|
|
1093
|
+
return callback && ts.isBlock(callback.body) ? [callback.body] : [];
|
|
1094
|
+
})[0];
|
|
1095
|
+
if (!body) {
|
|
1096
|
+
const block = [`describe(${JSON.stringify(describeTitle)}, () => {`, ...render(itName, " "), "});", ""];
|
|
1097
|
+
return `${ensureNamedImport(imported, "bun:test", "describe").trimEnd()}\n\n${block.join("\n")}`;
|
|
1098
|
+
}
|
|
1099
|
+
const firstStatement = body.statements[0];
|
|
1100
|
+
const indent = firstStatement
|
|
1101
|
+
? lineIndentAt(imported, firstStatement.getStart(source))
|
|
1102
|
+
: `${lineIndentAt(imported, body.getStart(source))} `;
|
|
1103
|
+
const insertAt = lineStartAt(imported, body.getEnd() - 1);
|
|
1104
|
+
return spliceText(imported, insertAt, insertAt, `${render(itName, indent).join("\n")}\n`);
|
|
1105
|
+
};
|
package/workflow/types.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type WorkflowFormat = "markdown" | "json";
|
|
|
8
8
|
export type WorkflowPlanInputs = Record<string, string | null>;
|
|
9
9
|
export type PrimitiveFormat = "markdown" | "json";
|
|
10
10
|
export type UiSurface = "view" | "unit" | "template";
|
|
11
|
-
export type WorkflowValidationKind = "sync" | "lint" | "typecheck" | "doctor" | "custom";
|
|
11
|
+
export type WorkflowValidationKind = "verify" | "sync" | "lint" | "typecheck" | "doctor" | "custom";
|
|
12
12
|
export type WorkflowFailureScope = "workspace-config" | "environment" | "source-change" | "unknown";
|
|
13
13
|
export type WorkflowValidationStatus = "passed" | "failed" | "unknown";
|
|
14
14
|
export interface WorkflowValidationSummary {
|
|
@@ -49,6 +49,14 @@ export interface AddEnumFieldInput extends PrimitiveTargetInput {
|
|
|
49
49
|
|
|
50
50
|
export interface AddMutationInput extends PrimitiveTargetInput {
|
|
51
51
|
mutation: string | null;
|
|
52
|
+
guards?: string[] | null;
|
|
53
|
+
withCalls?: string[] | null;
|
|
54
|
+
afterCalls?: string[] | null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CreateInteractionInput {
|
|
58
|
+
app: string | null;
|
|
59
|
+
interaction: string | null;
|
|
52
60
|
}
|
|
53
61
|
|
|
54
62
|
export interface AddSliceInput extends PrimitiveTargetInput {
|
|
@@ -94,6 +102,7 @@ export interface WorkflowSpec {
|
|
|
94
102
|
predictedChanges: readonly WorkflowPredictedChange[];
|
|
95
103
|
validation: readonly WorkflowValidation[];
|
|
96
104
|
completionCriteria: readonly string[];
|
|
105
|
+
replacesRules?: readonly string[];
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
export interface WorkflowDiagnostic {
|
|
@@ -382,6 +391,13 @@ export interface PrimitiveWriteReport {
|
|
|
382
391
|
nextActions: PrimitiveNextAction[];
|
|
383
392
|
}
|
|
384
393
|
|
|
394
|
+
export interface ScaffoldFile {
|
|
395
|
+
path: string;
|
|
396
|
+
content: string;
|
|
397
|
+
action: "create" | "modify";
|
|
398
|
+
reason: string;
|
|
399
|
+
}
|
|
400
|
+
|
|
385
401
|
export type PrimitiveFileMap = Record<string, { filename: string; content: string }>;
|
|
386
402
|
|
|
387
403
|
export interface WorkflowApplyReport {
|
|
@@ -482,4 +498,5 @@ export interface WorkflowPrimitiveOperations {
|
|
|
482
498
|
addEnumField: (input: AddEnumFieldInput) => Promise<PrimitiveWriteReport>;
|
|
483
499
|
addMutation: (input: AddMutationInput) => Promise<PrimitiveWriteReport>;
|
|
484
500
|
addSlice: (input: AddSliceInput) => Promise<PrimitiveWriteReport>;
|
|
501
|
+
createInteraction: (input: CreateInteractionInput) => Promise<PrimitiveWriteReport>;
|
|
485
502
|
}
|
package/workspaceLayout.ts
CHANGED
|
@@ -7,6 +7,7 @@ export const appRootAllowedFiles = new Set([
|
|
|
7
7
|
"AGENTS.md",
|
|
8
8
|
"CLAUDE.md",
|
|
9
9
|
"akan.app.json",
|
|
10
|
+
"akan.baseline.json",
|
|
10
11
|
"akan.config.ts",
|
|
11
12
|
"client.ts",
|
|
12
13
|
"main.ts",
|
|
@@ -38,6 +39,7 @@ export const libRootAllowedFiles = new Set([
|
|
|
38
39
|
"AGENTS.md",
|
|
39
40
|
"CLAUDE.md",
|
|
40
41
|
"README.md",
|
|
42
|
+
"akan.baseline.json",
|
|
41
43
|
"akan.config.ts",
|
|
42
44
|
"akan.lib.json",
|
|
43
45
|
"client.ts",
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import { WritePath } from "./writePath";
|
|
4
|
+
|
|
5
|
+
const pathIn = (body: string, enters?: (fn: ts.SignatureDeclaration) => boolean) => {
|
|
6
|
+
const sourceFile = ts.createSourceFile("a.ts", `async function run() {\n${body}\n}`, ts.ScriptTarget.Latest, true);
|
|
7
|
+
const fn = sourceFile.statements[0];
|
|
8
|
+
if (!fn || !ts.isFunctionDeclaration(fn) || !fn.body) throw new Error("fixture has no function body");
|
|
9
|
+
const isWrite = (call: ts.CallExpression) => /^write/.test(call.expression.getText(sourceFile));
|
|
10
|
+
const path = new WritePath(isWrite, { enters });
|
|
11
|
+
const calls = path.along(fn.body);
|
|
12
|
+
return { labels: calls.map((call) => path.labelOf(call, sourceFile)), count: path.countOf(calls) };
|
|
13
|
+
};
|
|
14
|
+
const writesIn = (body: string, enters?: (fn: ts.SignatureDeclaration) => boolean) =>
|
|
15
|
+
pathIn(body, enters).labels.map((label) => label.replace(" (per item)", ""));
|
|
16
|
+
|
|
17
|
+
describe("WritePath", () => {
|
|
18
|
+
test("adds up writes in sequence, nested callbacks and logical operands included", () => {
|
|
19
|
+
expect(
|
|
20
|
+
writesIn("await writeA();\nawait Promise.all(ids.map((id) => writeB(id)));\nok && (await writeC());"),
|
|
21
|
+
).toEqual(["writeA", "writeB", "writeC"]);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("counts only the busiest of exclusive branches", () => {
|
|
25
|
+
expect(writesIn("if (x) await writeA();\nelse await writeB();")).toEqual(["writeA"]);
|
|
26
|
+
expect(writesIn("await (x ? writeA() : Promise.all([writeB(), writeC()]));")).toEqual(["writeB", "writeC"]);
|
|
27
|
+
expect(
|
|
28
|
+
writesIn(
|
|
29
|
+
"switch (x) {\n case 1:\n await writeA();\n break;\n default:\n await writeB();\n await writeC();\n}",
|
|
30
|
+
),
|
|
31
|
+
).toEqual(["writeB", "writeC"]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("reads an if that returns as the else of the statements after it", () => {
|
|
35
|
+
expect(
|
|
36
|
+
writesIn("await writeA();\nif (x) {\n await writeB();\n return;\n}\nawait writeC();\nawait writeD();"),
|
|
37
|
+
).toEqual(["writeA", "writeC", "writeD"]);
|
|
38
|
+
expect(writesIn("if (x) await writeB();\nawait writeC();")).toEqual(["writeB", "writeC"]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("reads every branch that returns as exclusive of the statements after it, however it is nested", () => {
|
|
42
|
+
const countIn = (body: string) => pathIn(body).count;
|
|
43
|
+
expect(
|
|
44
|
+
countIn(
|
|
45
|
+
"if (x) {\n await writeA();\n return;\n} else if (y) {\n await writeB();\n return;\n}\nawait writeC();",
|
|
46
|
+
),
|
|
47
|
+
).toBe(1);
|
|
48
|
+
expect(countIn('switch (x) {\n case "a":\n await writeA();\n return;\n}\nawait writeC();')).toBe(1);
|
|
49
|
+
expect(countIn("if (x) {\n if (y) {\n await writeA();\n return;\n }\n}\nawait writeC();")).toBe(1);
|
|
50
|
+
expect(writesIn("try {\n await writeA();\n return;\n} catch {\n await writeB();\n}")).toEqual([
|
|
51
|
+
"writeA",
|
|
52
|
+
"writeB",
|
|
53
|
+
]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("runs a switch clause without break on into the next one", () => {
|
|
57
|
+
expect(
|
|
58
|
+
writesIn("switch (x) {\n case 1:\n await writeA();\n case 2:\n await writeB();\n break;\n}"),
|
|
59
|
+
).toEqual(["writeA", "writeB"]);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("counts a write in a loop or a per-element callback as two, but not the list it loops over", () => {
|
|
63
|
+
expect(pathIn("for (const id of ids) await writeA(id);")).toEqual({ labels: ["writeA (per item)"], count: 2 });
|
|
64
|
+
expect(pathIn("for (const id of await writeA()) read(id);")).toEqual({ labels: ["writeA"], count: 1 });
|
|
65
|
+
expect(pathIn("while (await more()) await writeA();")).toEqual({ labels: ["writeA (per item)"], count: 2 });
|
|
66
|
+
expect(pathIn("await Promise.all(ids.map((id) => writeA(id)));")).toEqual({
|
|
67
|
+
labels: ["writeA (per item)"],
|
|
68
|
+
count: 2,
|
|
69
|
+
});
|
|
70
|
+
expect(pathIn("await read().then(() => writeA());")).toEqual({ labels: ["writeA"], count: 1 });
|
|
71
|
+
expect(pathIn("for (let i = 0; i < ids.length; i++) await writeA(ids[i]);")).toEqual({
|
|
72
|
+
labels: ["writeA (per item)"],
|
|
73
|
+
count: 2,
|
|
74
|
+
});
|
|
75
|
+
expect(pathIn("while (queue.length) await writeA(queue.shift());")).toEqual({
|
|
76
|
+
labels: ["writeA (per item)"],
|
|
77
|
+
count: 2,
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("counts a retry or a poll as one write, and a loop that returns on its write as one", () => {
|
|
82
|
+
expect(pathIn("for (let i = 0; i < 3; i++) {\n try {\n await writeA();\n return;\n } catch {}\n}")).toEqual(
|
|
83
|
+
{
|
|
84
|
+
labels: ["writeA"],
|
|
85
|
+
count: 1,
|
|
86
|
+
},
|
|
87
|
+
);
|
|
88
|
+
expect(pathIn("let ok = false;\nwhile (!ok) ok = await writeA();")).toEqual({ labels: ["writeA"], count: 1 });
|
|
89
|
+
expect(pathIn("for (const id of ids)\n if (id) {\n await writeA(id);\n return;\n }")).toEqual({
|
|
90
|
+
labels: ["writeA"],
|
|
91
|
+
count: 1,
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("weighs a branch writing per item above one writing once", () => {
|
|
96
|
+
expect(pathIn("if (x) await writeA();\nelse for (const id of ids) await writeB(id);")).toEqual({
|
|
97
|
+
labels: ["writeB (per item)"],
|
|
98
|
+
count: 2,
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("leaves out a nested function the caller does not enter", () => {
|
|
103
|
+
const body = "await writeA();\nids.map((id) => writeB(id));\nsetTimeout(() => writeC(), 0);";
|
|
104
|
+
const entersMapOnly = (fn: ts.SignatureDeclaration) =>
|
|
105
|
+
ts.isCallExpression(fn.parent) && fn.parent.expression.getText().endsWith(".map");
|
|
106
|
+
|
|
107
|
+
expect(writesIn(body, entersMapOnly)).toEqual(["writeA", "writeB"]);
|
|
108
|
+
});
|
|
109
|
+
});
|
package/writePath.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
interface Flow {
|
|
4
|
+
through: ts.CallExpression[] | null;
|
|
5
|
+
exit: ts.CallExpression[] | null;
|
|
6
|
+
jump: ts.CallExpression[] | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
//? Exclusive branches never run together, so only the busiest one counts, a branch that returns included; a write in
|
|
10
|
+
//? a loop over items or a per-element callback runs once per item, so it counts as two.
|
|
11
|
+
export class WritePath {
|
|
12
|
+
static readonly #perElement = new Set([
|
|
13
|
+
"map",
|
|
14
|
+
"flatMap",
|
|
15
|
+
"forEach",
|
|
16
|
+
"filter",
|
|
17
|
+
"reduce",
|
|
18
|
+
"some",
|
|
19
|
+
"every",
|
|
20
|
+
"find",
|
|
21
|
+
"findIndex",
|
|
22
|
+
]);
|
|
23
|
+
readonly #matches: (call: ts.CallExpression) => boolean;
|
|
24
|
+
readonly #enters: (fn: ts.SignatureDeclaration) => boolean;
|
|
25
|
+
readonly #repeated = new WeakSet<ts.CallExpression>();
|
|
26
|
+
|
|
27
|
+
constructor(
|
|
28
|
+
matches: (call: ts.CallExpression) => boolean,
|
|
29
|
+
{ enters = () => true }: { enters?: (fn: ts.SignatureDeclaration) => boolean } = {},
|
|
30
|
+
) {
|
|
31
|
+
this.#matches = matches;
|
|
32
|
+
this.#enters = enters;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
along(node: ts.Node): ts.CallExpression[] {
|
|
36
|
+
if (ts.isFunctionLike(node) && !this.#enters(node)) return [];
|
|
37
|
+
if (WritePath.isPerElementCallback(node)) return this.#repeat(this.#children(node));
|
|
38
|
+
if (WritePath.#isFlowStatement(node)) return this.#busiestOf(this.#flowOf(node));
|
|
39
|
+
if (ts.isConditionalExpression(node))
|
|
40
|
+
return [...this.along(node.condition), ...this.#busier(this.along(node.whenTrue), this.along(node.whenFalse))];
|
|
41
|
+
const own = ts.isCallExpression(node) && this.#matches(node) ? [node] : [];
|
|
42
|
+
return [...own, ...this.#children(node)];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
isRepeated(call: ts.CallExpression) {
|
|
46
|
+
return this.#repeated.has(call);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
countOf(calls: ts.CallExpression[]) {
|
|
50
|
+
return calls.reduce((count, call) => count + (this.isRepeated(call) ? 2 : 1), 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
labelOf(call: ts.CallExpression, sourceFile: ts.SourceFile) {
|
|
54
|
+
const callee = call.expression.getText(sourceFile);
|
|
55
|
+
return this.isRepeated(call) ? `${callee} (per item)` : callee;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static isPerElementCallback(node: ts.Node) {
|
|
59
|
+
const call = node.parent;
|
|
60
|
+
return (
|
|
61
|
+
(ts.isArrowFunction(node) || ts.isFunctionExpression(node)) &&
|
|
62
|
+
!!call &&
|
|
63
|
+
ts.isCallExpression(call) &&
|
|
64
|
+
call.arguments.some((arg) => arg === node) &&
|
|
65
|
+
ts.isPropertyAccessExpression(call.expression) &&
|
|
66
|
+
WritePath.#perElement.has(call.expression.name.text)
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
#children(node: ts.Node) {
|
|
71
|
+
const children: ts.Node[] = [];
|
|
72
|
+
ts.forEachChild(node, (child) => {
|
|
73
|
+
children.push(child);
|
|
74
|
+
});
|
|
75
|
+
return children.flatMap((child) => this.along(child));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
#repeat(calls: ts.CallExpression[]) {
|
|
79
|
+
for (const call of calls) this.#repeated.add(call);
|
|
80
|
+
return calls;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#flowOf(node: ts.Statement): Flow {
|
|
84
|
+
if (ts.isBlock(node)) return this.#sequenceOf(node.statements);
|
|
85
|
+
if (ts.isReturnStatement(node) || ts.isThrowStatement(node))
|
|
86
|
+
return { through: null, exit: node.expression ? this.along(node.expression) : [], jump: null };
|
|
87
|
+
if (ts.isBreakStatement(node) || ts.isContinueStatement(node)) return { through: null, exit: null, jump: [] };
|
|
88
|
+
if (ts.isLabeledStatement(node)) return this.#flowOf(node.statement);
|
|
89
|
+
if (ts.isIfStatement(node))
|
|
90
|
+
return WritePath.#prefixed(
|
|
91
|
+
this.along(node.expression),
|
|
92
|
+
this.#merged(
|
|
93
|
+
this.#flowOf(node.thenStatement),
|
|
94
|
+
node.elseStatement ? this.#flowOf(node.elseStatement) : WritePath.#onward([]),
|
|
95
|
+
),
|
|
96
|
+
);
|
|
97
|
+
if (ts.isSwitchStatement(node)) return WritePath.#prefixed(this.along(node.expression), this.#switchOf(node));
|
|
98
|
+
if (ts.isTryStatement(node)) return this.#tryOf(node);
|
|
99
|
+
if (ts.isForOfStatement(node) || ts.isForInStatement(node))
|
|
100
|
+
return this.#loopOf(this.along(node.expression), [], node.statement, true);
|
|
101
|
+
if (ts.isForStatement(node))
|
|
102
|
+
return this.#loopOf(
|
|
103
|
+
node.initializer ? this.along(node.initializer) : [],
|
|
104
|
+
[node.condition, node.incrementor].flatMap((part) => (part ? this.along(part) : [])),
|
|
105
|
+
node.statement,
|
|
106
|
+
WritePath.#walksItems(node.condition),
|
|
107
|
+
);
|
|
108
|
+
if (ts.isWhileStatement(node) || ts.isDoStatement(node))
|
|
109
|
+
return this.#loopOf([], this.along(node.expression), node.statement, WritePath.#walksItems(node.expression));
|
|
110
|
+
return WritePath.#onward(this.along(node));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
#sequenceOf(statements: readonly ts.Statement[]): Flow {
|
|
114
|
+
let through: ts.CallExpression[] | null = [];
|
|
115
|
+
let exit: ts.CallExpression[] | null = null;
|
|
116
|
+
let jump: ts.CallExpression[] | null = null;
|
|
117
|
+
for (const statement of statements) {
|
|
118
|
+
if (through === null) break;
|
|
119
|
+
const flow = this.#flowOf(statement);
|
|
120
|
+
if (flow.exit) exit = this.#busierOf(exit, [...through, ...flow.exit]);
|
|
121
|
+
if (flow.jump) jump = this.#busierOf(jump, [...through, ...flow.jump]);
|
|
122
|
+
through = flow.through && [...through, ...flow.through];
|
|
123
|
+
}
|
|
124
|
+
return { through, exit, jump };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
//? A clause without `break` runs on into the next one; `break` leaves the switch and goes on after it.
|
|
128
|
+
#switchOf(node: ts.SwitchStatement): Flow {
|
|
129
|
+
const hasDefault = node.caseBlock.clauses.some(ts.isDefaultClause);
|
|
130
|
+
let flow: Flow = { through: hasDefault ? null : [], exit: null, jump: null };
|
|
131
|
+
let fallen: ts.CallExpression[] | null = null;
|
|
132
|
+
for (const clause of node.caseBlock.clauses) {
|
|
133
|
+
const entry = fallen && this.countOf(fallen) > 0 ? fallen : [];
|
|
134
|
+
const own = WritePath.#prefixed(entry, this.#sequenceOf(clause.statements));
|
|
135
|
+
flow = this.#merged(flow, { through: own.jump, exit: own.exit, jump: null });
|
|
136
|
+
fallen = own.through;
|
|
137
|
+
}
|
|
138
|
+
return this.#merged(flow, { through: fallen, exit: null, jump: null });
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
//? A catch runs after whatever the try sent before it threw, so both count; a finally follows every path.
|
|
142
|
+
#tryOf(node: ts.TryStatement): Flow {
|
|
143
|
+
const tried = this.#flowOf(node.tryBlock);
|
|
144
|
+
const sent = this.#busiestOf(tried);
|
|
145
|
+
const caught = node.catchClause
|
|
146
|
+
? WritePath.#prefixed(sent, this.#flowOf(node.catchClause.block))
|
|
147
|
+
: { through: null, exit: sent, jump: null };
|
|
148
|
+
const flow = this.#merged(tried, caught);
|
|
149
|
+
if (!node.finallyBlock) return flow;
|
|
150
|
+
const after = this.#busiestOf(this.#flowOf(node.finallyBlock));
|
|
151
|
+
return {
|
|
152
|
+
through: flow.through && [...flow.through, ...after],
|
|
153
|
+
exit: flow.exit && [...flow.exit, ...after],
|
|
154
|
+
jump: flow.jump && [...flow.jump, ...after],
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
//? An iteration that leaves the function sends its writes once; one that goes on repeats them for a loop over items.
|
|
159
|
+
#loopOf(once: ts.CallExpression[], everyTurn: ts.CallExpression[], body: ts.Statement, isPerItem: boolean): Flow {
|
|
160
|
+
const flow = this.#flowOf(body);
|
|
161
|
+
const turn = [...everyTurn, ...(this.#busierOf(flow.through, flow.jump) ?? [])];
|
|
162
|
+
const repeated = isPerItem ? this.#repeat(turn) : turn;
|
|
163
|
+
return { through: [...once, ...repeated], exit: flow.exit && [...once, ...flow.exit], jump: null };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
#merged(a: Flow, b: Flow): Flow {
|
|
167
|
+
return {
|
|
168
|
+
through: this.#busierOf(a.through, b.through),
|
|
169
|
+
exit: this.#busierOf(a.exit, b.exit),
|
|
170
|
+
jump: this.#busierOf(a.jump, b.jump),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
#busiestOf({ through, exit, jump }: Flow) {
|
|
175
|
+
return this.#busierOf(this.#busierOf(through, exit), jump) ?? [];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
#busierOf(a: ts.CallExpression[] | null, b: ts.CallExpression[] | null) {
|
|
179
|
+
return a && b ? this.#busier(a, b) : (a ?? b);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
static #onward(calls: ts.CallExpression[]): Flow {
|
|
183
|
+
return { through: calls, exit: null, jump: null };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static #prefixed(calls: ts.CallExpression[], { through, exit, jump }: Flow): Flow {
|
|
187
|
+
return {
|
|
188
|
+
through: through && [...calls, ...through],
|
|
189
|
+
exit: exit && [...calls, ...exit],
|
|
190
|
+
jump: jump && [...calls, ...jump],
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static #isFlowStatement(node: ts.Node): node is ts.Statement {
|
|
195
|
+
return (
|
|
196
|
+
ts.isBlock(node) ||
|
|
197
|
+
ts.isIfStatement(node) ||
|
|
198
|
+
ts.isSwitchStatement(node) ||
|
|
199
|
+
ts.isTryStatement(node) ||
|
|
200
|
+
ts.isLabeledStatement(node) ||
|
|
201
|
+
ts.isReturnStatement(node) ||
|
|
202
|
+
ts.isThrowStatement(node) ||
|
|
203
|
+
ts.isBreakStatement(node) ||
|
|
204
|
+
ts.isContinueStatement(node) ||
|
|
205
|
+
ts.isIterationStatement(node, false)
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
//? A loop bound by `xs.length` or asking a call for more (`while (await next())`) walks items; a bound it counts
|
|
210
|
+
//? itself (`i < 3`, `!ok`) is a retry or a poll, which sends one write however many times it tries.
|
|
211
|
+
static #walksItems(node: ts.Node | undefined): boolean {
|
|
212
|
+
if (!node) return false;
|
|
213
|
+
if (ts.isCallExpression(node)) return true;
|
|
214
|
+
if (ts.isPropertyAccessExpression(node) && ["length", "size"].includes(node.name.text)) return true;
|
|
215
|
+
return !!ts.forEachChild(node, (child) => WritePath.#walksItems(child) || undefined);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
#busier = (a: ts.CallExpression[], b: ts.CallExpression[]) => (this.countOf(b) > this.countOf(a) ? b : a);
|
|
219
|
+
}
|