@akanjs/devkit 3.1.2-rc.4 → 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 (85) 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 +1 -1
  6. package/apiReference.test.ts +37 -0
  7. package/apiReference.ts +240 -0
  8. package/applicationBuildReporter.test.ts +25 -3
  9. package/applicationBuildReporter.ts +8 -1
  10. package/applicationBuildRunner.test.ts +34 -14
  11. package/applicationBuildRunner.ts +76 -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 +16 -7
  22. package/codeAgent/skills/akan-service/SKILL.md +48 -19
  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.ts +6 -2
  54. package/incrementalBuilder/builderGraphPayloads.ts +60 -0
  55. package/incrementalBuilder/incrementalBuilder.proc.test.ts +65 -1
  56. package/incrementalBuilder/incrementalBuilder.proc.ts +5 -36
  57. package/integration/devStability.integration.test.ts +1 -1
  58. package/integration/devStabilityHarness.test.ts +1 -1
  59. package/lint/no-deep-internal-import.grit +1 -1
  60. package/maintainabilityBaseline.ts +7 -2
  61. package/maintainabilityContract.test.ts +183 -0
  62. package/maintainabilityContract.ts +145 -30
  63. package/maintainabilityExceptionScanner.test.ts +13 -2
  64. package/maintainabilityExceptionScanner.ts +16 -6
  65. package/maintainabilityLimits.ts +12 -0
  66. package/maintainabilityReport.test.ts +35 -0
  67. package/maintainabilityReport.ts +29 -8
  68. package/mobile/nativePublicEnv.test.ts +5 -1
  69. package/package.json +3 -3
  70. package/routeJsBudget.test.ts +177 -0
  71. package/routeJsBudget.ts +222 -0
  72. package/structureScanner.test.ts +167 -0
  73. package/structureScanner.ts +164 -0
  74. package/transforms/barrelAnalyzer.ts +10 -3
  75. package/transforms/barrelImportsPlugin.ts +13 -7
  76. package/transforms/serverEnvPlugin.test.ts +1 -1
  77. package/transforms/serverEnvPlugin.ts +1 -1
  78. package/transforms/transforms.test.ts +64 -12
  79. package/transforms/tsconfigPackageResolver.test.ts +12 -0
  80. package/workflow/plan.ts +79 -17
  81. package/workflow/types.ts +1 -0
  82. package/workspaceGraph.test.ts +58 -0
  83. package/workspaceGraph.ts +32 -3
  84. package/workspaceLayout.ts +1 -0
  85. /package/integration/{devStabilityHarness.ts → devStabilityHarness.fixture.ts} +0 -0
package/gitRevision.ts CHANGED
@@ -43,8 +43,12 @@ export class GitRevision {
43
43
  };
44
44
  }
45
45
 
