@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.
Files changed (93) hide show
  1. package/agentAssets.test.ts +65 -0
  2. package/agentAssets.ts +166 -0
  3. package/agentsIndex.ts +4 -4
  4. package/akanContext.ts +44 -2
  5. package/applicationBuildRunner.test.ts +17 -0
  6. package/applicationBuildRunner.ts +3 -1
  7. package/biome.base.json +4 -1
  8. package/codeAgent/agent/akanSystemPrompt.ts +1 -1
  9. package/codeAgent/feedback/VerifyFeedback.ts +3 -4
  10. package/codeAgent/skills/akan-agent/SKILL.md +36 -0
  11. package/codeAgent/skills/akan-debug/SKILL.md +3 -3
  12. package/codeAgent/skills/akan-deps/SKILL.md +1 -2
  13. package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
  14. package/codeAgent/skills/akan-page/SKILL.md +44 -0
  15. package/codeAgent/skills/akan-query/SKILL.md +38 -0
  16. package/codeAgent/skills/akan-runtime/SKILL.md +40 -0
  17. package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
  18. package/codeAgent/skills/akan-transport/SKILL.md +40 -0
  19. package/codeAgent/skills/akan-validate/SKILL.md +13 -11
  20. package/codeAgent/tools/AkanCli.ts +2 -1
  21. package/codeAgent/tools/AkanEditScope.ts +8 -3
  22. package/codeAgent/tools/AkanToolPack.ts +4 -10
  23. package/codeAgent/tools/AkanVerifier.ts +13 -31
  24. package/codeAgent/tools/akanCodePlugins.test.ts +12 -30
  25. package/commentDensityScanner.test.ts +121 -0
  26. package/commentDensityScanner.ts +101 -0
  27. package/componentSizeScanner.test.ts +36 -0
  28. package/componentSizeScanner.ts +52 -0
  29. package/componentWriteChainScanner.test.ts +137 -0
  30. package/componentWriteChainScanner.ts +104 -0
  31. package/conventionSet.test.ts +73 -0
  32. package/conventionSet.ts +67 -0
  33. package/deprecatedCommentScanner.test.ts +54 -0
  34. package/deprecatedCommentScanner.ts +30 -0
  35. package/endpointReach.test.ts +58 -0
  36. package/endpointReach.ts +80 -0
  37. package/executors.test.ts +0 -4
  38. package/index.ts +1 -0
  39. package/kindBranchScanner.test.ts +89 -0
  40. package/kindBranchScanner.ts +158 -0
  41. package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
  42. package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
  43. package/lint/no-oversized-list-limit.grit +73 -0
  44. package/maintainabilityBaseline.test.ts +280 -0
  45. package/maintainabilityBaseline.ts +220 -0
  46. package/maintainabilityContract.test.ts +846 -0
  47. package/maintainabilityContract.ts +404 -0
  48. package/maintainabilityExceptionScanner.test.ts +90 -0
  49. package/maintainabilityExceptionScanner.ts +85 -0
  50. package/maintainabilityLimits.ts +60 -0
  51. package/maintainabilityReport.test.ts +130 -0
  52. package/maintainabilityReport.ts +153 -0
  53. package/package.json +2 -2
  54. package/prompter.ts +7 -0
  55. package/qualityScanner.test.ts +75 -0
  56. package/qualityScanner.ts +17 -1
  57. package/serviceLoopCallScanner.test.ts +56 -0
  58. package/serviceLoopCallScanner.ts +122 -0
  59. package/sourceComments.test.ts +45 -0
  60. package/sourceComments.ts +70 -0
  61. package/storeAccessScanner.test.ts +84 -0
  62. package/storeAccessScanner.ts +71 -0
  63. package/storeSource.ts +159 -0
  64. package/storeWriteScanner.test.ts +292 -0
  65. package/storeWriteScanner.ts +50 -0
  66. package/testOnlyExportScanner.test.ts +83 -0
  67. package/testOnlyExportScanner.ts +124 -0
  68. package/typeChecker.ts +32 -0
  69. package/typecheck/projectTypecheck.test.ts +18 -0
  70. package/typecheck/projectTypecheck.ts +29 -5
  71. package/typecheck/typecheck.proc.ts +6 -0
  72. package/typecheck/typecheckFailure.ts +13 -0
  73. package/workflow/artifacts.ts +16 -2
  74. package/workflow/executor.ts +22 -7
  75. package/workflow/guardCatalog.test.ts +59 -0
  76. package/workflow/guardCatalog.ts +108 -0
  77. package/workflow/index.ts +4 -0
  78. package/workflow/interactionScaffold.ts +122 -0
  79. package/workflow/mutationScaffold.ts +519 -0
  80. package/workflow/mutationSource.test.ts +108 -0
  81. package/workflow/mutationSource.ts +219 -0
  82. package/workflow/plan.ts +54 -0
  83. package/workflow/render.ts +19 -4
  84. package/workflow/source.test.ts +185 -0
  85. package/workflow/source.ts +151 -24
  86. package/workflow/types.ts +18 -1
  87. package/workspaceLayout.ts +2 -0
  88. package/writePath.test.ts +109 -0
  89. package/writePath.ts +219 -0
  90. package/writePlacementScanner.test.ts +77 -0
  91. package/writePlacementScanner.ts +50 -0
  92. package/writingActionIndex.test.ts +99 -0
  93. package/writingActionIndex.ts +63 -0
