@akanjs/devkit 3.1.2-rc.3 → 3.2.0-rc.0

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.
Files changed (93) hide show
  1. package/CHANGELOG.md +348 -0
  2. package/abstractDoc.ts +1 -1
  3. package/agentAssets.ts +2 -2
  4. package/akanConfig/akanConfig.ts +2 -2
  5. package/akanMcpContract.ts +2 -2
  6. package/apiReference.test.ts +37 -0
  7. package/apiReference.ts +240 -0
  8. package/applicationBuildReporter.test.ts +54 -0
  9. package/applicationBuildReporter.ts +12 -0
  10. package/applicationBuildRunner.test.ts +34 -14
  11. package/applicationBuildRunner.ts +77 -14
  12. package/artifact/implicitRootLayout.test.ts +42 -0
  13. package/artifact/implicitRootLayout.ts +5 -6
  14. package/artifact/storeBootstrap.ts +24 -0
  15. package/cloud/cloudApi.test.ts +70 -7
  16. package/cloud/cloudApi.ts +10 -4
  17. package/cloud/constants.ts +3 -0
  18. package/cloud/globalConfig.test.ts +36 -0
  19. package/cloud/globalConfig.ts +10 -1
  20. package/codeAgent/skills/akan-debug/SKILL.md +2 -1
  21. package/codeAgent/skills/akan-page/SKILL.md +30 -2
  22. package/codeAgent/skills/akan-service/SKILL.md +77 -1
  23. package/commandDecorators/command.ts +55 -9
  24. package/commandDecorators/commandDecorators.test.ts +131 -2
  25. package/declarationReader.ts +211 -0
  26. package/executors.test.ts +65 -8
  27. package/executors.ts +5 -3
  28. package/frameworkGraph.test.ts +121 -0
  29. package/frameworkGraph.ts +165 -0
  30. package/frontendBuild/allRoutesBuilder.test.ts +84 -0
  31. package/frontendBuild/allRoutesBuilder.ts +32 -12
  32. package/frontendBuild/buildRouteClient.test.ts +192 -2
  33. package/frontendBuild/bundleDefine.ts +4 -0
  34. package/frontendBuild/clientBuildTypes.ts +7 -1
  35. package/frontendBuild/clientEntriesBundler.test.ts +82 -0
  36. package/frontendBuild/clientEntriesBundler.ts +55 -4
  37. package/frontendBuild/csrArtifactBuilder.test.ts +23 -0
  38. package/frontendBuild/csrArtifactBuilder.ts +2 -1
  39. package/frontendBuild/fontOptimizer.test.ts +54 -0
  40. package/frontendBuild/fontOptimizer.ts +5 -4
  41. package/frontendBuild/index.ts +3 -0
  42. package/frontendBuild/routeClientBuilder.ts +6 -2
  43. package/frontendBuild/routeModulePreloads.test.ts +59 -0
  44. package/frontendBuild/routeModulePreloads.ts +52 -0
  45. package/frontendBuild/ssrBaseArtifactBuilder.ts +35 -10
  46. package/frontendBuild/ssrDevBundler.test.ts +26 -0
  47. package/frontendBuild/ssrDevBundler.ts +2 -1
  48. package/frontendBuild/vendorImportScanner.test.ts +71 -0
  49. package/frontendBuild/vendorImportScanner.ts +80 -0
  50. package/frontendBuild/vendorNarrowing.test.ts +103 -0
  51. package/frontendBuild/vendorNarrowing.ts +64 -0
  52. package/frontendBuild/vendorSpecifiers.ts +13 -0
  53. package/gitRevision.test.ts +82 -0
  54. package/gitRevision.ts +128 -0
  55. package/incrementalBuilder/builderGraphPayloads.ts +60 -0
  56. package/incrementalBuilder/incrementalBuilder.proc.test.ts +105 -6
  57. package/incrementalBuilder/incrementalBuilder.proc.ts +6 -10
  58. package/integration/devStability.integration.test.ts +1 -1
  59. package/integration/devStabilityHarness.test.ts +1 -1
  60. package/lint/__fixtures__/no-daisyui-legacy-class/bad.tsx +4 -2
  61. package/lint/__fixtures__/no-js-private-class-method/bad.tsx +2 -0
  62. package/lint/__fixtures__/no-js-private-class-method/good.tsx +3 -2
  63. package/lint/no-daisyui-legacy-class.grit +9 -7
  64. package/lint/no-deep-internal-import.grit +1 -1
  65. package/lint/no-js-private-class-method.grit +9 -28
  66. package/maintainabilityBaseline.ts +7 -2
  67. package/maintainabilityContract.test.ts +183 -0
  68. package/maintainabilityContract.ts +145 -30
  69. package/maintainabilityExceptionScanner.test.ts +13 -2
  70. package/maintainabilityExceptionScanner.ts +16 -6
  71. package/maintainabilityLimits.ts +12 -0
  72. package/maintainabilityReport.test.ts +35 -0
  73. package/maintainabilityReport.ts +29 -8
  74. package/mobile/nativePublicEnv.test.ts +5 -1
  75. package/package.json +3 -3
  76. package/routeJsBudget.test.ts +177 -0
  77. package/routeJsBudget.ts +222 -0
  78. package/structureScanner.test.ts +167 -0
  79. package/structureScanner.ts +164 -0
  80. package/transforms/barrelAnalyzer.ts +10 -3
  81. package/transforms/barrelImportsPlugin.ts +13 -7
  82. package/transforms/serverEnvPlugin.test.ts +1 -1
  83. package/transforms/serverEnvPlugin.ts +1 -1
  84. package/transforms/transforms.test.ts +64 -12
  85. package/transforms/tsconfigPackageResolver.test.ts +12 -0
  86. package/typeChecker.test.ts +100 -0
  87. package/typeChecker.ts +14 -2
  88. package/workflow/plan.ts +79 -17
  89. package/workflow/types.ts +1 -0
  90. package/workspaceGraph.test.ts +121 -0
  91. package/workspaceGraph.ts +149 -16
  92. package/workspaceLayout.ts +1 -0
  93. /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
