@akanjs/devkit 3.2.0-rc.1 → 3.2.0-rc.10

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 (108) hide show
  1. package/CHANGELOG.md +233 -11
  2. package/agentAssets.ts +1 -1
  3. package/akanApp/akanApp.host.test.ts +127 -0
  4. package/akanApp/akanApp.host.ts +50 -57
  5. package/akanApp/devHostPolicy.test.ts +11 -11
  6. package/akanApp/devHostPolicy.ts +2 -4
  7. package/akanContext.ts +2 -5
  8. package/akanMcpContract.ts +21 -10
  9. package/akanMcpToolError.ts +16 -0
  10. package/apiReport/apiDeclarationPrinter.ts +120 -0
  11. package/apiReport/apiReport.test.ts +497 -0
  12. package/apiReport/apiReport.ts +232 -0
  13. package/apiReport/apiReportDiff.ts +126 -0
  14. package/apiReport/apiReportPolicy.ts +65 -0
  15. package/apiReport/apiTargets.ts +311 -0
  16. package/apiReport/changesetBump.ts +19 -0
  17. package/applicationBuildReporter.test.ts +21 -0
  18. package/applicationBuildReporter.ts +8 -0
  19. package/applicationBuildRunner.test.ts +67 -0
  20. package/applicationBuildRunner.ts +6 -1
  21. package/artifact/implicitRootLayout.test.ts +4 -0
  22. package/artifact/implicitRootLayout.ts +13 -1
  23. package/artifact/storeBootstrap.ts +3 -2
  24. package/bareErrorScanner.ts +126 -0
  25. package/biome.base.json +2 -1
  26. package/codeAgent/skills/akan-debug/SKILL.md +7 -3
  27. package/codeAgent/skills/akan-err/SKILL.md +7 -0
  28. package/codeAgent/skills/akan-mcp/SKILL.md +7 -1
  29. package/codeAgent/skills/akan-scaffold/SKILL.md +3 -2
  30. package/codeAgent/skills/akan-transport/SKILL.md +2 -0
  31. package/codeAgent/skills/akan-validate/SKILL.md +1 -0
  32. package/codeAgent/tools/McpClient.ts +28 -6
  33. package/codeAgent/tools/McpToolPack.ts +1 -1
  34. package/codeAgent/tools/akanCodePlugins.test.ts +13 -1
  35. package/commandDecorators/argMeta.ts +4 -0
  36. package/commandDecorators/cliError.ts +72 -0
  37. package/commandDecorators/command.ts +186 -96
  38. package/commandDecorators/commandDecorators.test.ts +240 -17
  39. package/commandDecorators/exitCodeError.ts +6 -7
  40. package/commandDecorators/index.ts +1 -0
  41. package/commandDecorators/targetMeta.ts +3 -0
  42. package/conventionSet.test.ts +5 -2
  43. package/csrE2e/csrE2eHarness.fixture.ts +1 -1
  44. package/errorCodeReference.test.ts +282 -0
  45. package/errorCodeReference.ts +71 -0
  46. package/errorCodeScan.ts +551 -0
  47. package/frontendBuild/allRoutesBuilder.test.ts +16 -4
  48. package/frontendBuild/allRoutesBuilder.ts +24 -6
  49. package/frontendBuild/buildRouteClient.test.ts +43 -0
  50. package/frontendBuild/bundleDefine.ts +2 -0
  51. package/frontendBuild/clientBuildTypes.ts +5 -2
  52. package/frontendBuild/clientEntryDiscovery.test.ts +80 -0
  53. package/frontendBuild/clientEntryDiscovery.ts +53 -9
  54. package/frontendBuild/csrArtifactBuilder.test.ts +48 -0
  55. package/frontendBuild/csrArtifactBuilder.ts +9 -3
  56. package/frontendBuild/csrDevBundler.ts +12 -2
  57. package/frontendBuild/csrDevModuleCompiler.test.ts +97 -0
  58. package/frontendBuild/csrDevModuleCompiler.ts +37 -8
  59. package/frontendBuild/csrDevPatcher.test.ts +89 -2
  60. package/frontendBuild/csrDevPatcher.ts +47 -7
  61. package/frontendBuild/csrDevTypes.ts +16 -0
  62. package/frontendBuild/cssCompiler.test.ts +63 -1
  63. package/frontendBuild/devBarrelImports.ts +132 -0
  64. package/frontendBuild/devRegistryBundler.ts +19 -4
  65. package/frontendBuild/mcpAppViewBuilder.test.ts +67 -0
  66. package/frontendBuild/nativeCore.test.ts +148 -0
  67. package/frontendBuild/precompressArtifacts.ts +1 -1
  68. package/frontendBuild/ssrBaseArtifactBuilder.test.ts +26 -0
  69. package/frontendBuild/ssrBaseArtifactBuilder.ts +23 -19
  70. package/frontendBuild/ssrDevBundler.ts +13 -2
  71. package/frontendBuild/vendorImportScanner.test.ts +24 -3
  72. package/frontendBuild/vendorImportScanner.ts +25 -10
  73. package/incrementalBuilder/incrementalBuilder.host.test.ts +30 -1
  74. package/incrementalBuilder/incrementalBuilder.host.ts +10 -22
  75. package/lint/__fixtures__/no-import-common-err/bad.tsx +13 -0
  76. package/lint/__fixtures__/no-import-common-err/good.tsx +12 -0
  77. package/lint/no-import-common-err.grit +31 -0
  78. package/lint/no-init-fetch-in-client.grit +1 -1
  79. package/lint/no-web-only-api-outside-webkit.grit +3 -3
  80. package/maintainabilityBaseline.test.ts +49 -0
  81. package/maintainabilityBaseline.ts +73 -8
  82. package/maintainabilityContract.test.ts +302 -1
  83. package/maintainabilityContract.ts +87 -22
  84. package/maintainabilityExceptionScanner.test.ts +14 -0
  85. package/maintainabilityExceptionScanner.ts +5 -1
  86. package/maintainabilityLimits.ts +12 -0
  87. package/maintainabilityReport.ts +20 -3
  88. package/moduleFieldNames.test.ts +56 -0
  89. package/moduleFieldNames.ts +25 -0
  90. package/package.json +3 -3
  91. package/preview/PreviewView.ts +32 -11
  92. package/preview/previewView.test.ts +76 -9
  93. package/routeJsBudget.test.ts +47 -11
  94. package/routeJsBudget.ts +88 -25
  95. package/transforms/barrelAnalyzer.ts +46 -1
  96. package/transforms/barrelImportsPlugin.ts +61 -45
  97. package/transforms/transforms.test.ts +67 -1
  98. package/typecheck/projectTypecheck.ts +5 -1
  99. package/typecheck/typecheck.proc.ts +3 -2
  100. package/typecheck/typecheckFailure.ts +6 -1
  101. package/workflow/artifacts.ts +25 -7
  102. package/workflow/fieldType.ts +92 -0
  103. package/workflow/index.ts +2 -0
  104. package/workflow/plainName.ts +28 -0
  105. package/workflow/plan.ts +67 -22
  106. package/workflow/source.ts +0 -5
  107. package/workspaceGraph.test.ts +200 -3
  108. package/workspaceGraph.ts +46 -8
