@akanjs/devkit 3.1.1-rc.15 → 3.1.1-rc.17
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/agentAssets.test.ts +65 -0
- package/agentAssets.ts +166 -0
- package/agentsIndex.ts +4 -4
- package/akanContext.ts +44 -2
- package/applicationBuildRunner.test.ts +17 -0
- package/applicationBuildRunner.ts +3 -1
- package/biome.base.json +4 -1
- package/codeAgent/agent/akanSystemPrompt.ts +1 -1
- package/codeAgent/feedback/VerifyFeedback.ts +3 -4
- package/codeAgent/skills/akan-agent/SKILL.md +36 -0
- package/codeAgent/skills/akan-debug/SKILL.md +3 -3
- 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/AkanEditScope.ts +8 -3
- package/codeAgent/tools/AkanToolPack.ts +4 -10
- package/codeAgent/tools/AkanVerifier.ts +13 -31
- package/codeAgent/tools/akanCodePlugins.test.ts +12 -30
- 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/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/index.ts +1 -0
- 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/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/package.json +2 -2
- package/prompter.ts +7 -0
- package/qualityScanner.test.ts +75 -0
- package/qualityScanner.ts +17 -1
- package/serviceLoopCallScanner.test.ts +56 -0
- package/serviceLoopCallScanner.ts +122 -0
- 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/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
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { SourceFileInfo } from "./qualityScanner";
|
|
4
|
+
import { SignalMutationIndex } from "./signalMutationIndex";
|
|
5
|
+
import { WritePlacementScanner } from "./writePlacementScanner";
|
|
6
|
+
import { WritingActionIndex } from "./writingActionIndex";
|
|
7
|
+
|
|
8
|
+
const fileOf = (file: string, content: string): SourceFileInfo => ({
|
|
9
|
+
file,
|
|
10
|
+
absolutePath: `/repo/${file}`,
|
|
11
|
+
content,
|
|
12
|
+
sourceFile: ts.createSourceFile(
|
|
13
|
+
file,
|
|
14
|
+
content,
|
|
15
|
+
ts.ScriptTarget.Latest,
|
|
16
|
+
true,
|
|
17
|
+
file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
|
|
18
|
+
),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const index = new WritingActionIndex(
|
|
22
|
+
new SignalMutationIndex([
|
|
23
|
+
fileOf(
|
|
24
|
+
"apps/demo/lib/court/court.signal.ts",
|
|
25
|
+
`export class CourtEndpoint extends endpoint(srv.court, ({ query, mutation }) => ({
|
|
26
|
+
skipNvrOnCourt: mutation(cnst.Court, { guards: [User] }).param("courtId", ID).exec(() => null),
|
|
27
|
+
courtCount: query(Int, { guards: [User] }).exec(() => 0),
|
|
28
|
+
})) {}
|
|
29
|
+
`,
|
|
30
|
+
),
|
|
31
|
+
]),
|
|
32
|
+
[
|
|
33
|
+
fileOf(
|
|
34
|
+
"apps/demo/lib/court/court.store.ts",
|
|
35
|
+
`import { fetch, sig } from "../useClient";
|
|
36
|
+
export class CourtStore extends store(sig.court, () => ({})) {
|
|
37
|
+
async skipNvrOnCourt(courtId: string) {
|
|
38
|
+
this.setCourt(await fetch.skipNvrOnCourt(courtId));
|
|
39
|
+
}
|
|
40
|
+
setPicked(courtId: string) {
|
|
41
|
+
this.set({ picked: courtId });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
`,
|
|
45
|
+
),
|
|
46
|
+
],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const scan = (file: string, body: string) => {
|
|
50
|
+
const content = `"use client";\nimport { fetch as api, st } from "@apps/demo/client";\n\n${body}\n`;
|
|
51
|
+
return new WritePlacementScanner(index).scan(
|
|
52
|
+
file,
|
|
53
|
+
`apps/demo/${file}`,
|
|
54
|
+
fileOf(`apps/demo/${file}`, content).sourceFile,
|
|
55
|
+
);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
describe("WritePlacementScanner", () => {
|
|
59
|
+
test("names every server write a ui/ or webkit/ file sends, by call or by reference, under any import name", () => {
|
|
60
|
+
expect(
|
|
61
|
+
scan(
|
|
62
|
+
"webkit/useSkip.tsx",
|
|
63
|
+
"export const useSkip = (courtId: string) => ({\n skip: () => st.do.skipNvrOnCourt(courtId),\n bound: st.do.skipNvrOnCourt,\n count: () => api.courtCount(),\n direct: () => api.skipNvrOnCourt(courtId),\n});",
|
|
64
|
+
),
|
|
65
|
+
).toEqual({ line: 5, writes: ["st.do.skipNvrOnCourt", "st.do.skipNvrOnCourt", "api.skipNvrOnCourt"] });
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("lets a module component call a writing action, and a ui/ file set client state, but not a fetch mutation", () => {
|
|
69
|
+
expect(
|
|
70
|
+
scan("lib/court/Court.Zone.tsx", "export const Skip = ({ id }: { id: string }) => st.do.skipNvrOnCourt(id);"),
|
|
71
|
+
).toBeNull();
|
|
72
|
+
expect(scan("ui/Pick.tsx", "export const Pick = ({ id }: { id: string }) => st.do.setPicked(id);")).toBeNull();
|
|
73
|
+
expect(
|
|
74
|
+
scan("lib/court/Court.Zone.tsx", "export const Skip = ({ id }: { id: string }) => api.skipNvrOnCourt(id);"),
|
|
75
|
+
).toEqual({ line: 4, writes: ["api.skipNvrOnCourt"] });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
import { StoreAccessScanner } from "./storeAccessScanner";
|
|
3
|
+
import type { WritingActionIndex } from "./writingActionIndex";
|
|
4
|
+
|
|
5
|
+
export interface WritePlacement {
|
|
6
|
+
line: number;
|
|
7
|
+
writes: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
//* A write leaves the browser through one store action, started by one module component: an agent, a test or another
|
|
11
|
+
//* screen reaches it through the store, and `ui/` / `webkit/` stay free of any one model.
|
|
12
|
+
export class WritePlacementScanner {
|
|
13
|
+
readonly #index: WritingActionIndex;
|
|
14
|
+
|
|
15
|
+
constructor(index: WritingActionIndex) {
|
|
16
|
+
this.#index = index;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
scan(file: string, workspaceFile: string, sourceFile: ts.SourceFile): WritePlacement | null {
|
|
20
|
+
const fetchNames = StoreAccessScanner.localNamesOf(sourceFile, "fetch");
|
|
21
|
+
const stNames = file.startsWith("lib/") ? new Set<string>() : StoreAccessScanner.stNamesOf(sourceFile);
|
|
22
|
+
if (!fetchNames.size && !stNames.size) return null;
|
|
23
|
+
const sites: ts.PropertyAccessExpression[] = [];
|
|
24
|
+
const visit = (node: ts.Node) => {
|
|
25
|
+
if (ts.isPropertyAccessExpression(node) && this.#isWrite(node, workspaceFile, fetchNames, stNames))
|
|
26
|
+
sites.push(node);
|
|
27
|
+
ts.forEachChild(node, visit);
|
|
28
|
+
};
|
|
29
|
+
visit(sourceFile);
|
|
30
|
+
const first = sites[0];
|
|
31
|
+
if (!first) return null;
|
|
32
|
+
return {
|
|
33
|
+
line: sourceFile.getLineAndCharacterOfPosition(first.getStart(sourceFile)).line + 1,
|
|
34
|
+
writes: sites.map((site) => site.getText(sourceFile)),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
#isWrite(access: ts.PropertyAccessExpression, workspaceFile: string, fetchNames: Set<string>, stNames: Set<string>) {
|
|
39
|
+
const owner = access.expression;
|
|
40
|
+
if (ts.isIdentifier(owner) && fetchNames.has(owner.text))
|
|
41
|
+
return this.#index.isMutation(workspaceFile, access.name.text);
|
|
42
|
+
return (
|
|
43
|
+
ts.isPropertyAccessExpression(owner) &&
|
|
44
|
+
owner.name.text === "do" &&
|
|
45
|
+
ts.isIdentifier(owner.expression) &&
|
|
46
|
+
stNames.has(owner.expression.text) &&
|
|
47
|
+
this.#index.isWritingAction(access.name.text)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import ts from "typescript";
|
|
3
|
+
import type { SourceFileInfo } from "./qualityScanner";
|
|
4
|
+
import { SignalMutationIndex } from "./signalMutationIndex";
|
|
5
|
+
import { WritingActionIndex } from "./writingActionIndex";
|
|
6
|
+
|
|
7
|
+
const fileOf = (file: string, content: string): SourceFileInfo => ({
|
|
8
|
+
file,
|
|
9
|
+
absolutePath: `/repo/${file}`,
|
|
10
|
+
content,
|
|
11
|
+
sourceFile: ts.createSourceFile(file, content, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const signals = [
|
|
15
|
+
fileOf(
|
|
16
|
+
"apps/demo/lib/court/court.signal.ts",
|
|
17
|
+
`export class CourtSlice extends slice(srv.court, { guards: { root: Admin, cru: User } }, () => ({})) {}
|
|
18
|
+
export class CourtEndpoint extends endpoint(srv.court, ({ query, mutation }) => ({
|
|
19
|
+
courtCount: query(Int, { guards: [User] }).exec(() => 0),
|
|
20
|
+
})) {}
|
|
21
|
+
`,
|
|
22
|
+
),
|
|
23
|
+
fileOf(
|
|
24
|
+
"libs/shared/lib/push/push.signal.ts",
|
|
25
|
+
`export class PushEndpoint extends endpoint(srv.push, ({ mutation }) => ({
|
|
26
|
+
pushPcCommand: mutation(Boolean, { guards: [User] }).param("courtId", ID).exec(() => true),
|
|
27
|
+
})) {}
|
|
28
|
+
`,
|
|
29
|
+
),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const courtStore = fileOf(
|
|
33
|
+
"apps/demo/lib/court/court.store.ts",
|
|
34
|
+
`import { fetch, sig } from "../useClient";
|
|
35
|
+
export class CourtStore extends store(sig.court, () => ({})) {
|
|
36
|
+
async countCourts() {
|
|
37
|
+
this.set({ count: await fetch.courtCount() });
|
|
38
|
+
}
|
|
39
|
+
async openCourt(courtId: string) {
|
|
40
|
+
const root = this as unknown as RootStore;
|
|
41
|
+
await root.restartPc(courtId);
|
|
42
|
+
}
|
|
43
|
+
async refreshAfterOpen(courtId: string) {
|
|
44
|
+
await this.openCourt(courtId);
|
|
45
|
+
await this.countCourts();
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
`,
|
|
49
|
+
);
|
|
50
|
+
const pcStore = fileOf(
|
|
51
|
+
"apps/demo/lib/_pc/pc.store.ts",
|
|
52
|
+
`import { fetch, sig } from "../useClient";
|
|
53
|
+
export class PcStore extends store(sig.pc, () => ({})) {
|
|
54
|
+
async restartPc(courtId: string) {
|
|
55
|
+
await PcStore.sendRestart(courtId);
|
|
56
|
+
}
|
|
57
|
+
static async sendRestart(courtId: string) {
|
|
58
|
+
await fetch.pushPcCommand(courtId);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
`,
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const indexOf = (storeFiles: SourceFileInfo[]) => new WritingActionIndex(new SignalMutationIndex(signals), storeFiles);
|
|
65
|
+
|
|
66
|
+
describe("WritingActionIndex", () => {
|
|
67
|
+
test("holds the generated actions that send a mutation for a model with a slice, not its form setters", () => {
|
|
68
|
+
const index = indexOf([courtStore, pcStore]);
|
|
69
|
+
for (const name of [
|
|
70
|
+
"createCourt",
|
|
71
|
+
"updateCourt",
|
|
72
|
+
"removeCourt",
|
|
73
|
+
"mergeCourt",
|
|
74
|
+
"createCourtInForm",
|
|
75
|
+
"updateCourtInForm",
|
|
76
|
+
"submitCourt",
|
|
77
|
+
])
|
|
78
|
+
expect(index.isWritingAction(name)).toBe(true);
|
|
79
|
+
for (const name of ["setNameOnCourt", "writeOnCourt", "uploadImageOnCourt", "newCourt", "editCourt", "viewCourt"])
|
|
80
|
+
expect(index.isWritingAction(name)).toBe(false);
|
|
81
|
+
expect(index.isWritingAction("createPc")).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("follows writes through a static helper, the RootStore alias and a sibling action to a fixed point", () => {
|
|
85
|
+
const index = indexOf([courtStore, pcStore]);
|
|
86
|
+
|
|
87
|
+
expect(index.isWritingAction("restartPc")).toBe(true);
|
|
88
|
+
expect(index.isWritingAction("openCourt")).toBe(true);
|
|
89
|
+
expect(index.isWritingAction("refreshAfterOpen")).toBe(true);
|
|
90
|
+
expect(index.isWritingAction("countCourts")).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("knows no action of a store it was not given", () => {
|
|
94
|
+
const index = indexOf([courtStore]);
|
|
95
|
+
|
|
96
|
+
expect(index.isWritingAction("restartPc")).toBe(false);
|
|
97
|
+
expect(index.isWritingAction("openCourt")).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type ts from "typescript";
|
|
2
|
+
import type { SourceFileInfo } from "./qualityScanner";
|
|
3
|
+
import type { SignalMutationIndex } from "./signalMutationIndex";
|
|
4
|
+
import { type StoreCallTarget, type StoreMethod, StoreSource } from "./storeSource";
|
|
5
|
+
|
|
6
|
+
//? Stores merge into one root, so an action name means one action wherever it is called from.
|
|
7
|
+
export class WritingActionIndex {
|
|
8
|
+
readonly #mutations: SignalMutationIndex;
|
|
9
|
+
readonly #actions = new Set<string>();
|
|
10
|
+
readonly #statics = new Set<string>();
|
|
11
|
+
|
|
12
|
+
constructor(mutations: SignalMutationIndex, storeFiles: Pick<SourceFileInfo, "file" | "sourceFile">[]) {
|
|
13
|
+
this.#mutations = mutations;
|
|
14
|
+
const sources = storeFiles.map((storeFile) => new StoreSource(storeFile));
|
|
15
|
+
for (const source of sources)
|
|
16
|
+
for (const { refName } of source.stores) {
|
|
17
|
+
const generated = refName ? StoreSource.generatedWritesOf(refName) : [];
|
|
18
|
+
if (generated[0] && mutations.isMutation(source.file, generated[0]))
|
|
19
|
+
for (const name of generated) this.#actions.add(name);
|
|
20
|
+
}
|
|
21
|
+
const methods = sources.flatMap((source) =>
|
|
22
|
+
source.methods.map((method) => ({ file: source.file, method, targets: source.targetsIn(method) })),
|
|
23
|
+
);
|
|
24
|
+
let grew = true;
|
|
25
|
+
while (grew) {
|
|
26
|
+
grew = false;
|
|
27
|
+
for (const { file, method, targets } of methods) {
|
|
28
|
+
if (this.#holds(method) || !targets.some((target) => this.#writes(file, target))) continue;
|
|
29
|
+
if (method.isStatic) this.#statics.add(`${method.store}.${method.name}`);
|
|
30
|
+
else this.#actions.add(method.name);
|
|
31
|
+
grew = true;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
isWritingAction(name: string) {
|
|
37
|
+
return this.#actions.has(name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
isMutation(consumerFile: string, name: string) {
|
|
41
|
+
return this.#mutations.isMutation(consumerFile, name);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
writeOf(source: StoreSource, method: StoreMethod, call: ts.CallExpression): StoreCallTarget | null {
|
|
45
|
+
const target = source.targetOf(method, call);
|
|
46
|
+
if (!target || !this.#writes(source.file, target)) return null;
|
|
47
|
+
const isSelf =
|
|
48
|
+
target.kind !== "fetch" &&
|
|
49
|
+
target.name === method.name &&
|
|
50
|
+
(target.kind === "static" ? method.isStatic && target.store === method.store : !method.isStatic);
|
|
51
|
+
return isSelf ? null : target;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
#holds(method: StoreMethod) {
|
|
55
|
+
return method.isStatic ? this.#statics.has(`${method.store}.${method.name}`) : this.#actions.has(method.name);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
#writes(file: string, target: StoreCallTarget) {
|
|
59
|
+
if (target.kind === "fetch") return this.#mutations.isMutation(file, target.name);
|
|
60
|
+
if (target.kind === "action") return this.#actions.has(target.name);
|
|
61
|
+
return this.#statics.has(`${target.store}.${target.name}`);
|
|
62
|
+
}
|
|
63
|
+
}
|