46
- async filesAt(sha: string, isWanted: (file: string) => boolean): Promise<Map<string, string>> {
47
- const listed = await this.#git(["ls-tree", "-r", "-z", "--name-only", sha, "--", ...systemPaths]);
46
+ async filesAt(
47
+ sha: string,
48
+ isWanted: (file: string) => boolean,
49
+ paths: string[] = systemPaths,
50
+ ): Promise<Map<string, string>> {
51
+ const listed = await this.#git(["ls-tree", "-r", "-z", "--name-only", sha, "--", ...paths]);
48
52
  if (listed === null) throw new Error(`git ls-tree failed for ${sha}`);
49
53
  const files = listed.split("\0").filter((file) => file && isWanted(file));
50
54
  const texts = new Map<string, string>();
@@ -0,0 +1,60 @@
1
+ import type { App } from "@akanjs/devkit/commandDecorators";
2
+ import { FrameworkGraph } from "@akanjs/devkit/frameworkGraph";
3
+ import { GitRevision } from "@akanjs/devkit/gitRevision";
4
+ import { WorkspaceGraph } from "@akanjs/devkit/workspaceGraph";
5
+ import type { BuildChangesPayload, BuilderGraphReq, BuildFrameworkPayload, BuildGraphPayload } from "akanjs/server";
6
+
7
+ export class BuilderGraphPayloads {
8
+ readonly #app: App;
9
+ readonly #generation: () => number;
10
+ #graph: { generation: number; payload: BuildGraphPayload } | null = null;
11
+ #changes: { key: string; payload: Promise<BuildChangesPayload> } | null = null;
12
+
13
+ constructor(app: App, generation: () => number) {
14
+ this.#app = app;
15
+ this.#generation = generation;
16
+ }
17
+
18
+ touched(files: string[]) {
19
+ FrameworkGraph.touched(this.#app.workspace.workspaceRoot, files);
20
+ }
21
+
22
+ async of(msg: BuilderGraphReq): Promise<BuildGraphPayload | BuildChangesPayload | BuildFrameworkPayload> {
23
+ if (msg.framework) return await this.#framework();
24
+ return msg.changes ? await this.#changesSince(msg.changes.since) : await this.#workspaceGraph();
25
+ }
26
+
27
+ async #workspaceGraph(): Promise<BuildGraphPayload> {
28
+ const generation = this.#generation();
29
+ if (this.#graph?.generation !== generation) {
30
+ const { workspace } = this.#app;
31
+ const graph = WorkspaceGraph.visibleTo(await WorkspaceGraph.build(workspace), this.#app.name);
32
+ this.#graph = { generation, payload: { workspaceRoot: workspace.workspaceRoot, graph } };
33
+ }
34
+ return this.#graph.payload;
35
+ }
36
+
37
+ async #framework(): Promise<BuildFrameworkPayload> {
38
+ const { workspaceRoot } = this.#app.workspace;
39
+ return { workspaceRoot, framework: await FrameworkGraph.cached(workspaceRoot) };
40
+ }
41
+
42
+ async #changesSince(since: string | null): Promise<BuildChangesPayload> {
43
+ const { workspace, name } = this.#app;
44
+ const { workspaceRoot } = workspace;
45
+ const forkPoint = await new GitRevision(workspaceRoot).forkPointOf(since);
46
+ if (forkPoint.error !== undefined) return { workspaceRoot, changes: { error: forkPoint.error } };
47
+ const key = `${this.#generation()}:${forkPoint.sha}`;
48
+ if (this.#changes?.key !== key) {
49
+ const payload = WorkspaceGraph.changesAt(workspace, forkPoint.sha).then((changes) => ({
50
+ workspaceRoot,
51
+ changes: WorkspaceGraph.changesVisibleTo(changes, name),
52
+ }));
53
+ payload.catch(() => {
54
+ if (this.#changes?.payload === payload) this.#changes = null;
55
+ });
56
+ this.#changes = { key, payload };
57
+ }
58
+ return await this.#changes.payload;
59
+ }
60
+ }
@@ -218,7 +218,11 @@ process.send({ type: "build-batch-result", data: { generation, errors: {}, artif
218
218
  const graph = answerTo(5);
219
219
  if (!("graph" in graph)) throw new Error("build-graph answered with changes");
220
220
  expect(graph.workspaceRoot).toBe(root);
221
- expect(graph.graph.nodes.map((node) => node.id).sort()).toEqual(["model:apps/demo:memo", "model:libs/shared:file"]);
221
+ expect(graph.graph.nodes.map((node) => node.id).sort()).toEqual([
222
+ "model:apps/demo:memo",
223
+ "model:libs/shared:file",
224
+ "route:apps/demo:/",
225
+ ]);
222
226
  const changes = answerTo(6);
223
227
  if (!("changes" in changes) || "error" in changes.changes)
224
228
  throw new Error(`no changes: ${JSON.stringify(changes)}`);
@@ -227,6 +231,66 @@ process.send({ type: "build-batch-result", data: { generation, errors: {}, artif
227
231
  expect(answerTo(7)).toEqual(changes);
228
232
  }, 30_000);
229
233
 
234
+ test("maps the framework on request, and maps it again after a save under pkgs/akanjs", async () => {
235
+ const { root } = track(await createTempApp("demo"));
236
+ const appDir = path.join(root, "apps/demo");
237
+ await writeText(
238
+ path.join(root, "pkgs/@akanjs/devkit/incrementalBuilder/buildBatch.proc.ts"),
239
+ `const { generation } = JSON.parse(process.argv[2]);
240
+ process.send({ type: "build-batch-result", data: { generation, errors: {}, artifact: {}, optimizedFonts: { css: "", files: [] } } });
241
+ `,
242
+ );
243
+ await writeText(path.join(appDir, "page/_index.tsx"), "export default () => null;\n");
244
+ await writeText(
245
+ path.join(appDir, "tsconfig.json"),
246
+ JSON.stringify({ compilerOptions: { target: "ESNext", paths: { "akanjs/*": ["./pkgs/akanjs/*"] } } }),
247
+ );
248
+ await writeText(path.join(root, "pkgs/akanjs/base/scalar.ts"), "export class Scalar {}\n");
249
+ const messages: BuilderMessage[] = [];
250
+ const proc = Bun.spawn(["bun", path.join(import.meta.dir, "incrementalBuilder.proc.ts")], {
251
+ cwd: appDir,
252
+ env: {
253
+ ...process.env,
254
+ AKAN_WORKSPACE_ROOT: root,
255
+ AKAN_PUBLIC_APP_NAME: "demo",
256
+ AKAN_PUBLIC_REPO_NAME: "repo",
257
+ AKAN_PUBLIC_SERVE_DOMAIN: "localhost",
258
+ AKAN_PUBLIC_ENV: "local",
259
+ },
260
+ stdio: ["ignore", "ignore", "pipe"],
261
+ serialization: "advanced",
262
+ ipc: (message: BuilderMessage) => {
263
+ messages.push(message);
264
+ },
265
+ });
266
+ const classesAnsweredTo = async (id: number) => {
267
+ proc.send({ type: "build-graph", id, framework: true } satisfies BuilderMessage);
268
+ await until(proc, `build-graph-res ${id}`, () =>
269
+ messages.some((message) => message.type === "build-graph-res" && message.id === id),
270
+ );
271
+ const answer = messages.find((message) => message.type === "build-graph-res" && message.id === id);
272
+ if (answer?.type !== "build-graph-res" || !answer.ok || !("framework" in answer.data))
273
+ throw new Error(`no framework map: ${JSON.stringify(answer)}`);
274
+ expect(answer.data.workspaceRoot).toBe(root);
275
+ return answer.data.framework?.facets.flatMap(({ classes }) => classes.map(({ name }) => name));
276
+ };
277
+ try {
278
+ await until(proc, "builder-ready", () => messages.some((message) => message.type === "builder-ready"));
279
+ expect(await classesAnsweredTo(1)).toEqual(["Scalar"]);
280
+ await writeText(path.join(root, "pkgs/akanjs/base/scalar.ts"), "export class Scalar {}\nexport class Unit {}\n");
281
+ const deadline = Date.now() + 15_000;
282
+ let id = 2;
283
+ while (Date.now() < deadline) {
284
+ if ((await classesAnsweredTo(id++))?.includes("Unit")) break;
285
+ await wait(200);
286
+ }
287
+ expect(await classesAnsweredTo(id)).toEqual(["Scalar", "Unit"]);
288
+ } finally {
289
+ proc.kill();
290
+ await proc.exited;
291
+ }
292
+ }, 40_000);
293
+
230
294
  test("answers a second shell request in the same generation without building again", async () => {
231
295
  const { root } = track(await createTempApp("demo"));
232
296
  const appDir = path.join(root, "apps/demo");
@@ -18,18 +18,14 @@ import { RouteClientBuilder } from "@akanjs/devkit/frontendBuild/routeClientBuil
18
18
  import { ServerGraphFile } from "@akanjs/devkit/frontendBuild/serverGraphFile";
19
19
  import { SsrDevBundler } from "@akanjs/devkit/frontendBuild/ssrDevBundler";
20
20
  import { WatchRootResolver } from "@akanjs/devkit/frontendBuild/watchRootResolver";
21
- import { GitRevision } from "@akanjs/devkit/gitRevision";
22
- import { WorkspaceGraph } from "@akanjs/devkit/workspaceGraph";
23
21
  import { Logger } from "akanjs/common";
24
22
  import type {
25
23
  BaseBuildArtifact,
26
- BuildChangesPayload,
27
24
  BuilderCsrReq,
28
25
  BuilderGraphReq,
29
26
  BuilderMessage,
30
27
  BuilderReq,
31
28
  BuilderRes,
32
- BuildGraphPayload,
33
29
  BuildPhase,
34
30
  BuildRouteResultPayload,
35
31
  ChangeBatch,
@@ -39,6 +35,7 @@ import { CSR_DEV_PATCHING_MARKER } from "akanjs/server/hmr/csrDevManifest";
39
35
  import type { BuildBatchNeed, BuildBatchRequest, BuildBatchResult, OptimizedFonts } from "./buildBatchProtocol";
40
36
  import { BuildBatchRunner } from "./buildBatchRunner";
41
37
  import { BuilderChannel } from "./builderChannel";
38
+ import { BuilderGraphPayloads } from "./builderGraphPayloads";
42
39
  import { type BatchJob, BuilderWorkQueue, type DiscoveryJob } from "./builderWorkQueue";
43
40
  import { prepareDevWatchBatch } from "./devWatchBatch";
44
41
 
@@ -69,8 +66,7 @@ class IncrementalBuilder {
69
66
  #watcher: HmrWatcher | null = null;
70
67
  #generation = 0;
71
68
  #shellKey: string | null = null;
72
- #graph: { generation: number; payload: BuildGraphPayload } | null = null;
73
- #changes: { key: string; payload: Promise<BuildChangesPayload> } | null = null;
69
+ readonly #graphPayloads: BuilderGraphPayloads;
74
70
  #discoveredThrough = 0;
75
71
  #csrActive = IncrementalBuilder.#csrArmedByEnv();
76
72
  #csrBundler: CsrDevBundler;
@@ -122,6 +118,7 @@ class IncrementalBuilder {
122
118
  this.#discovery = options.discovery;
123
119
  this.#generation = options.initialGeneration ?? 0;
124
120
  this.#discoveredThrough = this.#generation;
121
+ this.#graphPayloads = new BuilderGraphPayloads(options.app, () => this.#generation);
125
122
  this.#batchRunner = new BuildBatchRunner({
126
123
  workspaceRoot: options.app.workspace.workspaceRoot,
127
124
  cwd: options.app.cwdPath,
@@ -370,6 +367,7 @@ class IncrementalBuilder {
370
367
  }
371
368
 
372
369
  async #handleWatchBatch(batch: ChangeBatch) {
370
+ this.#graphPayloads.touched(batch.files);
373
371
  const rawKinds = new Set(batch.kinds);
374
372
  if (rawKinds.size === 0) return;
375
373
  const generation = ++this.#generation;
@@ -882,7 +880,7 @@ class IncrementalBuilder {
882
880
 
883
881
  async handleBuildGraph(msg: BuilderGraphReq): Promise<void> {
884
882
  try {
885
- const data = msg.changes ? await this.#changesPayload(msg.changes.since) : await this.#graphPayload();
883
+ const data = await this.#graphPayloads.of(msg);
886
884
  await BuilderChannel.send({ type: "build-graph-res", id: msg.id, ok: true, data });
887
885
  } catch (err) {
888
886
  const error = ApplicationBuildReporter.formatError(err, this.#app.workspace.workspaceRoot);
@@ -891,35 +889,6 @@ class IncrementalBuilder {
891
889
  }
892
890
  }
893
891
 
894
- async #graphPayload(): Promise<BuildGraphPayload> {
895
- const generation = this.#generation;
896
- if (this.#graph?.generation !== generation) {
897
- const { workspace } = this.#app;
898
- const graph = WorkspaceGraph.visibleTo(await WorkspaceGraph.build(workspace), this.#app.name);
899
- this.#graph = { generation, payload: { workspaceRoot: workspace.workspaceRoot, graph } };
900
- }
901
- return this.#graph.payload;
902
- }
903
-
904
- async #changesPayload(since: string | null): Promise<BuildChangesPayload> {
905
- const { workspace, name } = this.#app;
906
- const { workspaceRoot } = workspace;
907
- const forkPoint = await new GitRevision(workspaceRoot).forkPointOf(since);
908
- if (forkPoint.error !== undefined) return { workspaceRoot, changes: { error: forkPoint.error } };
909
- const key = `${this.#generation}:${forkPoint.sha}`;
910
- if (this.#changes?.key !== key) {
911
- const payload = WorkspaceGraph.changesAt(workspace, forkPoint.sha).then((changes) => ({
912
- workspaceRoot,
913
- changes: WorkspaceGraph.changesVisibleTo(changes, name),
914
- }));
915
- payload.catch(() => {
916
- if (this.#changes?.payload === payload) this.#changes = null;
917
- });
918
- this.#changes = { key, payload };
919
- }
920
- return await this.#changes.payload;
921
- }
922
-
923
892
  // On demand: dev serves CSR only via the opt-in `/__csr` and `?csr=true` routes; once built, it rebuilds every save.
924
893
  async handleBuildCsr(msg: BuilderCsrReq): Promise<void> {
925
894
  if (msg.shell) {
@@ -1,7 +1,7 @@
1
1
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
2
  import { AkanAppHost } from "../akanApp";
3
3
  import { DevGeneratedIndexSync } from "../frontendBuild";
4
- import { DevStabilityHarness, type DevStabilityHmrProbe } from "./devStabilityHarness";
4
+ import { DevStabilityHarness, type DevStabilityHmrProbe } from "./devStabilityHarness.fixture";
5
5
 
6
6
  const integrationEnabled = process.env.AKAN_DEV_STABILITY_INTEGRATION === "1";
7
7
  // Contention pushes a cold dev-server boot from ~3s to nearly a minute; 120s ran out mid-restart.
@@ -2,7 +2,7 @@ import { afterEach, describe, expect, test } from "bun:test";
2
2
  import { mkdir, readdir, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { tempDirs } from "../testHelpers";
5
- import { DevStabilityHarness } from "./devStabilityHarness";
5
+ import { DevStabilityHarness } from "./devStabilityHarness.fixture";
6
6
 
7
7
  const servers: Bun.Server<undefined>[] = [];
8
8
  const makeRoot = tempDirs("akan-harness-port-");
@@ -19,7 +19,7 @@ or {
19
19
  $source <: r"\"\.\./\.\./.*\"",
20
20
  register_diagnostic(
21
21
  span = $source,
22
- message = "Module files should not import from two or more parent directories."
22
+ message = "Module files should not import from two or more parent directories. Import from the layer barrel instead: @apps/<app>/<layer> or @libs/<lib>/<layer>."
23
23
  )
24
24
  },
25
25
  JsModuleSource() as $source where {
@@ -16,14 +16,16 @@ export interface BaselineEntry {
16
16
 
17
17
  type Counts = ReadonlyMap<string, number>;
18
18
 
19
- //* One section per rule, each key holding what the gate measured: lines, store accesses, writes or calls. A count
20
- //* only falls, so a recorded entry cannot take on more of the debt it records.
19
+ //* One section per rule, each key holding what the gate measured: lines, members, store accesses, writes or calls.
20
+ //* A count only falls, so a recorded entry cannot take on more of the debt it records.
21
21
  export class MaintainabilityBaseline {
22
22
  static readonly fileName = "akan.baseline.json";
23
23
  static readonly reasonMinChars = 10;
24
24
  static readonly debtRules = [
25
25
  "akan.size.file",
26
26
  "akan.size.component",
27
+ "akan.size.function",
28
+ "akan.size.class",
27
29
  "akan.component.store-access",
28
30
  "akan.store.multi-write",
29
31
  "akan.store.root-write-chain",
@@ -34,12 +36,15 @@ export class MaintainabilityBaseline {
34
36
  static readonly reasonedRules: ReadonlySet<DebtRuleId> = new Set([
35
37
  "akan.size.file",
36
38
  "akan.size.component",
39
+ "akan.size.function",
40
+ "akan.size.class",
37
41
  "akan.component.store-access",
38
42
  "akan.service.call-in-loop",
39
43
  ]);
40
44
 
41
45
  static countOf(violation: MaintainabilityViolation) {
42
46
  if (violation.kind === "size") return violation.lineCount;
47
+ if (violation.kind === "classSize") return violation.memberCount;
43
48
  if (violation.kind === "storeAccess") return violation.accessCount;
44
49
  if (violation.kind === "multiWrite") return violation.writeCount;
45
50
  if (violation.kind === "writePlacement") return violation.writes.length;
@@ -498,6 +498,8 @@ describe("MaintainabilityContract rules", () => {
498
498
  expect(MaintainabilityContract.ruleIds).toEqual([
499
499
  "akan.size.file",
500
500
  "akan.size.component",
501
+ "akan.size.function",
502
+ "akan.size.class",
501
503
  "akan.store.multi-write",
502
504
  "akan.store.root-write-chain",
503
505
  "akan.component.write-chain",
@@ -844,3 +846,184 @@ export class CourtEndpoint extends endpoint(srv.court, ({ mutation }) => ({
844
846
  expect(await contractOf(root).enforce({ fix: false })).toEqual([]);
845
847
  });
846
848
  });
849
+
850
+ describe("MaintainabilityContract packages", () => {
851
+ const packageOf = (root: string) => new MaintainabilityContract(root, { type: "pkg", name: "@demo/core" });
852
+ const functionOf = (name: string, lineNum: number) => `export function ${name}() {\n${linesOf(lineNum - 2)}\n}\n`;
853
+ const classOf = (name: string, memberNum: number) =>
854
+ `export class ${name} {\n${Array.from({ length: memberNum }, (_, idx) => ` field${idx} = ${idx};`).join("\n")}\n}\n`;
855
+
856
+ test("holds a package file to 800 lines, a function to 150 and a class to 60 members, tests and barrels aside", async () => {
857
+ const root = await makeWorkspace({
858
+ "pkgs/@demo/core/server/host.ts": `${classOf("Host", 61)}${functionOf("boot", 151)}${linesOf(600)}`,
859
+ "pkgs/@demo/core/server/atLimit.ts": `${classOf("Small", 60)}${functionOf("fits", 150)}${linesOf(800 - 212)}\n`,
860
+ "pkgs/@demo/core/server/host.test.ts": `${functionOf("suite", 900)}`,
861
+ "pkgs/@demo/core/server/host.fixture.ts": `${functionOf("fixture", 900)}`,
862
+ "pkgs/@demo/core/server/host.instance.ts": `${functionOf("instance", 900)}`,
863
+ "pkgs/@demo/core/test/helpers.ts": `${functionOf("helper", 900)}`,
864
+ "pkgs/@demo/core/types.d.ts": linesOf(900),
865
+ "pkgs/@demo/core/index.ts": Array.from({ length: 801 }, (_, idx) => `export * from "./m${idx}";`).join("\n"),
866
+ });
867
+
868
+ expect(await packageOf(root).measure()).toEqual([
869
+ {
870
+ kind: "size",
871
+ rule: "akan.size.file",
872
+ key: "server/host.ts",
873
+ file: "server/host.ts",
874
+ line: 1,
875
+ lineCount: 814,
876
+ maxLines: 800,
877
+ },
878
+ {
879
+ kind: "size",
880
+ rule: "akan.size.function",
881
+ key: "server/host.ts#boot",
882
+ file: "server/host.ts",
883
+ line: 64,
884
+ lineCount: 151,
885
+ maxLines: 150,
886
+ function: "boot",
887
+ },
888
+ {
889
+ kind: "classSize",
890
+ rule: "akan.size.class",
891
+ key: "server/host.ts#Host",
892
+ file: "server/host.ts",
893
+ line: 1,
894
+ owner: "Host",
895
+ memberCount: 61,
896
+ maxMembers: 60,
897
+ },
898
+ ]);
899
+ });
900
+
901
+ test("reads a package in a repository through git, so ignored output is never measured", async () => {
902
+ const root = await makeWorkspace({
903
+ ".gitignore": "**/dist\n",
904
+ "pkgs/@demo/core/dist/bundle.ts": linesOf(900),
905
+ "pkgs/@demo/core/server/committed.ts": linesOf(801),
906
+ });
907
+ await commitAll(root);
908
+ await writeText(path.join(root, "pkgs/@demo/core/server/untracked.ts"), linesOf(802));
909
+
910
+ expect((await packageOf(root).measure()).map(({ key }) => key)).toEqual([
911
+ "server/committed.ts",
912
+ "server/untracked.ts",
913
+ ]);
914
+ });
915
+
916
+ test("measures nothing in a package git ignores whole, and leaves a package's examples out", async () => {
917
+ const root = await makeWorkspace({
918
+ ".gitignore": "pkgs/gen/\n",
919
+ "pkgs/gen/package.json": "{}\n",
920
+ "pkgs/gen/big.ts": functionOf("generated", 162),
921
+ "pkgs/@demo/core/examples/sample/src/selftest.ts": functionOf("runSelftest", 900),
922
+ });
923
+ await commitAll(root);
924
+
925
+ expect(await new MaintainabilityContract(root, { type: "pkg", name: "gen" }).measure()).toEqual([]);
926
+ expect(await packageOf(root).measure()).toEqual([]);
927
+ });
928
+
929
+ test("numbers only the breaches sharing a name, so a short function above leaves a recorded key in place", async () => {
930
+ const callbackOf = (receiver: string, lineNum: number) => `${receiver}.map(() => {\n${linesOf(lineNum - 2)}\n});\n`;
931
+ const root = await makeWorkspace({
932
+ "pkgs/@demo/core/run.ts": `ys.map(() => 1);\n${callbackOf("xs", 160)}${callbackOf("zs", 170)}`,
933
+ });
934
+
935
+ expect((await packageOf(root).measure()).map(({ key }) => key)).toEqual(["run.ts#map()", "run.ts#map()[2]"]);
936
+ });
937
+
938
+ test("records a package's breaches in its own baseline, which only shrinks from the default branch", async () => {
939
+ const root = await makeWorkspace({ "pkgs/@demo/core/server/host.ts": functionOf("boot", 200) });
940
+
941
+ expect((await packageOf(root).seed()).result).toBe("written");
942
+ expect(await readFile(path.join(root, "pkgs/@demo/core/akan.baseline.json"), "utf8")).toBe(
943
+ '{\n "akan.size.function": {\n "server/host.ts#boot": 200\n }\n}\n',
944
+ );
945
+ await commitAll(root);
946
+ await writeText(path.join(root, "pkgs/@demo/core/server/host.ts"), functionOf("boot", 210));
947
+
948
+ const contract = packageOf(root);
949
+ const failures = await contract.enforce({ fix: false });
950
+
951
+ expect(failures).toMatchObject([{ rule: "akan.size.function", key: "server/host.ts#boot", recorded: 200 }]);
952
+ expect(contract.describe(failures)).toBe(
953
+ "[maintainability] 1 violation(s) in pkgs/@demo/core:\n" +
954
+ " pkgs/@demo/core/server/host.ts:1 akan.size.function function boot has 210 lines, over the 150-line limit, up from 200\n" +
955
+ "How to fix:\n" +
956
+ " function: Split it by family into named functions or a collaborator class, each under the limit, keeping its signature for callers.",
957
+ );
958
+ });
959
+ });
960
+
961
+ describe("MaintainabilityContract reference without a baseline", () => {
962
+ const packageOf = (root: string) => new MaintainabilityContract(root, { type: "pkg", name: "@demo/core" });
963
+ const functionOf = (name: string, lineNum: number) => `export function ${name}() {\n${linesOf(lineNum - 2)}\n}\n`;
964
+ const host = "pkgs/@demo/core/server/host.ts";
965
+ const writePackageBaseline = async (root: string, json: object) =>
966
+ await writeText(path.join(root, "pkgs/@demo/core/akan.baseline.json"), `${JSON.stringify(json, null, 2)}\n`);
967
+
968
+ test("refuses an entry for debt the fork point's code did not hold, unless a person gave it a reason", async () => {
969
+ const root = await makeWorkspace({ [host]: functionOf("boot", 100) });
970
+ await commitAll(root);
971
+ await writeText(path.join(root, host), functionOf("boot", 200));
972
+ await writePackageBaseline(root, { "akan.size.function": { "server/host.ts#boot": 200 } });
973
+
974
+ const contract = packageOf(root);
975
+ const failures = await contract.enforce({ fix: false });
976
+
977
+ expect(failures).toMatchObject([
978
+ { kind: "baselineGrowth", key: "server/host.ts#boot", count: 200, committed: undefined, measured: true, line: 3 },
979
+ ]);
980
+ expect(contract.describe(failures)).toMatch(
981
+ /pkgs\/@demo\/core\/akan\.baseline\.json:3 {2}akan\.baseline\.growth {2}entry "server\/host\.ts#boot" under akan\.size\.function is debt the code at [0-9a-f]{12} does not hold/,
982
+ );
983
+
984
+ await writePackageBaseline(root, {
985
+ "akan.size.function": { "server/host.ts#boot": 200 },
986
+ reasons: { "server/host.ts#boot": "One state machine a person chose to keep whole." },
987
+ });
988
+ expect(await packageOf(root).enforce({ fix: false })).toEqual([]);
989
+ });
990
+
991
+ test("lets a scope adopt the gate with the debt its code held at the fork point, and no more", async () => {
992
+ const root = await makeWorkspace({ [host]: functionOf("boot", 200) });
993
+ await commitAll(root);
994
+
995
+ expect((await packageOf(root).seed()).result).toBe("written");
996
+ expect(await packageOf(root).enforce({ fix: false })).toEqual([]);
997
+
998
+ await writeText(path.join(root, host), functionOf("boot", 210));
999
+ await packageOf(root).seed({ force: true });
1000
+
1001
+ expect(await packageOf(root).enforce({ fix: false })).toMatchObject([
1002
+ { kind: "baselineGrowth", key: "server/host.ts#boot", count: 210, committed: 200, measured: true },
1003
+ ]);
1004
+ });
1005
+
1006
+ test("holds an app new to the branch, or one whose paid-off baseline was removed, to none", async () => {
1007
+ const zone = "apps/demo/lib/court/Court.Zone.tsx";
1008
+ const root = await makeWorkspace({ "apps/other/lib/court/Court.Zone.tsx": linesOf(10) });
1009
+ await commitAll(root);
1010
+ await writeText(path.join(root, zone), linesOf(450));
1011
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 450 } });
1012
+
1013
+ expect(await contractOf(root).enforce({ fix: false })).toMatchObject([
1014
+ { kind: "baselineGrowth", key: "lib/court/Court.Zone.tsx", count: 450, committed: undefined, measured: true },
1015
+ ]);
1016
+
1017
+ await writeText(path.join(root, zone), linesOf(400));
1018
+ await contractOf(root).enforce({ fix: true });
1019
+ expect(existsSync(path.join(root, "apps/demo/akan.baseline.json"))).toBe(false);
1020
+ await git(root, "add", "-A");
1021
+ await git(root, "commit", "-q", "-m", "paid off");
1022
+ await writeText(path.join(root, zone), linesOf(450));
1023
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 450 } });
1024
+
1025
+ expect(await contractOf(root).enforce({ fix: false })).toMatchObject([
1026
+ { kind: "baselineGrowth", key: "lib/court/Court.Zone.tsx", committed: undefined, measured: true },
1027
+ ]);
1028
+ });
1029
+ });