@@ -0,0 +1,31 @@
1
+ engine biome(1.0)
2
+ language js(typescript, jsx)
3
+
4
+ // `akanjs/common`'s `Err` takes any string, so a mistyped key compiles and `no-throw-raw-error`, which only knows a raw
5
+ // `Error`, passes it. The `Err` from an app's or lib's dict is the same class typed with that dictionary's keys.
6
+ //
7
+ // `import type` stays legal, to annotate a caught `Err`: a type cannot be constructed. A namespace import is not
8
+ // matched, since the rule cannot tell `common.Err` from the helpers such an import is for.
9
+ or {
10
+ or {
11
+ JsShorthandNamedImportSpecifier() as $specifier where { $specifier <: r"Err" },
12
+ JsNamedImportSpecifier() as $specifier where { $specifier <: r"Err\s+as\s[\s\S]*" }
13
+ } where {
14
+ $specifier <: within JsImport() as $statement,
15
+ not $statement <: r"import\s+type[\s\S]*"
16
+ },
17
+ JsExportNamedFromSpecifier() as $specifier where {
18
+ $specifier <: r"Err(?:\s+as\s[\s\S]*)?",
19
+ $specifier <: within JsExport() as $statement,
20
+ not $statement <: r"export\s+type[\s\S]*"
21
+ }
22
+ } where {
23
+ $statement <: contains JsModuleSource() as $source where {
24
+ $source <: r"[\x22']akanjs/common(?:/(?:err|index)(?:\.ts)?)?[\x22']"
25
+ },
26
+ register_diagnostic(
27
+ span = $specifier,
28
+ message = "Do not import `Err` from akanjs/common: it takes any string, so a mistyped key compiles. Import the typed `Err` from the app/lib dict on the server (import { Err } from '../dict') or from the package client entrypoint in UI ('@libs/<lib>/client', '@apps/<app>/client'), and register the key in that module's dictionary `.error({})`. `common/` and `env/` have no legal `Err`: keep throwing code out of them. Write 'import type' if only the type is needed.",
29
+ severity = "error"
30
+ )
31
+ }
@@ -11,7 +11,7 @@ language js(typescript, jsx)
11
11
  // closure reaches nothing.
12
12
  //
13
13
  // The client is not missing the load, only this spelling of it: every slice also generates
14
- // `st.do.init<Model><Suffix>` (pkgs/akanjs/store/action.ts), which runs the same two queries and commits
14
+ // `st.do.init<Model><Suffix>` (pkgs/akanjs/store/sliceListActions.ts), which runs the same two queries and commits
15
15
  // them to state.
16
16
  //
17
17
  // Both names are matched by shape, since a lint rule cannot know which slices exist. The generated one is