@@ -0,0 +1,846 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { existsSync } from "node:fs";
3
+ import { readFile } from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { MaintainabilityBaseline } from "./maintainabilityBaseline";
6
+ import { MaintainabilityContract } from "./maintainabilityContract";
7
+ import { MaintainabilityLimits } from "./maintainabilityLimits";
8
+ import { tempDirs, writeText } from "./testHelpers";
9
+
10
+ const makeTempRoot = tempDirs("akan-maintainability-");
11
+
12
+ const makeWorkspace = async (files: Record<string, string>) => {
13
+ const root = await makeTempRoot();
14
+ for (const [filePath, content] of Object.entries(files)) await writeText(path.join(root, filePath), content);
15
+ return root;
16
+ };
17
+
18
+ const linesOf = (lineNum: number) => Array.from({ length: lineNum }, (_, idx) => `// line ${idx}`).join("\n");
19
+ const componentOf = (name: string, lineNum: number) =>
20
+ `export const ${name} = () => {\n${linesOf(lineNum - 3)}\n return null;\n};\n`;
21
+
22
+ const signal = `export class CourtSlice extends slice(srv.court, { guards: { root: Admin, cru: User } }, () => ({})) {}
23
+ export class CourtEndpoint extends endpoint(srv.court, ({ mutation }) => ({
24
+ assignCourtPc: mutation(Boolean, { guards: [User] }).param("courtId", ID).exec(() => true),
25
+ })) {}
26
+ `;
27
+ const multiWriteStore = `import { fetch, sig } from "../useClient";
28
+ export class CourtStore extends store(sig.court, () => ({})) {
29
+ async assignCourtPcOnCourt(courtId: string) {
30
+ await fetch.assignCourtPc(courtId);
31
+ await fetch.pushPcCommand(courtId);
32
+ }
33
+ }
34
+ `;
35
+ const pushSignal = `export class PushEndpoint extends endpoint(srv.push, ({ mutation }) => ({
36
+ pushPcCommand: mutation(Boolean, { guards: [User] }).param("courtId", ID).exec(() => true),
37
+ })) {}
38
+ `;
39
+
40
+ const contractOf = (root: string, libNames: string[] = []) =>
41
+ new MaintainabilityContract(root, { type: "app", name: "demo" }, libNames);
42
+ const baselineOf = async (root: string) => await readFile(path.join(root, "apps/demo/akan.baseline.json"), "utf8");
43
+ const lineEntryOf = (text: string, key: string) =>
44
+ MaintainabilityBaseline.parse(text).sections.get("akan.size.file")?.get(key);
45
+ const writeBaseline = async (root: string, json: object) =>
46
+ await writeText(path.join(root, "apps/demo/akan.baseline.json"), `${JSON.stringify(json, null, 2)}\n`);
47
+
48
+ const git = async (root: string, ...args: string[]) => {
49
+ const proc = Bun.spawn(
50
+ ["git", "-c", "user.name=akan", "-c", "user.email=akan@example.com", "-c", "commit.gpgsign=false", ...args],
51
+ { cwd: root, stdout: "ignore", stderr: "pipe" },
52
+ );
53
+ if ((await proc.exited) !== 0) throw new Error(await new Response(proc.stderr).text());
54
+ };
55
+ const commitAll = async (root: string) => {
56
+ await git(root, "init", "-q", "-b", "main");
57
+ await git(root, "add", "-A");
58
+ await git(root, "commit", "-q", "-m", "baseline");
59
+ };
60
+
61
+ describe("MaintainabilityContract limits", () => {
62
+ test("holds each file kind to its line limit, the abstract to AbstractDoc's", async () => {
63
+ const atLimit = Object.fromEntries(
64
+ Object.entries(MaintainabilityLimits.fileMaxLines).map(([suffix, maxLines]) => [
65
+ `apps/demo/lib/court/at${suffix}`,
66
+ linesOf(maxLines),
67
+ ]),
68
+ );
69
+ const overLimit = Object.fromEntries(
70
+ Object.entries(MaintainabilityLimits.fileMaxLines).map(([suffix, maxLines]) => [
71
+ `apps/demo/lib/court/over${suffix}`,
72
+ linesOf(maxLines + 1),
73
+ ]),
74
+ );
75
+ const root = await makeWorkspace({ ...atLimit, ...overLimit, "apps/demo/lib/court/court.signal.ts": linesOf(900) });
76
+
77
+ const violations = await contractOf(root).measure();
78
+
79
+ expect(MaintainabilityLimits.fileMaxLines[".abstract.md"]).toBe(300);
80
+ expect(violations.map(({ key, line }) => ({ key, line }))).toEqual(
81
+ Object.keys(overLimit)
82
+ .map((file) => ({ key: file.slice("apps/demo/".length), line: 1 }))
83
+ .sort((a, b) => (a.key < b.key ? -1 : 1)),
84
+ );
85
+ expect(violations.find(({ key }) => key.endsWith(".service.ts"))).toMatchObject({ lineCount: 501, maxLines: 500 });
86
+ });
87
+
88
+ test("measures components and hooks in lib/, ui/ and webkit/ only, keyed file#Component", async () => {
89
+ const root = await makeWorkspace({
90
+ "apps/demo/ui/Board.tsx": `${componentOf("Board", 251)}${componentOf("Cell", 120)}`,
91
+ "apps/demo/lib/court/Court.Unit.tsx": `import { cn } from "akanjs/client";\n${componentOf("Card", 260)}`,
92
+ "apps/demo/webkit/useBoard.tsx": componentOf("useBoard", 255),
93
+ "apps/demo/page/court.tsx": componentOf("Page", 300),
94
+ "apps/demo/ui/Board.test.tsx": componentOf("Fixture", 300),
95
+ });
96
+
97
+ const violations = await contractOf(root).measure();
98
+
99
+ expect(violations).toEqual([
100
+ {
101
+ kind: "size",
102
+ rule: "akan.size.component",
103
+ key: "lib/court/Court.Unit.tsx#Card",
104
+ file: "lib/court/Court.Unit.tsx",
105
+ line: 2,
106
+ lineCount: 260,
107
+ maxLines: 250,
108
+ component: "Card",
109
+ },
110
+ {
111
+ kind: "size",
112
+ rule: "akan.size.component",
113
+ key: "ui/Board.tsx#Board",
114
+ file: "ui/Board.tsx",
115
+ line: 1,
116
+ lineCount: 251,
117
+ maxLines: 250,
118
+ component: "Board",
119
+ },
120
+ {
121
+ kind: "size",
122
+ rule: "akan.size.component",
123
+ key: "webkit/useBoard.tsx#useBoard",
124
+ file: "webkit/useBoard.tsx",
125
+ line: 1,
126
+ lineCount: 255,
127
+ maxLines: 250,
128
+ component: "useBoard",
129
+ },
130
+ ]);
131
+ });
132
+
133
+ test("holds every ui/ and webkit/ source to the folder cap, leaving out tests, specs and re-export barrels", async () => {
134
+ const cap = MaintainabilityLimits.folderMaxLines["ui/"];
135
+ const barrel = Array.from({ length: cap + 50 }, (_, idx) => `export * from "./part${idx}";`).join("\n");
136
+ const root = await makeWorkspace({
137
+ "apps/demo/ui/Big.tsx": linesOf(cap + 1),
138
+ "apps/demo/ui/Fits.tsx": linesOf(cap),
139
+ "apps/demo/ui/Grid/index.ts": Array.from({ length: cap + 1 }, (_, idx) => `export const v${idx} = ${idx};`).join(
140
+ "\n",
141
+ ),
142
+ "apps/demo/ui/index.ts": barrel,
143
+ "apps/demo/ui/Big.test.tsx": linesOf(cap + 100),
144
+ "apps/demo/webkit/useBoard.tsx": linesOf(cap + 2),
145
+ "apps/demo/webkit/boardLayout.ts": linesOf(cap + 3),
146
+ "apps/demo/webkit/boardLayout.spec.ts": linesOf(cap + 100),
147
+ "apps/demo/webkit/index.ts": `${barrel}\nexport {};`,
148
+ "apps/demo/common/table.ts": linesOf(cap + 100),
149
+ });
150
+
151
+ const violations = await contractOf(root).measure();
152
+
153
+ expect(MaintainabilityLimits.folderMaxLines["webkit/"]).toBe(cap);
154
+ expect(violations).toMatchObject([
155
+ { rule: "akan.size.file", key: "ui/Big.tsx", lineCount: cap + 1, maxLines: cap },
156
+ { rule: "akan.size.file", key: "ui/Grid/index.ts", lineCount: cap + 1, maxLines: cap },
157
+ { rule: "akan.size.file", key: "webkit/boardLayout.ts", lineCount: cap + 3, maxLines: cap },
158
+ { rule: "akan.size.file", key: "webkit/useBoard.tsx", lineCount: cap + 2, maxLines: cap },
159
+ ]);
160
+ expect(violations).toHaveLength(4);
161
+ });
162
+
163
+ test("reads a store's mutations from its own signals and the libs it reaches", async () => {
164
+ const root = await makeWorkspace({
165
+ "apps/demo/lib/court/court.signal.ts": signal,
166
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
167
+ "libs/shared/lib/push/push.signal.ts": pushSignal,
168
+ });
169
+
170
+ expect(await contractOf(root).measure()).toEqual([]);
171
+ expect(await contractOf(root, ["shared"]).measure()).toEqual([
172
+ {
173
+ kind: "multiWrite",
174
+ rule: "akan.store.multi-write",
175
+ key: "lib/court/court.store.ts#assignCourtPcOnCourt",
176
+ file: "lib/court/court.store.ts",
177
+ line: 5,
178
+ owner: "assignCourtPcOnCourt",
179
+ writes: ["fetch.assignCourtPc", "fetch.pushPcCommand"],
180
+ writeCount: 2,
181
+ },
182
+ ]);
183
+ });
184
+ });
185
+
186
+ describe("MaintainabilityContract ratchet", () => {
187
+ const zone = "apps/demo/lib/court/Court.Zone.tsx";
188
+
189
+ test("fails a breach the baseline does not record, and one that grew past its entry", async () => {
190
+ const root = await makeWorkspace({ [zone]: linesOf(450), "apps/demo/lib/court/Court.View.tsx": linesOf(500) });
191
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 440 } });
192
+
193
+ const failures = await contractOf(root).enforce({ fix: false });
194
+
195
+ expect(failures).toMatchObject([
196
+ { key: "lib/court/Court.View.tsx", lineCount: 500 },
197
+ { key: "lib/court/Court.Zone.tsx", lineCount: 450, recorded: 440 },
198
+ ]);
199
+ });
200
+
201
+ test("passes a breach at its entry and a listed multi-write, writing nothing", async () => {
202
+ const root = await makeWorkspace({
203
+ [zone]: linesOf(450),
204
+ "apps/demo/lib/court/court.signal.ts": `${signal}\n${pushSignal}`,
205
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
206
+ });
207
+ const baseline = {
208
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 450 },
209
+ "akan.store.multi-write": { "lib/court/court.store.ts#assignCourtPcOnCourt": 2 },
210
+ };
211
+ await writeBaseline(root, baseline);
212
+ const before = await baselineOf(root);
213
+
214
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
215
+ expect(await baselineOf(root)).toBe(before);
216
+ });
217
+
218
+ test("tightens a shrunk entry and drops a fixed or deleted one, only with fix", async () => {
219
+ const root = await makeWorkspace({ [zone]: linesOf(420), "apps/demo/lib/court/Court.View.tsx": linesOf(300) });
220
+ await writeBaseline(root, {
221
+ "akan.size.file": {
222
+ "lib/court/Court.Zone.tsx": 450,
223
+ "lib/court/Court.View.tsx": 480,
224
+ "lib/gone/Gone.Zone.tsx": 900,
225
+ },
226
+ "akan.store.multi-write": { "lib/court/court.store.ts#assignCourtPcOnCourt": 2 },
227
+ });
228
+ const before = await baselineOf(root);
229
+
230
+ expect(await contractOf(root).enforce({ fix: false })).toEqual([]);
231
+ expect(await baselineOf(root)).toBe(before);
232
+
233
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
234
+ expect(await baselineOf(root)).toBe('{\n "akan.size.file": {\n "lib/court/Court.Zone.tsx": 420\n }\n}\n');
235
+
236
+ await writeText(path.join(root, zone), linesOf(400));
237
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
238
+ expect(existsSync(path.join(root, "apps/demo/akan.baseline.json"))).toBe(false);
239
+ });
240
+
241
+ test("refuses a working baseline that adds or raises an entry over the committed one", async () => {
242
+ const root = await makeWorkspace({
243
+ [zone]: linesOf(450),
244
+ "apps/demo/lib/court/Court.View.tsx": linesOf(500),
245
+ "apps/demo/lib/court/court.signal.ts": `${signal}\n${pushSignal}`,
246
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
247
+ });
248
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 440 } });
249
+ await commitAll(root);
250
+ await writeBaseline(root, {
251
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 460, "lib/court/Court.View.tsx": 500 },
252
+ "akan.store.multi-write": { "lib/court/court.store.ts#assignCourtPcOnCourt": 2 },
253
+ });
254
+
255
+ const failures = await contractOf(root).enforce({ fix: false });
256
+
257
+ expect(failures).toMatchObject([
258
+ { kind: "baselineGrowth", key: "lib/court/Court.Zone.tsx", count: 460, committed: 440, line: 3 },
259
+ { kind: "baselineGrowth", key: "lib/court/Court.View.tsx", file: "akan.baseline.json", line: 4 },
260
+ {
261
+ kind: "baselineGrowth",
262
+ key: "lib/court/court.store.ts#assignCourtPcOnCourt",
263
+ debtRule: "akan.store.multi-write",
264
+ line: 7,
265
+ },
266
+ ]);
267
+ expect(failures).toHaveLength(3);
268
+ expect(contractOf(root).describe(failures)).toMatch(
269
+ /apps\/demo\/akan\.baseline\.json:3 {2}akan\.baseline\.growth {2}entry "lib\/court\/Court\.Zone\.tsx" under akan\.size\.file allows 460 lines, above the 440 at [0-9a-f]{12}; the baseline may only shrink/,
270
+ );
271
+ });
272
+
273
+ test("measures growth from the fork point with the default branch, so a raise the branch committed still fails", async () => {
274
+ const root = await makeWorkspace({ [zone]: linesOf(450) });
275
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 440 } });
276
+ await commitAll(root);
277
+ await git(root, "checkout", "-q", "-b", "feature");
278
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 460 } });
279
+ await git(root, "commit", "-q", "-am", "raise");
280
+
281
+ expect(await contractOf(root).enforce({ fix: false })).toMatchObject([
282
+ { kind: "baselineGrowth", key: "lib/court/Court.Zone.tsx", count: 460, committed: 440 },
283
+ ]);
284
+ });
285
+
286
+ test("refuses headroom a baseline left unfixed records above what the code measures", async () => {
287
+ const root = await makeWorkspace({ [zone]: linesOf(450), "apps/demo/lib/court/Court.View.tsx": linesOf(10) });
288
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 450 } });
289
+ await commitAll(root);
290
+ await writeBaseline(root, {
291
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 900, "lib/court/Court.View.tsx": 900 },
292
+ });
293
+
294
+ expect(await contractOf(root).enforce({ fix: false })).toMatchObject([
295
+ { kind: "baselineGrowth", key: "lib/court/Court.Zone.tsx", count: 900, committed: 450 },
296
+ { kind: "baselineGrowth", key: "lib/court/Court.View.tsx", count: 900, committed: undefined },
297
+ ]);
298
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
299
+ expect(await baselineOf(root)).toBe('{\n "akan.size.file": {\n "lib/court/Court.Zone.tsx": 450\n }\n}\n');
300
+ });
301
+
302
+ test("fails where the fork point cannot be found, instead of comparing the baseline with itself", async () => {
303
+ const origin = await makeWorkspace({ [zone]: linesOf(450) });
304
+ await writeBaseline(origin, { "akan.size.file": { "lib/court/Court.Zone.tsx": 440 } });
305
+ await commitAll(origin);
306
+ await git(origin, "checkout", "-q", "-b", "feature");
307
+ await writeBaseline(origin, { "akan.size.file": { "lib/court/Court.Zone.tsx": 460 } });
308
+ await git(origin, "commit", "-q", "-am", "raise");
309
+ const shallow = await makeTempRoot();
310
+ await git(shallow, "init", "-q");
311
+ await git(shallow, "fetch", "-q", "--depth=1", `file://${origin}`, "+refs/heads/feature:refs/remotes/pull/1/merge");
312
+ await git(shallow, "checkout", "-q", "--detach", "refs/remotes/pull/1/merge");
313
+
314
+ await expect(contractOf(shallow).enforce({ fix: false })).rejects.toThrow(
315
+ "apps/demo/akan.baseline.json is compared with its copy where this branch forked from the default branch, but this is a shallow clone",
316
+ );
317
+
318
+ await git(origin, "checkout", "-q", "--orphan", "unrelated");
319
+ await git(origin, "commit", "-q", "-m", "unrelated");
320
+ await expect(contractOf(origin).enforce({ fix: false })).rejects.toThrow("HEAD shares no commit with main");
321
+ });
322
+
323
+ test("reads a reference baseline holding a section this version does not know", async () => {
324
+ const root = await makeWorkspace({ [zone]: linesOf(450) });
325
+ await writeBaseline(root, {
326
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 450 },
327
+ "akan.size.retired": { "lib/court/Court.Zone.tsx": 2 },
328
+ });
329
+ await commitAll(root);
330
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 450 } });
331
+
332
+ expect(await contractOf(root).enforce({ fix: false })).toEqual([]);
333
+ });
334
+
335
+ test("passes a shape entry added or raised with a reason, and refuses a chained write whatever its reason", async () => {
336
+ const root = await makeWorkspace({
337
+ [zone]: linesOf(450),
338
+ "apps/demo/lib/court/Court.View.tsx": linesOf(500),
339
+ "apps/demo/ui/Board.tsx": linesOf(420),
340
+ "apps/demo/lib/court/court.signal.ts": `${signal}\n${pushSignal}`,
341
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
342
+ });
343
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 440 } });
344
+ await commitAll(root);
345
+ await writeBaseline(root, {
346
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 450, "lib/court/Court.View.tsx": 500, "ui/Board.tsx": 420 },
347
+ "akan.store.multi-write": { "lib/court/court.store.ts#assignCourtPcOnCourt": 2 },
348
+ reasons: {
349
+ "lib/court/Court.Zone.tsx": "The court canvas is one pointer state machine.",
350
+ "ui/Board.tsx": "Generated SVG path table for the court layout.",
351
+ "lib/court/Court.View.tsx": "a b c d e f g h i",
352
+ "lib/court/court.store.ts#assignCourtPcOnCourt": "Assigning and pushing must stay two calls for now.",
353
+ },
354
+ });
355
+ const contract = contractOf(root);
356
+
357
+ const failures = await contract.enforce({ fix: false });
358
+
359
+ expect(failures).toMatchObject([
360
+ { kind: "baselineGrowth", key: "lib/court/Court.View.tsx", count: 500, committed: undefined },
361
+ { kind: "baselineGrowth", key: "lib/court/court.store.ts#assignCourtPcOnCourt" },
362
+ ]);
363
+ expect(failures).toHaveLength(2);
364
+ expect(contract.describe(failures)).not.toContain("reason");
365
+ });
366
+
367
+ test("lowers a reasoned entry as its file shrinks, and drops it with its reason once it passes", async () => {
368
+ const root = await makeWorkspace({ [zone]: linesOf(420), "apps/demo/lib/court/Court.View.tsx": linesOf(300) });
369
+ await writeBaseline(root, {
370
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 450, "lib/court/Court.View.tsx": 480 },
371
+ reasons: {
372
+ "lib/court/Court.Zone.tsx": "The court canvas is one pointer state machine.",
373
+ "lib/court/Court.View.tsx": "The schedule grid renders every slot inline.",
374
+ "lib/gone/Gone.Zone.tsx": "A file deleted long ago kept this reason.",
375
+ },
376
+ });
377
+
378
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
379
+ expect(await baselineOf(root)).toBe(`{
380
+ "akan.size.file": {
381
+ "lib/court/Court.Zone.tsx": 420
382
+ },
383
+ "reasons": {
384
+ "lib/court/Court.Zone.tsx": "The court canvas is one pointer state machine."
385
+ }
386
+ }
387
+ `);
388
+
389
+ await writeText(path.join(root, zone), linesOf(400));
390
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
391
+ expect(existsSync(path.join(root, "apps/demo/akan.baseline.json"))).toBe(false);
392
+ });
393
+
394
+ test("lets a committed baseline shrink", async () => {
395
+ const root = await makeWorkspace({ [zone]: linesOf(450) });
396
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 460 } });
397
+ await commitAll(root);
398
+ await writeText(path.join(root, zone), linesOf(430));
399
+
400
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
401
+ expect(lineEntryOf(await baselineOf(root), "lib/court/Court.Zone.tsx")).toBe(430);
402
+ });
403
+
404
+ test("names each breach with its place, size and limit, and how to fix it", async () => {
405
+ const root = await makeWorkspace({
406
+ [zone]: `${componentOf("Canvas", 300)}${linesOf(150)}`,
407
+ "apps/demo/lib/court/court.signal.ts": `${signal}\n${pushSignal}`,
408
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
409
+ });
410
+ const contract = contractOf(root);
411
+
412
+ const message = contract.describe(await contract.enforce({ fix: false }));
413
+
414
+ expect(message).toContain("[maintainability] 3 violation(s) in apps/demo:");
415
+ expect(message).toContain(
416
+ "apps/demo/lib/court/Court.Zone.tsx:1 akan.size.file file has 450 lines, over the 400-line limit\n" +
417
+ " apps/demo/lib/court/Court.Zone.tsx:1 akan.size.component component Canvas has 300 lines, over the 250-line limit\n" +
418
+ " apps/demo/lib/court/court.store.ts:5 akan.store.multi-write store action assignCourtPcOnCourt sends 2 writes from the browser: fetch.assignCourtPc, fetch.pushPcCommand",
419
+ );
420
+ expect(message).toContain("*.Zone.tsx, component or hook: Split it, in this order: a mode/kind table");
421
+ expect(message).toContain("this.transaction(fn)");
422
+ expect(message).not.toContain("baseline");
423
+ expect(message).not.toContain("Path:");
424
+ });
425
+
426
+ test("prints a rule's command under its fix once, and nothing for a rule the map leaves out", async () => {
427
+ const root = await makeWorkspace({
428
+ [zone]: linesOf(450),
429
+ "apps/demo/lib/court/Court.Util.tsx": linesOf(410),
430
+ "apps/demo/lib/court/court.signal.ts": `${signal}\n${pushSignal}`,
431
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
432
+ });
433
+ const contract = contractOf(root);
434
+ const failures = await contract.enforce({ fix: false });
435
+ const plan = "akan workflow plan add-mutation --app demo --module <module> --mutation <mutation> --guard <guard>";
436
+
437
+ const message = contract.describe(failures, {
438
+ "akan.store.multi-write": [plan],
439
+ "akan.size.file": ["akan split <file>"],
440
+ });
441
+
442
+ expect(message).toContain(
443
+ "never model-bound code moved into `ui/`.\n Path: akan split <file>\n store action with several writes:",
444
+ );
445
+ expect(message).toContain(" *.Util.tsx, *.Zone.tsx: Split it, in this order:");
446
+ expect(message).toContain(`this.transaction(fn)\`, with a side effect`);
447
+ expect(message.split("\n").filter((line) => line === ` Path: ${plan}`)).toHaveLength(1);
448
+ expect(message.split("\n").filter((line) => line.startsWith(" Path:"))).toHaveLength(2);
449
+ });
450
+ });
451
+
452
+ describe("MaintainabilityContract seed", () => {
453
+ const zone = "apps/demo/lib/court/Court.Zone.tsx";
454
+
455
+ test("records every breach where no baseline exists, and keeps an existing one unless forced", async () => {
456
+ const root = await makeWorkspace({ [zone]: linesOf(450) });
457
+
458
+ expect((await contractOf(root).seed()).result).toBe("written");
459
+ expect(await baselineOf(root)).toBe('{\n "akan.size.file": {\n "lib/court/Court.Zone.tsx": 450\n }\n}\n');
460
+
461
+ await writeText(path.join(root, zone), linesOf(470));
462
+ expect((await contractOf(root).seed()).result).toBe("kept");
463
+ expect(lineEntryOf(await baselineOf(root), "lib/court/Court.Zone.tsx")).toBe(450);
464
+ expect((await contractOf(root).seed({ force: true })).result).toBe("written");
465
+ expect(lineEntryOf(await baselineOf(root), "lib/court/Court.Zone.tsx")).toBe(470);
466
+
467
+ await writeText(path.join(root, zone), linesOf(10));
468
+ expect((await contractOf(root).seed({ force: true })).result).toBe("removed");
469
+ expect(existsSync(path.join(root, "apps/demo/akan.baseline.json"))).toBe(false);
470
+ expect((await contractOf(root).seed()).result).toBe("clean");
471
+ });
472
+
473
+ test("keeps the reasons of the entries a forced seed records again", async () => {
474
+ const root = await makeWorkspace({ [zone]: linesOf(470) });
475
+ await writeBaseline(root, {
476
+ "akan.size.file": { "lib/court/Court.Zone.tsx": 450, "lib/gone/Gone.Zone.tsx": 900 },
477
+ reasons: {
478
+ "lib/court/Court.Zone.tsx": "The court canvas is one pointer state machine.",
479
+ "lib/gone/Gone.Zone.tsx": "A file deleted long ago kept this reason.",
480
+ },
481
+ });
482
+
483
+ expect((await contractOf(root).seed({ force: true })).result).toBe("written");
484
+ expect(await baselineOf(root)).toBe(`{
485
+ "akan.size.file": {
486
+ "lib/court/Court.Zone.tsx": 470
487
+ },
488
+ "reasons": {
489
+ "lib/court/Court.Zone.tsx": "The court canvas is one pointer state machine."
490
+ }
491
+ }
492
+ `);
493
+ });
494
+ });
495
+
496
+ describe("MaintainabilityContract rules", () => {
497
+ test("lists every rule a failure can carry", async () => {
498
+ expect(MaintainabilityContract.ruleIds).toEqual([
499
+ "akan.size.file",
500
+ "akan.size.component",
501
+ "akan.store.multi-write",
502
+ "akan.store.root-write-chain",
503
+ "akan.component.write-chain",
504
+ "akan.component.store-access",
505
+ "akan.write.placement",
506
+ "akan.service.call-in-loop",
507
+ "akan.baseline.growth",
508
+ ]);
509
+ const root = await makeWorkspace({
510
+ "apps/demo/lib/court/Court.Zone.tsx": `${componentOf("Canvas", 260)}${linesOf(150)}`,
511
+ "apps/demo/lib/court/court.signal.ts": `${signal}\n${pushSignal}`,
512
+ "apps/demo/lib/court/court.store.ts": multiWriteStore,
513
+ });
514
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 405 } });
515
+ await commitAll(root);
516
+ await writeBaseline(root, { "akan.size.file": { "lib/court/Court.Zone.tsx": 600 } });
517
+
518
+ const failures = await contractOf(root).enforce({ fix: false });
519
+
520
+ expect(failures.map(({ rule }) => rule)).toEqual([
521
+ "akan.baseline.growth",
522
+ "akan.size.component",
523
+ "akan.store.multi-write",
524
+ ]);
525
+ for (const { rule } of failures) expect(MaintainabilityContract.ruleIds).toContain(rule);
526
+ });
527
+ });
528
+
529
+ describe("MaintainabilityContract write chains", () => {
530
+ const courtStore = `import { fetch, sig } from "../useClient";
531
+ export class CourtStore extends store(sig.court, () => ({})) {
532
+ async assignCourtPcOnCourt(courtId: string) {
533
+ await fetch.assignCourtPc(courtId);
534
+ }
535
+ async assignAndPush(courtId: string) {
536
+ const root = this as unknown as RootStore;
537
+ await this.assignCourtPcOnCourt(courtId);
538
+ await root.pushPcCommandOnCourt(courtId);
539
+ }
540
+ }
541
+ `;
542
+ const pushStore = `import { fetch, sig } from "../useClient";
543
+ export class PushStore extends store(sig.push, () => ({})) {
544
+ async pushPcCommandOnCourt(courtId: string) {
545
+ await fetch.pushPcCommand(courtId);
546
+ }
547
+ }
548
+ `;
549
+ const install = `"use client";
550
+ import { st } from "@apps/demo/client";
551
+
552
+ export const Install = ({ courtId }: { courtId: string }) => {
553
+ const onAssign = async () => {
554
+ await st.do.assignCourtPcOnCourt(courtId);
555
+ await st.do.pushPcCommandOnCourt(courtId);
556
+ };
557
+ return <button type="button" onClick={() => void onAssign()} />;
558
+ };
559
+ `;
560
+
561
+ test("reads a write through another store, the libs' stores included, in a store and in a component handler", async () => {
562
+ const root = await makeWorkspace({
563
+ "apps/demo/lib/court/court.signal.ts": signal,
564
+ "apps/demo/lib/court/court.store.ts": courtStore,
565
+ "apps/demo/lib/court/Court.Zone.tsx": install,
566
+ "libs/shared/lib/push/push.signal.ts": pushSignal,
567
+ "libs/shared/lib/push/push.store.ts": pushStore,
568
+ });
569
+
570
+ expect(await contractOf(root).measure()).toEqual([]);
571
+ expect(await contractOf(root, ["shared"]).measure()).toEqual([
572
+ {
573
+ kind: "multiWrite",
574
+ rule: "akan.component.write-chain",
575
+ key: "lib/court/Court.Zone.tsx#Install",
576
+ file: "lib/court/Court.Zone.tsx",
577
+ line: 7,
578
+ owner: "Install",
579
+ writes: ["st.do.assignCourtPcOnCourt", "st.do.pushPcCommandOnCourt"],
580
+ writeCount: 2,
581
+ },
582
+ {
583
+ kind: "multiWrite",
584
+ rule: "akan.store.root-write-chain",
585
+ key: "lib/court/court.store.ts#assignAndPush",
586
+ file: "lib/court/court.store.ts",
587
+ line: 9,
588
+ owner: "assignAndPush",
589
+ writes: ["this.assignCourtPcOnCourt", "root.pushPcCommandOnCourt"],
590
+ writeCount: 2,
591
+ },
592
+ ]);
593
+ });
594
+
595
+ test("passes a chain the baseline lists, and drops the entry once the chain is gone", async () => {
596
+ const root = await makeWorkspace({
597
+ "apps/demo/lib/court/court.signal.ts": signal,
598
+ "apps/demo/lib/court/court.store.ts": courtStore,
599
+ "apps/demo/lib/court/Court.Zone.tsx": install,
600
+ "libs/shared/lib/push/push.signal.ts": pushSignal,
601
+ "libs/shared/lib/push/push.store.ts": pushStore,
602
+ });
603
+ await writeBaseline(root, {
604
+ "akan.store.root-write-chain": { "lib/court/court.store.ts#assignAndPush": 2 },
605
+ "akan.component.write-chain": { "lib/court/Court.Zone.tsx#Install": 2 },
606
+ });
607
+
608
+ expect(await contractOf(root, ["shared"]).enforce({ fix: true })).toEqual([]);
609
+
610
+ await writeText(
611
+ path.join(root, "apps/demo/lib/court/Court.Zone.tsx"),
612
+ install.replace(/\n.*pushPcCommandOnCourt.*\n/, "\n"),
613
+ );
614
+ expect(await contractOf(root, ["shared"]).enforce({ fix: true })).toEqual([]);
615
+ expect([...MaintainabilityBaseline.parse(await baselineOf(root)).entriesByRule()]).toEqual([
616
+ ["akan.store.root-write-chain", [{ key: "lib/court/court.store.ts#assignAndPush", count: 2, reason: undefined }]],
617
+ ]);
618
+ });
619
+ });
620
+
621
+ describe("MaintainabilityContract store access", () => {
622
+ const zone = "apps/demo/lib/court/Court.Zone.tsx";
623
+ const busyComponent = (accessNum: number) => `"use client";
624
+ import { st } from "@apps/demo/client";
625
+
626
+ export const Canvas = () => {
627
+ ${Array.from({ length: accessNum }, (_, idx) => ` const v${idx} = st.use.court${idx}();`).join("\n")}
628
+ return null;
629
+ };
630
+ `;
631
+
632
+ test("fails a component over the cap, passes it at its entry, and lowers the entry as it shrinks", async () => {
633
+ const cap = MaintainabilityLimits.componentMaxStoreAccesses;
634
+ const root = await makeWorkspace({ [zone]: busyComponent(cap) });
635
+ expect(await contractOf(root).measure()).toEqual([]);
636
+
637
+ await writeText(path.join(root, zone), busyComponent(cap + 4));
638
+ const contract = contractOf(root);
639
+ const failures = await contract.enforce({ fix: false });
640
+ expect(failures).toEqual([
641
+ {
642
+ kind: "storeAccess",
643
+ rule: "akan.component.store-access",
644
+ key: "lib/court/Court.Zone.tsx#Canvas",
645
+ file: "lib/court/Court.Zone.tsx",
646
+ line: 4,
647
+ component: "Canvas",
648
+ accessCount: cap + 4,
649
+ maxAccesses: cap,
650
+ },
651
+ ]);
652
+ expect(contract.describe(failures)).toContain(
653
+ `apps/demo/lib/court/Court.Zone.tsx:4 akan.component.store-access component Canvas reaches the store ${cap + 4} times, over the ${cap}-access limit`,
654
+ );
655
+ expect(contract.describe(failures)).toContain("webkit/use<X>.tsx");
656
+
657
+ await writeBaseline(root, { "akan.component.store-access": { "lib/court/Court.Zone.tsx#Canvas": cap + 2 } });
658
+ expect(await contractOf(root).enforce({ fix: false })).toMatchObject([{ accessCount: cap + 4, recorded: cap + 2 }]);
659
+
660
+ await writeText(path.join(root, zone), busyComponent(cap + 1));
661
+ expect(await contractOf(root).enforce({ fix: true })).toEqual([]);
662
+ expect(
663
+ MaintainabilityBaseline.parse(await baselineOf(root))
664
+ .sections.get("akan.component.store-access")
665
+ ?.get("lib/court/Court.Zone.tsx#Canvas"),
666
+ ).toBe(cap + 1);
667
+ });
668
+
669
+ test("refuses a working entry raised over the committed one", async () => {
670
+ const cap = MaintainabilityLimits.componentMaxStoreAccesses;
671
+ const root = await makeWorkspace({ [zone]: busyComponent(cap + 3) });
672
+ await writeBaseline(root, { "akan.component.store-access": { "lib/court/Court.Zone.tsx#Canvas": cap + 1 } });
673
+ await commitAll(root);
674
+ await writeBaseline(root, { "akan.component.store-access": { "lib/court/Court.Zone.tsx#Canvas": cap + 9 } });
675
+ const contract = contractOf(root);
676
+
677
+ const failures = await contract.enforce({ fix: false });
678
+
679
+ expect(failures).toMatchObject([
680
+ { rule: "akan.baseline.growth", key: "lib/court/Court.Zone.tsx#Canvas", count: cap + 9, line: 3 },
681
+ ]);
682
+ expect(contract.describe(failures)).toContain(
683
+ `entry "lib/court/Court.Zone.tsx#Canvas" under akan.component.store-access allows ${cap + 9} store accesses, above the ${cap + 1} at`,
684
+ );
685
+ });
686
+ });
687
+
688
+ describe("MaintainabilityContract write placement", () => {
689
+ const courtStore = `import { fetch, sig } from "../useClient";
690
+ export class CourtStore extends store(sig.court, () => ({})) {
691
+ async assignCourtPcOnCourt(courtId: string) {
692
+ await fetch.assignCourtPc(courtId);
693
+ }
694
+ setPicked(courtId: string) {
695
+ this.set({ picked: courtId });
696
+ }
697
+ }
698
+ `;
699
+
700
+ test("fails a write sent from ui/ or webkit/, or a fetch mutation outside a store, and passes a module component's st.do write", async () => {
701
+ const root = await makeWorkspace({
702
+ "apps/demo/lib/court/court.signal.ts": signal,
703
+ "apps/demo/lib/court/court.store.ts": courtStore,
704
+ "apps/demo/lib/court/Court.Zone.tsx": `"use client";
705
+ import { fetch, st } from "@apps/demo/client";
706
+
707
+ export const Assign = ({ courtId }: { courtId: string }) => (
708
+ <>
709
+ <button type="button" onClick={() => void st.do.assignCourtPcOnCourt(courtId)} />
710
+ <button type="button" onClick={() => void fetch.assignCourtPc(courtId)} />
711
+ </>
712
+ );
713
+ `,
714
+ "apps/demo/ui/CourtPicker.tsx": `"use client";
715
+ import { st } from "@apps/demo/client";
716
+
717
+ export const CourtPicker = ({ courtId }: { courtId: string }) => (
718
+ <button type="button" onClick={st.do.assignCourtPcOnCourt} onFocus={() => st.do.setPicked(courtId)} />
719
+ );
720
+ `,
721
+ "apps/demo/webkit/useAssign.tsx": `"use client";
722
+ import { fetch } from "@apps/demo/client";
723
+
724
+ export const useAssign = () => ({ assign: async (courtId: string) => await fetch.assignCourtPc(courtId) });
725
+ `,
726
+ "apps/demo/ui/Picker.tsx": `export const Picker = ({ onPick }: { onPick: () => void }) => (
727
+ <button type="button" onClick={onPick} />
728
+ );
729
+ `,
730
+ });
731
+
732
+ const violations = (await contractOf(root).measure()).filter(({ rule }) => rule === "akan.write.placement");
733
+
734
+ expect(violations).toEqual([
735
+ {
736
+ kind: "writePlacement",
737
+ rule: "akan.write.placement",
738
+ key: "lib/court/Court.Zone.tsx",
739
+ file: "lib/court/Court.Zone.tsx",
740
+ line: 7,
741
+ writes: ["fetch.assignCourtPc"],
742
+ },
743
+ {
744
+ kind: "writePlacement",
745
+ rule: "akan.write.placement",
746
+ key: "ui/CourtPicker.tsx",
747
+ file: "ui/CourtPicker.tsx",
748
+ line: 5,
749
+ writes: ["st.do.assignCourtPcOnCourt"],
750
+ },
751
+ {
752
+ kind: "writePlacement",
753
+ rule: "akan.write.placement",
754
+ key: "webkit/useAssign.tsx",
755
+ file: "webkit/useAssign.tsx",
756
+ line: 4,
757
+ writes: ["fetch.assignCourtPc"],
758
+ },
759
+ ]);
760
+ });
761
+
762
+ test("counts a hook's store reads against the same cap as a component's", async () => {
763
+ const cap = MaintainabilityLimits.componentMaxStoreAccesses;
764
+ const root = await makeWorkspace({
765
+ "apps/demo/webkit/useCourtState.tsx": `"use client";
766
+ import { st } from "@apps/demo/client";
767
+
768
+ export const useCourtState = () => {
769
+ ${Array.from({ length: cap + 1 }, (_, idx) => ` const v${idx} = st.use.court${idx}();`).join("\n")}
770
+ return { v0 };
771
+ };
772
+ `,
773
+ });
774
+
775
+ expect(await contractOf(root).measure()).toMatchObject([
776
+ { rule: "akan.component.store-access", key: "webkit/useCourtState.tsx#useCourtState", accessCount: cap + 1 },
777
+ ]);
778
+ });
779
+ });
780
+
781
+ describe("MaintainabilityContract service calls per item", () => {
782
+ const courtSignal = `export class CourtInternal extends internal(srv.court, ({ cron }) => ({
783
+ sweep: cron("0 * * * *").exec(function () {
784
+ return this.courtService.sweepAll();
785
+ }),
786
+ })) {}
787
+ export class CourtSlice extends slice(srv.court, { guards: { root: Admin, cru: User } }, () => ({})) {}
788
+ export class CourtEndpoint extends endpoint(srv.court, ({ mutation }) => ({
789
+ assignPcs: mutation(Boolean, { guards: [User] }).body("courtIds", [ID]).exec(function (courtIds) {
790
+ return this.courtService.assignPcs(courtIds);
791
+ }),
792
+ })) {}
793
+ `;
794
+ const courtService = `export class CourtService extends serve(db.court, ({ use }) => ({})) {
795
+ async assignPcs(courtIds: string[]) {
796
+ for (const courtId of courtIds) await this.#assign(courtId);
797
+ const courts = await this.loadCourtMany(courtIds);
798
+ return courts.length > 0;
799
+ }
800
+ async #assign(courtId: string) {
801
+ const pc = await this.pcService.pickFree();
802
+ await this.courtModel.assignPc(courtId, pc.id);
803
+ }
804
+ async retry(courtId: string) {
805
+ for (let attempt = 0; attempt < 3; attempt += 1) await this.courtModel.touch(courtId);
806
+ }
807
+ async sweepAll() {
808
+ const courts = await this.listAll();
809
+ await Promise.all(courts.map((court) => this.pcService.ping(court.pcId)));
810
+ }
811
+ }
812
+ `;
813
+
814
+ test("fails a service method an endpoint reaches that calls a service or the database per item, sparing jobs and counted loops", async () => {
815
+ const root = await makeWorkspace({
816
+ "apps/demo/lib/court/court.signal.ts": courtSignal,
817
+ "apps/demo/lib/court/court.service.ts": courtService,
818
+ });
819
+
820
+ expect(await contractOf(root).measure()).toEqual([
821
+ {
822
+ kind: "loopCall",
823
+ rule: "akan.service.call-in-loop",
824
+ key: "lib/court/court.service.ts#assignPcs",
825
+ file: "lib/court/court.service.ts",
826
+ line: 3,
827
+ owner: "assignPcs",
828
+ calls: ["this.#assign"],
829
+ },
830
+ ]);
831
+ });
832
+
833
+ test("lets a reasoned entry stand for a per-item call a person decided on", async () => {
834
+ const root = await makeWorkspace({
835
+ "apps/demo/lib/court/court.signal.ts": courtSignal,
836
+ "apps/demo/lib/court/court.service.ts": courtService,
837
+ });
838
+ await commitAll(root);
839
+ await writeBaseline(root, {
840
+ "akan.service.call-in-loop": { "lib/court/court.service.ts#assignPcs": 1 },
841
+ reasons: { "lib/court/court.service.ts#assignPcs": "Each court takes a lock of its own; at most 4 per call." },
842
+ });
843
+
844
+ expect(await contractOf(root).enforce({ fix: false })).toEqual([]);
845
+ });
846
+ });