package/workflow/types.ts CHANGED
@@ -74,6 +74,7 @@ export interface WorkflowInputSpec {
74
74
  required?: boolean;
75
75
  description: string;
76
76
  allowedValues?: readonly string[];
77
+ mustExist?: boolean;
77
78
  }
78
79
 
79
80
  export interface WorkflowStep {
@@ -0,0 +1,121 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import type { AkanChangeMap, AkanGraphEdge, AkanGraphNode } from "@akanjs/language";
3
+ import { WorkspaceGraph } from "./workspaceGraph";
4
+
5
+ const node = (id: string, owner: string | null, change?: AkanGraphNode["change"]): AkanGraphNode => ({
6
+ id,
7
+ kind: id.split(":")[0] as AkanGraphNode["kind"],
8
+ name: id,
9
+ label: id,
10
+ owner,
11
+ module: null,
12
+ detail: [],
13
+ at: null,
14
+ ...(change ? { change } : {}),
15
+ });
16
+ const edge = (from: string, to: string, change?: AkanGraphEdge["change"]): AkanGraphEdge => ({
17
+ from,
18
+ to,
19
+ kind: "calls",
20
+ label: null,
21
+ count: 1,
22
+ at: null,
23
+ ...(change ? { change } : {}),
24
+ });
25
+
26
+ describe("WorkspaceGraph.changesVisibleTo", () => {
27
+ it("keeps the app's and the libs' changes, and an unchanged neighbour only while an edge still reaches it", () => {
28
+ const changes: AkanChangeMap = {
29
+ version: 1,
30
+ nodes: [
31
+ node("endpoint:apps/demo:memo.pin", "apps/demo", "added"),
32
+ node("method:libs/shared:file.touch", "libs/shared"),
33
+ node("method:libs/shared:file.open", "libs/shared"),
34
+ node("component:apps/admin/page/a.tsx#default", "apps/admin", "changed"),
35
+ node("guard:Owner", null, "added"),
36
+ ],
37
+ edges: [
38
+ edge("endpoint:apps/demo:memo.pin", "method:libs/shared:file.touch", "added"),
39
+ edge("component:apps/admin/page/a.tsx#default", "method:libs/shared:file.open"),
40
+ ],
41
+ since: "0123456789ab",
42
+ summary: { added: 2, removed: 0, changed: 1 },
43
+ unmapped: [
44
+ { file: "apps/admin/page/b.tsx", change: "added" },
45
+ { file: "apps/demo/lib/memo/memo.dictionary.ts", change: "changed" },
46
+ { file: "libs/shared/lib/file/file.md", change: "removed" },
47
+ ],
48
+ };
49
+ const visible = WorkspaceGraph.changesVisibleTo(changes, "demo");
50
+ expect(visible.nodes.map(({ id }) => id)).toEqual([
51
+ "endpoint:apps/demo:memo.pin",
52
+ "method:libs/shared:file.touch",
53
+ "guard:Owner",
54
+ ]);
55
+ expect(visible.edges).toHaveLength(1);
56
+ expect(visible.summary).toEqual({ added: 2, removed: 0, changed: 0 });
57
+ expect(visible.unmapped.map(({ file }) => file)).toEqual([
58
+ "apps/demo/lib/memo/memo.dictionary.ts",
59
+ "libs/shared/lib/file/file.md",
60
+ ]);
61
+ expect(visible.since).toBe("0123456789ab");
62
+ });
63
+ });
64
+
65
+ describe("WorkspaceGraph.answer", () => {
66
+ it("answers a small graph whole, and drops untouched pages and their route-only guards from one past the limit", () => {
67
+ const fillers = Array.from({ length: 597 }, (_, idx) => node(`method:apps/demo:memo.m${idx}`, "apps/demo"));
68
+ const guard = { ...node("guard:libs/shared:signedIn", "libs/shared"), detail: ["route"] };
69
+ const graph = {
70
+ version: 1 as const,
71
+ nodes: [
72
+ node("layout:apps/demo:/", "apps/demo"),
73
+ node("route:apps/demo:/memo", "apps/demo"),
74
+ node("route:apps/demo:/peek", "apps/demo"),
75
+ node("endpoint:apps/demo:memo.pin", "apps/demo", "changed"),
76
+ guard,
77
+ ...fillers,
78
+ ],
79
+ edges: [
80
+ edge("layout:apps/demo:/", "route:apps/demo:/memo"),
81
+ edge("layout:apps/demo:/", "route:apps/demo:/peek"),
82
+ edge("layout:apps/demo:/", "guard:libs/shared:signedIn"),
83
+ edge("route:apps/demo:/peek", "endpoint:apps/demo:memo.pin"),
84
+ ],
85
+ };
86
+ const small = WorkspaceGraph.answer({ ...graph, nodes: graph.nodes.slice(0, 5) }, {});
87
+ if ("diagnostics" in small) throw new Error("expected an answer");
88
+ expect(small.nodes).toHaveLength(5);
89
+ const unscoped = WorkspaceGraph.answer(graph, {});
90
+ if ("diagnostics" in unscoped) throw new Error("expected an answer");
91
+ expect(unscoped.nodes.slice(0, 2).map(({ id }) => id)).toEqual([
92
+ "route:apps/demo:/peek",
93
+ "endpoint:apps/demo:memo.pin",
94
+ ]);
95
+ expect(unscoped.nodes).toHaveLength(599);
96
+ expect(unscoped.edges.map(({ from, to }) => `${from} -> ${to}`)).toEqual([
97
+ "route:apps/demo:/peek -> endpoint:apps/demo:memo.pin",
98
+ ]);
99
+ const scoped = WorkspaceGraph.answer(graph, { app: "demo" });
100
+ if ("diagnostics" in scoped) throw new Error("expected an answer");
101
+ expect(scoped.nodes).toHaveLength(602);
102
+ });
103
+ });
104
+
105
+ describe("WorkspaceGraph.scope", () => {
106
+ it("keeps a lib's guard without pulling in every app endpoint that uses it", () => {
107
+ const guard = "guard:libs/shared:Admin";
108
+ const own = "endpoint:libs/shared:file.touch";
109
+ const other = "endpoint:apps/demo:memo.pin";
110
+ const scoped = WorkspaceGraph.scope(
111
+ {
112
+ version: 1,
113
+ nodes: [node(guard, "libs/shared"), node(own, "libs/shared"), node(other, "apps/demo")],
114
+ edges: [edge(own, guard), edge(other, guard)],
115
+ },
116
+ { app: "shared" },
117
+ );
118
+ expect(scoped.nodes.map(({ id }) => id)).toEqual([guard, own]);
119
+ expect(scoped.edges).toEqual([edge(own, guard)]);
120
+ });
121
+ });
package/workspaceGraph.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { readFile } from "node:fs/promises";
2
2
  import path from "node:path";
3
- import { type AkanGraph, type AkanGraphNode, AkanIndex } from "@akanjs/language";
3
+ import { type AkanChangeMap, type AkanGraph, AkanGraphDiff, type AkanGraphNode, AkanIndex } from "@akanjs/language";
4
4
  import ts from "typescript";
5
5
  import type { Workspace } from "./commandDecorators";
6
+ import { GitRevision } from "./gitRevision";
6
7
 
7
8
  export interface GraphScope {
8
9
  app?: string | null;
@@ -11,26 +12,88 @@ export interface GraphScope {
11
12
 
12
13
  //? `env/` holds each environment's secrets in plain TypeScript; the graph never needs to read it.
13
14
  const indexedPath = /^(apps|libs)\/(?!(?:.*\/)?(?:node_modules|dist|\.akan|env)\/).*\.tsx?$/;
15
+ const hiddenPath = /(?:^|\/)(?:node_modules|dist|\.akan|env)\//;
14
16
  const unscopedNodeLimit = 600;
17
+ const pageKinds = new Set<AkanGraphNode["kind"]>(["route", "layout"]);
18
+ const cachedBaseLimit = 4;
15
19
  const slashed = (fileName: string) => fileName.replace(/\\/g, "/");
16
20
 
17
21
  export class WorkspaceGraph {
18
- static async build(workspace: Workspace): Promise<AkanGraph> {
22
+ static readonly #bases = new Map<string, Promise<AkanGraph>>();
23
+
24
+ static async build(workspace: Workspace, { digests = false } = {}): Promise<AkanGraph> {
19
25
  const root = workspace.workspaceRoot;
20
- const index = new AkanIndex({ ts, root });
21
- const texts = new Map<string, string>();
22
26
  const files = (await workspace.getAllFiles("{apps,libs}/**/*.{ts,tsx}"))
23
27
  .map(slashed)
24
28
  .filter((file) => indexedPath.test(file));
25
- await Promise.all(
26
- files.map(async (file) => {
27
- const fileName = slashed(path.join(root, file));
28
- const text = await readFile(fileName, "utf8");
29
- texts.set(fileName, text);
30
- index.setFile(fileName, text);
31
- }),
29
+ return WorkspaceGraph.#graphOf(root, await WorkspaceGraph.#textsOf(root, files), digests);
30
+ }
31
+
32
+ static async #textsOf(root: string, files: string[]) {
33
+ const read = await Promise.all(
34
+ files.map(async (file) => [file, await readFile(path.join(root, file), "utf8").catch(() => null)] as const),
32
35
  );
33
- return index.graph((fileName) => texts.get(fileName));
36
+ return new Map(read.filter((entry): entry is readonly [string, string] => entry[1] !== null));
37
+ }
38
+
39
+ static async changes(workspace: Workspace, since: string | null): Promise<AkanChangeMap | { error: string }> {
40
+ const forkPoint = await new GitRevision(workspace.workspaceRoot).forkPointOf(since);
41
+ if (forkPoint.error !== undefined) return { error: forkPoint.error };
42
+ return await WorkspaceGraph.changesAt(workspace, forkPoint.sha);
43
+ }
44
+
45
+ static async changesAt(workspace: Workspace, sha: string): Promise<AkanChangeMap> {
46
+ const root = workspace.workspaceRoot;
47
+ const git = new GitRevision(root);
48
+ const [before, after, files] = await Promise.all([
49
+ WorkspaceGraph.#baseAt(root, sha),
50
+ git
51
+ .workingFiles()
52
+ .then((working) =>
53
+ WorkspaceGraph.#textsOf(
54
+ root,
55
+ working.filter((file) => indexedPath.test(file)),
56
+ ),
57
+ )
58
+ .then((texts) => WorkspaceGraph.#graphOf(root, texts, true)),
59
+ git.changedFilesSince(sha),
60
+ ]);
61
+ const changes = AkanGraphDiff.between(before, after);
62
+ const mapped = new Set([
63
+ ...changes.nodes
64
+ .filter((node) => node.change)
65
+ .flatMap((node) => [
66
+ node.at?.file,
67
+ ...(node.fields ?? []).filter((field) => field.change).map(({ at }) => at.file),
68
+ ]),
69
+ ...changes.edges.filter((edge) => edge.change).map((edge) => edge.at?.file),
70
+ ]);
71
+ const unmapped = files.filter(({ file }) => !mapped.has(file) && !hiddenPath.test(file));
72
+ return { ...changes, since: sha.slice(0, 12), unmapped };
73
+ }
74
+
75
+ static #baseAt(root: string, sha: string) {
76
+ const key = `${root}\n${sha}`;
77
+ const cached = WorkspaceGraph.#bases.get(key);
78
+ if (cached) return cached;
79
+ const graph = new GitRevision(root)
80
+ .filesAt(sha, (file) => indexedPath.test(file))
81
+ .then((texts) => WorkspaceGraph.#graphOf(root, texts, true));
82
+ graph.catch(() => WorkspaceGraph.#bases.delete(key));
83
+ WorkspaceGraph.#bases.set(key, graph);
84
+ for (const old of [...WorkspaceGraph.#bases.keys()].slice(0, -cachedBaseLimit)) WorkspaceGraph.#bases.delete(old);
85
+ return graph;
86
+ }
87
+
88
+ static #graphOf(root: string, texts: Map<string, string>, digests: boolean) {
89
+ const index = new AkanIndex({ ts, root });
90
+ const byFileName = new Map<string, string>();
91
+ for (const [file, text] of texts) {
92
+ const fileName = slashed(path.join(root, file));
93
+ byFileName.set(fileName, text);
94
+ index.setFile(fileName, text);
95
+ }
96
+ return index.graph((fileName) => byFileName.get(fileName), { digests });
34
97
  }
35
98
 
36
99
  static scope(graph: AkanGraph, { app, module }: GraphScope): AkanGraph {
@@ -41,11 +104,36 @@ export class WorkspaceGraph {
41
104
  .filter((node) => (!owners || owners.has(node.owner ?? "")) && (!module || node.module === module))
42
105
  .map((node) => node.id),
43
106
  );
44
- const edges = graph.edges.filter((edge) => seeds.has(edge.from) || seeds.has(edge.to));
107
+ //? A lib's guard is linked from every app's endpoints; through it, a scope would reach all of them.
108
+ const guards = new Set(graph.nodes.filter((node) => node.kind === "guard").map((node) => node.id));
109
+ const reaches = (id: string) => seeds.has(id) && !guards.has(id);
110
+ const edges = graph.edges.filter((edge) => reaches(edge.from) || reaches(edge.to));
45
111
  const ids = new Set([...seeds, ...edges.flatMap((edge) => [edge.from, edge.to])]);
46
112
  return { version: 1, nodes: graph.nodes.filter((node) => ids.has(node.id)), edges };
47
113
  }
48
114
 
115
+ //? Pages are many and most only wrap one another, so a graph too large to answer whole drops the pages that neither
116
+ //? changed nor use a change first, and the route-only guards they leave unlinked.
117
+ static #withoutPages(graph: AkanGraph): AkanGraph {
118
+ const changed = new Set(graph.nodes.filter((node) => node.change).map((node) => node.id));
119
+ const touching = new Set(
120
+ graph.edges
121
+ .filter((edge) => edge.kind !== "wraps" && (changed.has(edge.from) || changed.has(edge.to)))
122
+ .flatMap((edge) => [edge.from, edge.to]),
123
+ );
124
+ const dropped = new Set(
125
+ graph.nodes
126
+ .filter((node) => pageKinds.has(node.kind) && !node.change && !touching.has(node.id))
127
+ .map((node) => node.id),
128
+ );
129
+ const edges = graph.edges.filter((edge) => !dropped.has(edge.from) && !dropped.has(edge.to));
130
+ const linked = new Set(edges.flatMap((edge) => [edge.from, edge.to]));
131
+ const isRouteOnly = (node: AkanGraphNode) =>
132
+ node.kind === "guard" && node.detail.includes("route") && !node.change && !linked.has(node.id);
133
+ const nodes = graph.nodes.filter((node) => !dropped.has(node.id) && !isRouteOnly(node));
134
+ return { version: 1, nodes, edges };
135
+ }
136
+
49
137
  static visibleTo(graph: AkanGraph, app: string): AkanGraph {
50
138
  const appOwner = `apps/${app}`;
51
139
  const isVisible = ({ owner }: AkanGraphNode) => !owner || owner === appOwner || owner.startsWith("libs/");
@@ -54,16 +142,61 @@ export class WorkspaceGraph {
54
142
  return { version: 1, nodes, edges: graph.edges.filter((edge) => ids.has(edge.from) && ids.has(edge.to)) };
55
143
  }
56
144
 
57
- static answer(graph: AkanGraph, scope: GraphScope) {
145
+ static changesVisibleTo(changes: AkanChangeMap, app: string): AkanChangeMap {
146
+ const visible = WorkspaceGraph.visibleTo(changes, app);
147
+ const linked = new Set(visible.edges.flatMap((edge) => [edge.from, edge.to]));
148
+ const nodes = visible.nodes.filter((node) => node.change || linked.has(node.id));
149
+ const appPath = `apps/${app}/`;
150
+ return {
151
+ ...changes,
152
+ nodes,
153
+ edges: visible.edges,
154
+ summary: WorkspaceGraph.#summaryOf(nodes),
155
+ unmapped: changes.unmapped.filter(({ file }) => file.startsWith(appPath) || file.startsWith("libs/")),
156
+ };
157
+ }
158
+
159
+ static #summaryOf(nodes: AkanGraphNode[]) {
160
+ const summary = { added: 0, removed: 0, changed: 0 };
161
+ for (const { change } of nodes) if (change) summary[change] += 1;
162
+ return summary;
163
+ }
164
+
165
+ static answerChanges(changes: AkanChangeMap | { error: string }, scope: GraphScope) {
166
+ if ("error" in changes)
167
+ return {
168
+ diagnostics: [{ severity: "error", code: "graph-since-unknown", message: changes.error, input: "since" }],
169
+ };
170
+ const answer = WorkspaceGraph.answer(
171
+ changes,
172
+ scope,
173
+ changes.nodes.filter((node) => node.change),
174
+ );
175
+ if ("diagnostics" in answer) return { since: changes.since, summary: changes.summary, ...answer };
176
+ const summary = WorkspaceGraph.#summaryOf(answer.nodes);
177
+ const owners = scope.app ? [`apps/${scope.app}/`, `libs/${scope.app}/`] : null;
178
+ const modules = scope.module ? [`/lib/${scope.module}/`, `/lib/_${scope.module}/`] : null;
179
+ const unmapped = changes.unmapped.filter(
180
+ ({ file }) =>
181
+ (!owners || owners.some((owner) => file.startsWith(owner))) &&
182
+ (!modules || modules.some((segment) => file.includes(segment))),
183
+ );
184
+ return { since: changes.since, summary, ...answer, unmapped };
185
+ }
186
+
187
+ static answer(graph: AkanGraph, scope: GraphScope, counted = graph.nodes) {
58
188
  if (scope.app || scope.module || graph.nodes.length <= unscopedNodeLimit) return WorkspaceGraph.scope(graph, scope);
189
+ const unscoped = WorkspaceGraph.#withoutPages(graph);
190
+ if (unscoped.nodes.length <= unscopedNodeLimit) return unscoped;
59
191
  const owners = new Map<string, number>();
60
- for (const node of graph.nodes) if (node.owner) owners.set(node.owner, (owners.get(node.owner) ?? 0) + 1);
192
+ for (const node of counted)
193
+ if (node.owner && !pageKinds.has(node.kind)) owners.set(node.owner, (owners.get(node.owner) ?? 0) + 1);
61
194
  return {
62
195
  diagnostics: [
63
196
  {
64
197
  severity: "error",
65
198
  code: "graph-scope-required",
66
- message: `The graph has ${graph.nodes.length} nodes. Pass app, module or both to scope it.`,
199
+ message: `The graph has ${unscoped.nodes.length} nodes besides its pages. Pass app, module or both to scope it.`,
67
200
  input: "app",
68
201
  },
69
202
  ],
@@ -8,6 +8,7 @@ export const appRootAllowedFiles = new Set([
8
8
  "CLAUDE.md",
9
9
  "akan.app.json",
10
10
  "akan.baseline.json",
11
+ "akan.budget.json",
11
12
  "akan.config.ts",
12
13
  "client.ts",
13
14
  "main.ts",