@@ -13,7 +13,7 @@ or {
13
13
  $access <: or { `navigator.share`, `navigator.canShare` },
14
14
  register_diagnostic(
15
15
  span = $access,
16
- message = "Android's WebView has no Web Share, so this does nothing in the app. Move it into a `webkit/` hook that branches on `isNativeApp()` from `akanjs/client/native`; the runtime's `share` plugin is the app side.",
16
+ message = "Android's WebView has no Web Share, so this does nothing in the app. Move it into a `webkit/` hook that branches on `isNativeApp()` from `akanjs/client`; the runtime's `share` plugin is the app side.",
17
17
  severity = "warn"
18
18
  )
19
19
  },
@@ -21,7 +21,7 @@ or {
21
21
  $access <: or { `navigator.serviceWorker`, `Notification.requestPermission`, `Notification.permission` },
22
22
  register_diagnostic(
23
23
  span = $access,
24
- message = "A native app page has no service worker or Notification API. Keep this in a `webkit/` hook behind `isNativeApp()`; native push is `push` from `akanjs/client/native`, and `usePushNotification` in libs/util wraps both.",
24
+ message = "A native app page has no service worker or Notification API. Keep this in a `webkit/` hook behind `isNativeApp()`; native push is `push` from `akanjs/client/native/push`, and `usePushNotification` in libs/util wraps both.",
25
25
  severity = "warn"
26
26
  )
27
27
  },
@@ -37,7 +37,7 @@ or {
37
37
  $access <: `navigator.vibrate`,
38
38
  register_diagnostic(
39
39
  span = $access,
40
- message = "iOS has no `navigator.vibrate`. Use `haptics` from `akanjs/client/native` in a `webkit/` hook.",
40
+ message = "iOS has no `navigator.vibrate`. Use `haptics` from `akanjs/client/native/haptics` in a `webkit/` hook.",
41
41
  severity = "warn"
42
42
  )
43
43
  }
@@ -39,6 +39,14 @@ const placementOf = (file: string, writes: string[]): MaintainabilityViolation =
39
39
  line: 1,
40
40
  writes,
41
41
  });
42
+ const bareOf = (file: string, count: number): MaintainabilityViolation => ({
43
+ kind: "bareError",
44
+ rule: "akan.error.bare-error",
45
+ key: file,
46
+ file,
47
+ line: 1,
48
+ lines: Array.from({ length: count }, (_, idx) => idx + 1),
49
+ });
42
50
  const baselineOf = (
43
51
  sections: { [rule in DebtRuleId]?: Record<string, number> },
44
52
  reasons: Record<string, string> = {},
@@ -217,6 +225,47 @@ describe("MaintainabilityBaseline", () => {
217
225
  expect(baseline.tightenedBy([]).reasons.size).toBe(0);
218
226
  });
219
227
 
228
+ test("holds a total rule to the sum of its section: a moved count passes, a raised sum names the files that rose", () => {
229
+ const baseline = baselineOf({ "akan.error.bare-error": { "server/a.ts": 2, "server/b.ts": 1 } });
230
+
231
+ expect(baseline.unallowed([bareOf("server/c.ts", 3)])).toEqual([]);
232
+ expect(baseline.unallowed([bareOf("server/a.ts", 3), bareOf("server/b.ts", 1)])).toEqual([
233
+ bareOf("server/a.ts", 3),
234
+ ]);
235
+ expect(baseline.tightenedBy([bareOf("server/c.ts", 2)]).sections.get("akan.error.bare-error")).toEqual(
236
+ new Map([["server/c.ts", 2]]),
237
+ );
238
+ expect(baseline.tightenedBy([bareOf("server/a.ts", 4)]).sections.get("akan.error.bare-error")).toEqual(
239
+ new Map([
240
+ ["server/a.ts", 2],
241
+ ["server/b.ts", 1],
242
+ ]),
243
+ );
244
+ expect(baselineOf({ "akan.error.bare-error": { "server/c.ts": 3 } }).grownFrom(baseline)).toEqual([]);
245
+ expect(baselineOf({ "akan.error.bare-error": { "server/c.ts": 4 } }).grownFrom(baseline)).toEqual([
246
+ { rule: "akan.error.bare-error", key: MaintainabilityBaseline.totalKey, count: 4, committed: 3 },
247
+ ]);
248
+ expect(baseline.grownFrom(new MaintainabilityBaseline())).toEqual([
249
+ { rule: "akan.error.bare-error", key: MaintainabilityBaseline.totalKey, count: 3, committed: undefined },
250
+ ]);
251
+ });
252
+
253
+ test("names a total recorded above the code's and no other, on a total rule only", () => {
254
+ const baseline = baselineOf({
255
+ "akan.size.file": { "lib/a/A.Zone.tsx": 900 },
256
+ "akan.error.bare-error": { "server/a.ts": 2, "server/b.ts": 1 },
257
+ });
258
+
259
+ expect(baseline.staleTotals([bareOf("server/a.ts", 2)])).toEqual([
260
+ { rule: "akan.error.bare-error", count: 2, recorded: 3 },
261
+ ]);
262
+ expect(baseline.staleTotals([])).toEqual([{ rule: "akan.error.bare-error", count: 0, recorded: 3 }]);
263
+ expect(baseline.staleTotals([bareOf("server/c.ts", 3)])).toEqual([]);
264
+ expect(baseline.staleTotals([bareOf("server/c.ts", 4)])).toEqual([]);
265
+ expect(baseline.tightenedBy([bareOf("server/a.ts", 1)]).staleTotals([bareOf("server/a.ts", 1)])).toEqual([]);
266
+ expect(new MaintainabilityBaseline().staleTotals([])).toEqual([]);
267
+ });
268
+
220
269
  test("prints the rule sections in rule order with sorted keys, as Biome would, and reprints them unchanged", () => {
221
270
  const baseline = MaintainabilityBaseline.of([
222
271
  writeOf("lib/z/z.store.ts#run"),
@@ -14,9 +14,16 @@ export interface BaselineEntry {
14
14
  reason?: string;
15
15
  }
16
16
 
17
+ export interface BaselineStaleTotal {
18
+ rule: DebtRuleId;
19
+ count: number;
20
+ recorded: number;
21
+ }
22
+
17
23
  type Counts = ReadonlyMap<string, number>;
18
24
 
19
- //* One section per rule, each key holding what the gate measured: lines, members, store accesses, writes or calls.
25
+ //* One section per rule, each key holding what the gate measured: lines, members, accesses, writes, calls or the
26
+ //* built-in Errors constructed or extended.
20
27
  //* A count only falls, so a recorded entry cannot take on more of the debt it records.
21
28
  export class MaintainabilityBaseline {
22
29
  static readonly fileName = "akan.baseline.json";
@@ -32,7 +39,13 @@ export class MaintainabilityBaseline {
32
39
  "akan.component.write-chain",
33
40
  "akan.write.placement",
34
41
  "akan.service.call-in-loop",
42
+ "akan.error.bare-error",
35
43
  ] as const satisfies readonly DebtRuleId[];
44
+ //* Added after baselines were committed: where the reference holds no section for one, its code there is the floor.
45
+ static readonly lateRules: ReadonlySet<DebtRuleId> = new Set(["akan.error.bare-error"]);
46
+ //* Held to the package's sum, so a moved or split file keeps its count; the per-file entries name the file that rose.
47
+ static readonly totalRules: ReadonlySet<DebtRuleId> = new Set(["akan.error.bare-error"]);
48
+ static readonly totalKey = "(total)";
36
49
  static readonly reasonedRules: ReadonlySet<DebtRuleId> = new Set([
37
50
  "akan.size.file",
38
51
  "akan.size.component",
@@ -48,6 +61,7 @@ export class MaintainabilityBaseline {
48
61
  if (violation.kind === "storeAccess") return violation.accessCount;
49
62
  if (violation.kind === "multiWrite") return violation.writeCount;
50
63
  if (violation.kind === "writePlacement") return violation.writes.length;
64
+ if (violation.kind === "bareError") return violation.lines.length;
51
65
  return violation.calls.length;
52
66
  }
53
67
 
@@ -116,6 +130,30 @@ export class MaintainabilityBaseline {
116
130
  return recorded !== undefined && MaintainabilityBaseline.countOf(violation) <= recorded;
117
131
  }
118
132
 
133
+ unallowed(violations: MaintainabilityViolation[]) {
134
+ const measured = MaintainabilityBaseline.of(violations);
135
+ return violations.filter(
136
+ (violation) =>
137
+ !(
138
+ MaintainabilityBaseline.totalRules.has(violation.rule) &&
139
+ measured.totalOf(violation.rule) <= this.totalOf(violation.rule)
140
+ ) && !this.allows(violation),
141
+ );
142
+ }
143
+
144
+ staleTotals(violations: MaintainabilityViolation[]): BaselineStaleTotal[] {
145
+ const measured = MaintainabilityBaseline.of(violations);
146
+ return [...MaintainabilityBaseline.totalRules].flatMap((rule) => {
147
+ const count = measured.totalOf(rule);
148
+ const recorded = this.totalOf(rule);
149
+ return count < recorded ? [{ rule, count, recorded }] : [];
150
+ });
151
+ }
152
+
153
+ totalOf(rule: DebtRuleId) {
154
+ return [...(this.sections.get(rule)?.values() ?? [])].reduce((sum, count) => sum + count, 0);
155
+ }
156
+
119
157
  recordedOf(violation: MaintainabilityViolation) {
120
158
  return this.sections.get(violation.rule)?.get(violation.key);
121
159
  }
@@ -140,13 +178,32 @@ export class MaintainabilityBaseline {
140
178
  );
141
179
  }
142
180
 
181
+ rulesOutside(other: MaintainabilityBaseline | null): DebtRuleId[] {
182
+ return [...this.sections]
183
+ .filter(
184
+ ([rule, counts]) =>
185
+ counts.size && (other === null || (MaintainabilityBaseline.lateRules.has(rule) && !other.sections.has(rule))),
186
+ )
187
+ .map(([rule]) => rule);
188
+ }
189
+
190
+ withSectionsOf(other: MaintainabilityBaseline | null, rules: ReadonlySet<DebtRuleId>) {
191
+ if (!other) return this;
192
+ const sections = new Map(this.sections);
193
+ for (const rule of rules) sections.set(rule, other.sections.get(rule) ?? new Map());
194
+ return new MaintainabilityBaseline(sections, this.reasons);
195
+ }
196
+
143
197
  tightenedBy(violations: MaintainabilityViolation[]) {
144
198
  const current = MaintainabilityBaseline.of(violations);
145
199
  const sections = new Map(
146
- [...this.sections].map(([rule, counts]) => [
147
- rule,
148
- MaintainabilityBaseline.#lowered(counts, current.sections.get(rule) ?? new Map()),
149
- ]),
200
+ [...this.sections].map(([rule, counts]) => {
201
+ const measured = current.sections.get(rule) ?? new Map<string, number>();
202
+ if (!MaintainabilityBaseline.totalRules.has(rule))
203
+ return [rule, MaintainabilityBaseline.#lowered(counts, measured)];
204
+ //? Over budget, a file that went down may only have moved its count; recording it would refuse that move later.
205
+ return [rule, current.totalOf(rule) > this.totalOf(rule) ? counts : measured];
206
+ }),
150
207
  );
151
208
  return new MaintainabilityBaseline(sections).#withReasonsOf(this.reasons);
152
209
  }
@@ -161,12 +218,20 @@ export class MaintainabilityBaseline {
161
218
  //* Only a shape entry grows with a reason: a write rule guards correctness, whatever the reason.
162
219
  grownFrom(committed: MaintainabilityBaseline): BaselineEntryGrowth[] {
163
220
  return [...this.sections].flatMap(([rule, counts]) =>
164
- MaintainabilityBaseline.#raised(counts, committed.sections.get(rule) ?? new Map())
165
- .filter(([key]) => !(MaintainabilityBaseline.reasonedRules.has(rule) && this.reasonOf(key)))
166
- .map(([key, count, recorded]) => ({ rule, key, count, committed: recorded })),
221
+ MaintainabilityBaseline.totalRules.has(rule)
222
+ ? this.#totalGrownFrom(committed, rule)
223
+ : MaintainabilityBaseline.#raised(counts, committed.sections.get(rule) ?? new Map())
224
+ .filter(([key]) => !(MaintainabilityBaseline.reasonedRules.has(rule) && this.reasonOf(key)))
225
+ .map(([key, count, recorded]) => ({ rule, key, count, committed: recorded })),
167
226
  );
168
227
  }
169
228
 
229
+ #totalGrownFrom(committed: MaintainabilityBaseline, rule: DebtRuleId): BaselineEntryGrowth[] {
230
+ const count = this.totalOf(rule);
231
+ const recorded = committed.sections.has(rule) ? committed.totalOf(rule) : undefined;
232
+ return count > (recorded ?? 0) ? [{ rule, key: MaintainabilityBaseline.totalKey, count, committed: recorded }] : [];
233
+ }
234
+
170
235
  stringify() {
171
236
  const sections = MaintainabilityBaseline.debtRules.flatMap((rule) => {
172
237
  const counts = this.sections.get(rule);
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import { existsSync } from "node:fs";
3
- import { readFile } from "node:fs/promises";
3
+ import { readFile, rm } from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { MaintainabilityBaseline } from "./maintainabilityBaseline";
6
6
  import { MaintainabilityContract } from "./maintainabilityContract";
@@ -506,6 +506,7 @@ describe("MaintainabilityContract rules", () => {
506
506
  "akan.component.store-access",
507
507
  "akan.write.placement",
508
508
  "akan.service.call-in-loop",
509
+ "akan.error.bare-error",
509
510
  "akan.baseline.growth",
510
511
  ]);
511
512
  const root = await makeWorkspace({
@@ -958,6 +959,306 @@ describe("MaintainabilityContract packages", () => {
958
959
  });
959
960
  });
960
961
 
962
+ describe("MaintainabilityContract bare errors", () => {
963
+ const manifest = '{ "name": "akanjs" }\n';
964
+ const akanjsOf = (root: string) => new MaintainabilityContract(root, { type: "pkg", name: "akanjs" });
965
+ const failing = (count: number, prefix = "f") =>
966
+ `${Array.from(
967
+ { length: count },
968
+ (_, idx) => `export const ${prefix}${idx} = () => {\n throw new Error("${idx}");\n};`,
969
+ ).join("\n")}\n`;
970
+ const shapes = `import { AkanError } from "akanjs/base";
971
+ import { Err } from "akanjs/dictionary";
972
+ export const a = () => {
973
+ throw new globalThis.Error("1");
974
+ };
975
+ export const b = () => {
976
+ throw Object.assign(new Error("2"), { statusCode: 400 });
977
+ };
978
+ export const c = (err?: Error) => {
979
+ throw err ?? Error("3");
980
+ };
981
+ export const d = (x: boolean) => {
982
+ throw x ? new TypeError("4a") : new RangeError("4b");
983
+ };
984
+ export const e = () => {
985
+ const failure = new Error("5");
986
+ throw failure;
987
+ };
988
+ export const f = () => {
989
+ throw (<Error>new Error("6"))!;
990
+ };
991
+ export const g = () => new Promise((_, reject) => reject(new Error("7") satisfies Error));
992
+ class LocalError extends Error {}
993
+ export const h = () => {
994
+ throw new LocalError("9");
995
+ };
996
+ export const i = (Error: ErrorConstructor) => {
997
+ throw new Error("shadowed by a parameter");
998
+ };
999
+ export const j = () => {
1000
+ const TypeError = class {};
1001
+ throw new TypeError();
1002
+ };
1003
+ export const k = (input: unknown) => {
1004
+ if (input === 1) throw new AkanError("coded", "coded", { statusCode: 400 });
1005
+ if (input === 2) throw new Err.NotFound("base.error.missing");
1006
+ if (input instanceof Error) Error.captureStackTrace(input);
1007
+ throw new DOMException("aborted", "AbortError");
1008
+ };
1009
+ `;
1010
+ const baselineAt = async (root: string) =>
1011
+ MaintainabilityBaseline.parse(
1012
+ await readFile(path.join(root, "pkgs/akanjs/akan.baseline.json"), "utf8"),
1013
+ ).sections.get("akan.error.bare-error");
1014
+
1015
+ test("counts each built-in Error constructed, rejected or extended under server/, service/ and signal/ of akanjs", async () => {
1016
+ const root = await makeWorkspace({
1017
+ "pkgs/akanjs/package.json": manifest,
1018
+ "pkgs/akanjs/server/shapes.ts": shapes,
1019
+ "pkgs/akanjs/signal/fail.ts": failing(1),
1020
+ "pkgs/akanjs/service/run.test.ts": failing(2),
1021
+ "pkgs/akanjs/client/run.ts": failing(2),
1022
+ "pkgs/akanjs/service/imported.ts": `import { Error } from "./errors";\nexport const fail = () => new Error("x");\n`,
1023
+ "pkgs/framework/package.json": manifest,
1024
+ "pkgs/framework/server/fail.ts": failing(1),
1025
+ "pkgs/other/package.json": '{ "name": "other" }\n',
1026
+ "pkgs/other/server/fail.ts": failing(1),
1027
+ });
1028
+
1029
+ expect(await akanjsOf(root).measure()).toEqual([
1030
+ {
1031
+ kind: "bareError",
1032
+ rule: "akan.error.bare-error",
1033
+ key: "server/shapes.ts",
1034
+ file: "server/shapes.ts",
1035
+ line: 4,
1036
+ lines: [4, 7, 10, 13, 13, 16, 20, 22, 23],
1037
+ },
1038
+ {
1039
+ kind: "bareError",
1040
+ rule: "akan.error.bare-error",
1041
+ key: "signal/fail.ts",
1042
+ file: "signal/fail.ts",
1043
+ line: 2,
1044
+ lines: [2],
1045
+ },
1046
+ ]);
1047
+ expect(await new MaintainabilityContract(root, { type: "pkg", name: "framework" }).measure()).toMatchObject([
1048
+ { rule: "akan.error.bare-error", key: "server/fail.ts" },
1049
+ ]);
1050
+ expect(await new MaintainabilityContract(root, { type: "pkg", name: "other" }).measure()).toEqual([]);
1051
+ });
1052
+
1053
+ test("holds the package's total, so a rename or a split passes and the entries follow the code", async () => {
1054
+ const root = await makeWorkspace({
1055
+ "pkgs/akanjs/package.json": manifest,
1056
+ "pkgs/akanjs/server/host.ts": failing(3),
1057
+ });
1058
+ await akanjsOf(root).seed();
1059
+ await commitAll(root);
1060
+ await git(root, "checkout", "-q", "-b", "feat");
1061
+
1062
+ await git(root, "mv", "pkgs/akanjs/server/host.ts", "pkgs/akanjs/server/moved.ts");
1063
+ expect(await akanjsOf(root).enforce({ fix: false })).toEqual([]);
1064
+ expect(await akanjsOf(root).enforce({ fix: true })).toEqual([]);
1065
+ expect(await baselineAt(root)).toEqual(new Map([["server/moved.ts", 3]]));
1066
+
1067
+ await writeText(path.join(root, "pkgs/akanjs/server/moved.ts"), failing(1));
1068
+ await writeText(path.join(root, "pkgs/akanjs/signal/extracted.ts"), failing(2, "g"));
1069
+ expect(await akanjsOf(root).enforce({ fix: false })).toEqual([]);
1070
+ expect(await akanjsOf(root).enforce({ fix: true })).toEqual([]);
1071
+ expect(await baselineAt(root)).toEqual(
1072
+ new Map([
1073
+ ["server/moved.ts", 1],
1074
+ ["signal/extracted.ts", 2],
1075
+ ]),
1076
+ );
1077
+
1078
+ await writeText(path.join(root, "pkgs/akanjs/signal/extracted.ts"), failing(1, "g"));
1079
+ expect(await akanjsOf(root).enforce({ fix: true })).toEqual([]);
1080
+ expect(await baselineAt(root)).toEqual(
1081
+ new Map([
1082
+ ["server/moved.ts", 1],
1083
+ ["signal/extracted.ts", 1],
1084
+ ]),
1085
+ );
1086
+ });
1087
+
1088
+ test("fails a new bare Error anywhere, naming the file whose count rose", async () => {
1089
+ const root = await makeWorkspace({
1090
+ "pkgs/akanjs/package.json": manifest,
1091
+ "pkgs/akanjs/server/host.ts": failing(3),
1092
+ });
1093
+ await akanjsOf(root).seed();
1094
+ await commitAll(root);
1095
+ await writeText(path.join(root, "pkgs/akanjs/service/fresh.ts"), failing(1));
1096
+
1097
+ const contract = akanjsOf(root);
1098
+ const failures = await contract.enforce({ fix: false });
1099
+
1100
+ expect(failures).toMatchObject([{ rule: "akan.error.bare-error", key: "service/fresh.ts", lines: [2] }]);
1101
+ expect(contract.describe(failures)).toBe(
1102
+ "[maintainability] 1 violation(s) in pkgs/akanjs:\n" +
1103
+ " pkgs/akanjs/service/fresh.ts:2 akan.error.bare-error constructs or extends 1 bare Error, on line 2, and the package's total grew\n" +
1104
+ "How to fix:\n" +
1105
+ " bare Error: Construct an `AkanError` subclass from `akanjs/base` with a code the caller can branch on, and a `statusCode` when it answers a request: an existing class whose code fits, or a new one beside the code that fails; a class extending a built-in Error extends `AkanError` instead. A failure another process reported travels as itself. The package's total is what is held, so moving a bare Error between files passes and `akan lint` rewrites the entries.",
1106
+ );
1107
+
1108
+ await writeText(path.join(root, "pkgs/akanjs/server/host.ts"), failing(4));
1109
+ await Bun.write(path.join(root, "pkgs/akanjs/service/fresh.ts"), "export const fine = 1;\n");
1110
+ expect(await akanjsOf(root).enforce({ fix: false })).toMatchObject([
1111
+ { rule: "akan.error.bare-error", key: "server/host.ts", lines: [2, 5, 8, 11], recorded: 3 },
1112
+ ]);
1113
+ });
1114
+
1115
+ test("refuses a working total raised over the fork point's, whatever file it is recorded under", async () => {
1116
+ const root = await makeWorkspace({
1117
+ "pkgs/akanjs/package.json": manifest,
1118
+ "pkgs/akanjs/server/host.ts": failing(2),
1119
+ });
1120
+ await akanjsOf(root).seed();
1121
+ await commitAll(root);
1122
+ await writeText(path.join(root, "pkgs/akanjs/server/host.ts"), failing(3));
1123
+ await akanjsOf(root).seed({ force: true });
1124
+
1125
+ const contract = akanjsOf(root);
1126
+ const failures = await contract.enforce({ fix: false });
1127
+
1128
+ expect(failures).toMatchObject([
1129
+ { kind: "baselineGrowth", debtRule: "akan.error.bare-error", key: "(total)", count: 3, committed: 2, line: 2 },
1130
+ ]);
1131
+ expect(contract.describe(failures)).toMatch(
1132
+ /akan\.baseline\.json:2 {2}akan\.baseline\.growth {2}akan\.error\.bare-error allows 3 bare Errors in all, above the 2 at [0-9a-f]{12}; the baseline may only shrink/,
1133
+ );
1134
+ });
1135
+
1136
+ test("refuses a paydown that keeps the baseline, so a later change cannot spend what it paid", async () => {
1137
+ const root = await makeWorkspace({
1138
+ "pkgs/akanjs/package.json": manifest,
1139
+ "pkgs/akanjs/server/a.ts": failing(3),
1140
+ });
1141
+ await akanjsOf(root).seed();
1142
+ await commitAll(root);
1143
+ await git(root, "checkout", "-q", "-b", "pr-1");
1144
+ await writeText(path.join(root, "pkgs/akanjs/server/a.ts"), failing(1));
1145
+
1146
+ const contract = akanjsOf(root);
1147
+ const failures = await contract.enforce({ fix: false });
1148
+
1149
+ expect(failures).toEqual([
1150
+ {
1151
+ kind: "staleTotal",
1152
+ rule: "akan.error.bare-error",
1153
+ key: "(total)",
1154
+ file: "akan.baseline.json",
1155
+ line: 2,
1156
+ count: 1,
1157
+ recorded: 3,
1158
+ },
1159
+ ]);
1160
+ expect(contract.describe(failures)).toBe(
1161
+ "[maintainability] 1 violation(s) in pkgs/akanjs:\n" +
1162
+ " pkgs/akanjs/akan.baseline.json:2 akan.error.bare-error records 3 bare Errors in all, above the 1 the code holds\n" +
1163
+ "How to fix:\n" +
1164
+ " baseline total: Run `akan verify <package>`, which lowers the section, and commit `akan.baseline.json` in this change: a recorded total above the code is room for a new failure anywhere. A bare `akan verify` covers only uncommitted files.",
1165
+ );
1166
+
1167
+ expect(await akanjsOf(root).enforce({ fix: true })).toEqual([]);
1168
+ expect(await baselineAt(root)).toEqual(new Map([["server/a.ts", 1]]));
1169
+ expect(await akanjsOf(root).enforce({ fix: false })).toEqual([]);
1170
+
1171
+ await git(root, "add", "-A");
1172
+ await git(root, "commit", "-q", "-m", "pr 1");
1173
+ await git(root, "checkout", "-q", "main");
1174
+ await git(root, "merge", "-q", "--ff-only", "pr-1");
1175
+ await git(root, "checkout", "-q", "-b", "pr-2");
1176
+ await writeText(path.join(root, "pkgs/akanjs/signal/new.ts"), failing(2, "g"));
1177
+
1178
+ expect(await akanjsOf(root).enforce({ fix: false })).toMatchObject([
1179
+ { rule: "akan.error.bare-error", key: "signal/new.ts", lines: [2, 5] },
1180
+ ]);
1181
+ });
1182
+
1183
+ test("refuses a stale total in a scope with no reference to compare with", async () => {
1184
+ const root = await makeWorkspace({
1185
+ "pkgs/akanjs/package.json": manifest,
1186
+ "pkgs/akanjs/server/a.ts": failing(1),
1187
+ "pkgs/akanjs/akan.baseline.json": '{\n "akan.error.bare-error": {\n "server/a.ts": 3\n }\n}\n',
1188
+ });
1189
+
1190
+ expect(await akanjsOf(root).enforce({ fix: false })).toMatchObject([{ kind: "staleTotal", count: 1, recorded: 3 }]);
1191
+ });
1192
+
1193
+ test("leaves the section untouched while over budget, so a move survives a run that also added one", async () => {
1194
+ const root = await makeWorkspace({
1195
+ "pkgs/akanjs/package.json": manifest,
1196
+ "pkgs/akanjs/server/a.ts": failing(3),
1197
+ "pkgs/akanjs/server/b.ts": failing(1),
1198
+ });
1199
+ await akanjsOf(root).seed();
1200
+ await commitAll(root);
1201
+ await git(root, "checkout", "-q", "-b", "feat");
1202
+ await writeText(path.join(root, "pkgs/akanjs/server/a.ts"), failing(2));
1203
+ await writeText(path.join(root, "pkgs/akanjs/server/b.ts"), failing(2));
1204
+ await writeText(path.join(root, "pkgs/akanjs/server/c.ts"), failing(1));
1205
+
1206
+ expect(await akanjsOf(root).enforce({ fix: true })).toMatchObject([
1207
+ { key: "server/b.ts", recorded: 1 },
1208
+ { key: "server/c.ts" },
1209
+ ]);
1210
+ expect(await baselineAt(root)).toEqual(
1211
+ new Map([
1212
+ ["server/a.ts", 3],
1213
+ ["server/b.ts", 1],
1214
+ ]),
1215
+ );
1216
+
1217
+ await rm(path.join(root, "pkgs/akanjs/server/c.ts"));
1218
+ expect(await akanjsOf(root).enforce({ fix: true })).toEqual([]);
1219
+ expect(await baselineAt(root)).toEqual(
1220
+ new Map([
1221
+ ["server/a.ts", 2],
1222
+ ["server/b.ts", 2],
1223
+ ]),
1224
+ );
1225
+ expect(await akanjsOf(root).enforce({ fix: false })).toEqual([]);
1226
+ });
1227
+
1228
+ test("adopts the rule in a baseline that predates it with the fork point's total, and no more", async () => {
1229
+ const root = await makeWorkspace({
1230
+ "pkgs/akanjs/package.json": manifest,
1231
+ "pkgs/akanjs/server/host.ts": failing(2),
1232
+ "pkgs/akanjs/akan.baseline.json": '{\n "akan.size.file": {\n "server/big.ts": 900\n }\n}\n',
1233
+ "pkgs/akanjs/server/big.ts": linesOf(900),
1234
+ });
1235
+ await commitAll(root);
1236
+
1237
+ await akanjsOf(root).seed({ force: true });
1238
+ expect(await baselineAt(root)).toEqual(new Map([["server/host.ts", 2]]));
1239
+ expect(await akanjsOf(root).enforce({ fix: false })).toEqual([]);
1240
+
1241
+ await writeText(path.join(root, "pkgs/akanjs/signal/fresh.ts"), failing(1));
1242
+ await akanjsOf(root).seed({ force: true });
1243
+ const contract = akanjsOf(root);
1244
+ const failures = await contract.enforce({ fix: false });
1245
+
1246
+ expect(failures).toMatchObject([
1247
+ {
1248
+ kind: "baselineGrowth",
1249
+ debtRule: "akan.error.bare-error",
1250
+ key: "(total)",
1251
+ count: 3,
1252
+ committed: 2,
1253
+ measured: true,
1254
+ },
1255
+ ]);
1256
+ expect(contract.describe(failures)).toMatch(
1257
+ /akan\.error\.bare-error allows 3 bare Errors in all, above the 2 the code at [0-9a-f]{12} holds with no entry for its rule recorded there/,
1258
+ );
1259
+ });
1260
+ });
1261
+
961
1262
  describe("MaintainabilityContract reference without a baseline", () => {
962
1263
  const packageOf = (root: string) => new MaintainabilityContract(root, { type: "pkg", name: "@demo/core" });
963
1264
  const functionOf = (name: string, lineNum: number) => `export function ${name}() {\n${linesOf(lineNum - 2)}\n}\n`;