@akanjs/devkit 3.1.1-rc.9 → 3.1.2-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 (188) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/agentAssets.test.ts +65 -0
  3. package/agentAssets.ts +166 -0
  4. package/agentsIndex.ts +4 -4
  5. package/akanApp/akanApp.host.test.ts +12 -0
  6. package/akanApp/akanApp.host.ts +12 -0
  7. package/akanConfig/akanConfig.test.ts +105 -0
  8. package/akanConfig/akanConfig.ts +21 -1
  9. package/akanConfig/productionLockfile.test.ts +31 -0
  10. package/akanConfig/productionLockfile.ts +2 -0
  11. package/akanContext.ts +129 -4
  12. package/akanMcpContract.test.ts +28 -1
  13. package/akanMcpContract.ts +51 -3
  14. package/applicationBuildRunner.test.ts +53 -0
  15. package/applicationBuildRunner.ts +7 -4
  16. package/artifact/implicitRootLayout.ts +4 -2
  17. package/biome.base.json +4 -1
  18. package/cloud/globalConfig.test.ts +34 -0
  19. package/cloud/globalConfig.ts +2 -2
  20. package/codeAgent/agent/AkanCodeEnv.ts +26 -0
  21. package/codeAgent/agent/AkanCodeServices.ts +5 -2
  22. package/codeAgent/agent/CodeAgent.ts +11 -0
  23. package/codeAgent/agent/CodeAgentGate.ts +156 -10
  24. package/codeAgent/agent/CodeAgentProxy.ts +5 -4
  25. package/codeAgent/agent/CodeAgentRpcHost.ts +3 -0
  26. package/codeAgent/agent/CodeAgentRpcListener.ts +102 -16
  27. package/codeAgent/agent/SubagentPool.ts +5 -0
  28. package/codeAgent/agent/WorkspaceTrust.ts +200 -0
  29. package/codeAgent/agent/akanCodePaths.ts +8 -1
  30. package/codeAgent/agent/akanSystemPrompt.ts +5 -1
  31. package/codeAgent/agent/codeAgent.test.ts +41 -2
  32. package/codeAgent/agent/codeAgentContract.test.ts +119 -0
  33. package/codeAgent/agent/codeAgentHosts.ts +30 -0
  34. package/codeAgent/agent/codeAgentProxy.test.ts +53 -1
  35. package/codeAgent/agent/codeAgentRpcListener.test.ts +115 -13
  36. package/codeAgent/agent/stringArg.ts +1 -1
  37. package/codeAgent/feedback/PreviewFeedback.ts +2 -2
  38. package/codeAgent/feedback/VerifyFeedback.ts +3 -4
  39. package/codeAgent/index.ts +2 -2
  40. package/codeAgent/skills/akan-agent/SKILL.md +43 -0
  41. package/codeAgent/skills/akan-code/SKILL.md +38 -11
  42. package/codeAgent/skills/akan-debug/SKILL.md +41 -6
  43. package/codeAgent/skills/akan-deps/SKILL.md +1 -2
  44. package/codeAgent/skills/akan-err/SKILL.md +2 -1
  45. package/codeAgent/skills/akan-mcp/SKILL.md +40 -0
  46. package/codeAgent/skills/akan-page/SKILL.md +44 -0
  47. package/codeAgent/skills/akan-query/SKILL.md +39 -0
  48. package/codeAgent/skills/akan-runtime/SKILL.md +43 -0
  49. package/codeAgent/skills/akan-scaffold/SKILL.md +16 -6
  50. package/codeAgent/skills/akan-transport/SKILL.md +40 -0
  51. package/codeAgent/skills/akan-validate/SKILL.md +13 -11
  52. package/codeAgent/tools/AkanCli.ts +2 -1
  53. package/codeAgent/tools/AkanCodePlugins.ts +5 -1
  54. package/codeAgent/tools/AkanEditScope.ts +8 -3
  55. package/codeAgent/tools/AkanToolPack.ts +4 -10
  56. package/codeAgent/tools/AkanVerifier.ts +13 -31
  57. package/codeAgent/tools/McpServerConfig.ts +6 -4
  58. package/codeAgent/tools/McpSignIn.ts +7 -7
  59. package/codeAgent/tools/McpTokenStore.ts +63 -16
  60. package/codeAgent/tools/McpToolPack.ts +8 -1
  61. package/codeAgent/tools/WebToolPack.ts +50 -6
  62. package/codeAgent/tools/akanCodePlugins.test.ts +130 -60
  63. package/codeAgent/tools/mcpOAuth.test.ts +50 -14
  64. package/codeAgent/tools/mcpServerConfig.test.ts +130 -1
  65. package/commandDecorators/command.ts +4 -1
  66. package/commandDecorators/workspaceVersionCheck.test.ts +17 -7
  67. package/commandDecorators/workspaceVersionCheck.ts +4 -1
  68. package/commentDensityScanner.test.ts +121 -0
  69. package/commentDensityScanner.ts +101 -0
  70. package/componentSizeScanner.test.ts +36 -0
  71. package/componentSizeScanner.ts +52 -0
  72. package/componentWriteChainScanner.test.ts +137 -0
  73. package/componentWriteChainScanner.ts +104 -0
  74. package/conventionSet.test.ts +73 -0
  75. package/conventionSet.ts +67 -0
  76. package/createTunnel.test.ts +200 -2
  77. package/createTunnel.ts +80 -6
  78. package/csrE2e/ssrRegistry.e2e.test.ts +6 -1
  79. package/deprecatedCommentScanner.test.ts +54 -0
  80. package/deprecatedCommentScanner.ts +30 -0
  81. package/endpointReach.test.ts +58 -0
  82. package/endpointReach.ts +80 -0
  83. package/envSeed.test.ts +90 -0
  84. package/envSeed.ts +68 -0
  85. package/executors.test.ts +19 -5
  86. package/executors.ts +59 -4
  87. package/fileSys.test.ts +30 -2
  88. package/fileSys.ts +14 -0
  89. package/formSetterScanner.test.ts +44 -1
  90. package/formSetterScanner.ts +53 -8
  91. package/frontendBuild/csrArtifactBuilder.ts +7 -0
  92. package/frontendBuild/mcpAppViewBuilder.test.ts +63 -0
  93. package/frontendBuild/mcpAppViewBuilder.ts +7 -1
  94. package/frontendBuild/optionalPeers.test.ts +132 -0
  95. package/index.ts +1 -0
  96. package/integration/devStability.integration.test.ts +9 -5
  97. package/integration/ssrMemoryProbe.ts +6 -0
  98. package/kindBranchScanner.test.ts +89 -0
  99. package/kindBranchScanner.ts +158 -0
  100. package/lint/__fixtures__/no-oversized-list-limit/bad.tsx +18 -0
  101. package/lint/__fixtures__/no-oversized-list-limit/good.tsx +23 -0
  102. package/lint/no-oversized-list-limit.grit +73 -0
  103. package/lint/no-unguarded-endpoint.grit +5 -5
  104. package/linter.test.ts +12 -0
  105. package/linter.ts +35 -12
  106. package/maintainabilityBaseline.test.ts +280 -0
  107. package/maintainabilityBaseline.ts +220 -0
  108. package/maintainabilityContract.test.ts +846 -0
  109. package/maintainabilityContract.ts +404 -0
  110. package/maintainabilityExceptionScanner.test.ts +90 -0
  111. package/maintainabilityExceptionScanner.ts +85 -0
  112. package/maintainabilityLimits.ts +60 -0
  113. package/maintainabilityReport.test.ts +130 -0
  114. package/maintainabilityReport.ts +153 -0
  115. package/mobile/desktopRelease.test.ts +1 -1
  116. package/mobile/desktopRelease.ts +3 -1
  117. package/mobile/desktopServerStage.test.ts +1 -0
  118. package/mobile/desktopServerStage.ts +3 -1
  119. package/mobile/nativeApp.test.ts +20 -5
  120. package/mobile/nativeApp.ts +4 -1
  121. package/mobile/nativeConfig.test.ts +209 -5
  122. package/mobile/nativeConfig.ts +127 -6
  123. package/mobile/nativePublicEnv.test.ts +2 -0
  124. package/mobile/nativePublicEnv.ts +13 -0
  125. package/package.json +4 -2
  126. package/platformTest/LinuxDockerTarget.ts +3 -0
  127. package/{codeAgent/feedback → preview}/PreviewChrome.ts +13 -7
  128. package/preview/PreviewView.ts +219 -0
  129. package/preview/index.ts +2 -0
  130. package/preview/previewView.test.ts +76 -0
  131. package/prompter.ts +7 -0
  132. package/qualityScanner.test.ts +75 -0
  133. package/qualityScanner.ts +17 -1
  134. package/routeSourceValidator.test.ts +11 -2
  135. package/routeSourceValidator.ts +14 -2
  136. package/serviceLoopCallScanner.test.ts +56 -0
  137. package/serviceLoopCallScanner.ts +122 -0
  138. package/shellOnlyEnv.test.ts +65 -0
  139. package/shellOnlyEnv.ts +54 -0
  140. package/signalMutationIndex.test.ts +15 -0
  141. package/signalMutationIndex.ts +23 -82
  142. package/sourceComments.test.ts +45 -0
  143. package/sourceComments.ts +70 -0
  144. package/sshHostKeys.ts +59 -0
  145. package/storeAccessScanner.test.ts +84 -0
  146. package/storeAccessScanner.ts +71 -0
  147. package/storeSource.ts +160 -0
  148. package/storeWriteScanner.test.ts +292 -0
  149. package/storeWriteScanner.ts +50 -0
  150. package/subspace.test.ts +10 -0
  151. package/subspaceClone.ts +3 -1
  152. package/subspaceConfig.ts +6 -0
  153. package/testOnlyExportScanner.test.ts +83 -0
  154. package/testOnlyExportScanner.ts +124 -0
  155. package/transforms/barrelImportsPlugin.ts +10 -4
  156. package/transforms/mcpAppPageTrimmer.test.ts +8 -2
  157. package/transforms/mcpAppPageTrimmer.ts +5 -20
  158. package/transforms/pureLazyMarker.ts +1 -1
  159. package/typeChecker.ts +32 -0
  160. package/typecheck/projectTypecheck.test.ts +18 -0
  161. package/typecheck/projectTypecheck.ts +29 -5
  162. package/typecheck/typecheck.proc.ts +6 -0
  163. package/typecheck/typecheckFailure.ts +13 -0
  164. package/types.ts +1 -0
  165. package/workflow/artifacts.ts +16 -2
  166. package/workflow/executor.ts +22 -7
  167. package/workflow/guardCatalog.test.ts +59 -0
  168. package/workflow/guardCatalog.ts +108 -0
  169. package/workflow/index.ts +4 -0
  170. package/workflow/interactionScaffold.ts +122 -0
  171. package/workflow/mutationScaffold.ts +533 -0
  172. package/workflow/mutationSource.test.ts +108 -0
  173. package/workflow/mutationSource.ts +219 -0
  174. package/workflow/plan.ts +82 -0
  175. package/workflow/render.ts +19 -4
  176. package/workflow/source.test.ts +185 -0
  177. package/workflow/source.ts +151 -24
  178. package/workflow/sourceAst.ts +10 -48
  179. package/workflow/types.ts +18 -1
  180. package/workspaceLayout.ts +2 -0
  181. package/writePath.test.ts +109 -0
  182. package/writePath.ts +219 -0
  183. package/writePlacementScanner.test.ts +77 -0
  184. package/writePlacementScanner.ts +50 -0
  185. package/writingActionIndex.test.ts +99 -0
  186. package/writingActionIndex.ts +63 -0
  187. package/codeAgent/feedback/PreviewView.ts +0 -95
  188. /package/{codeAgent/feedback → preview}/previewChrome.test.ts +0 -0
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import type { BaseBuildArtifact } from "akanjs/server";
4
4
  import { type PageEntry, resolveSsrPageEntriesForApp } from "../artifact/implicitRootLayout";
5
5
  import type { App } from "../commandDecorators";
6
+ import { type BarrelImportsPluginOptions, createBarrelImportsPlugin } from "../transforms/barrelImportsPlugin";
6
7
  import { createBrowserDirnamePlugin } from "../transforms/browserDirnamePlugin";
7
8
  import { SideEffectImportOrder } from "../transforms/sideEffectImportOrder";
8
9
  import { bundleDefine } from "./bundleDefine";
@@ -67,6 +68,7 @@ export class CsrArtifactBuilder {
67
68
  const options = CsrArtifactBuilder.bundleOptions(this.#app, this.#command, akanConfig.optimizeImports);
68
69
  const result = await Bun.build({
69
70
  ...options,
71
+ plugins: [...options.plugins, await CsrArtifactBuilder.barrelImportsPlugin(this.#app)],
70
72
  entrypoints: htmlBasePaths.map((basePath) => this.#generatedPath(CsrArtifactBuilder.htmlFilename(basePath))),
71
73
  files: generatedFiles,
72
74
  root: this.#generatedDir,
@@ -118,6 +120,11 @@ export class CsrArtifactBuilder {
118
120
  };
119
121
  }
120
122
 
123
+ //? Whole, the akanjs/ui barrel bundles every component and the optional peers (react-icons) they import.
124
+ static async barrelImportsPlugin(app: App, pipeAfter?: BarrelImportsPluginOptions["pipeAfter"]) {
125
+ return await createBarrelImportsPlugin(app, { barrels: ["akanjs/ui"], pipeAfter });
126
+ }
127
+
121
128
  /** Where a build records the `AKAN_PUBLIC_*` it inlined, which nothing in the minified bundle names. */
122
129
  static publicEnvPath(app: App) {
123
130
  return path.join(app.dist.cwdPath, ".akan", "artifact", "csr-public-env.json");
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, test } from "bun:test";
2
2
  import path from "node:path";
3
3
  import { tempDirs, writeText as write } from "../testHelpers";
4
+ import { CsrArtifactBuilder } from "./csrArtifactBuilder";
4
5
  import { McpAppViewBuilder } from "./mcpAppViewBuilder";
5
6
 
6
7
  const makeTempRoot = tempDirs("akan-mcp-app-views-");
@@ -132,6 +133,68 @@ describe("McpAppViewBuilder.discover", () => {
132
133
  });
133
134
  });
134
135
 
136
+ describe("McpAppViewBuilder.build", () => {
137
+ test("rewrites the akanjs/ui barrel, trims the page to its apps and drops a lazy component nothing renders", async () => {
138
+ const root = await makeTempRoot();
139
+ const file = (rel: string) => path.join(root, rel);
140
+ await write(
141
+ file("node_modules/akanjs/package.json"),
142
+ JSON.stringify({
143
+ name: "akanjs",
144
+ exports: { "./ui": "./ui/index.ts", "./ui/*": "./ui/*", "./webkit": "./webkit.ts" },
145
+ }),
146
+ );
147
+ await write(
148
+ file("node_modules/akanjs/webkit.ts"),
149
+ "export const McpAppBoot = { run: (view: unknown) => console.info(view) };\n",
150
+ );
151
+ await write(
152
+ file("node_modules/akanjs/ui/index.ts"),
153
+ 'export { Link } from "./Link";\nexport { Toolbox } from "./Toolbox";\n',
154
+ );
155
+ await write(file("node_modules/akanjs/ui/Link.ts"), 'export const Link = "a link";\n');
156
+ await write(
157
+ file("node_modules/akanjs/ui/Toolbox.ts"),
158
+ 'console.info("a toolbox loaded");\nexport const Toolbox = "a toolbox";\n',
159
+ );
160
+ await write(file("apps/demo/ui/lazy.ts"), "export const lazy = (load: () => Promise<unknown>) => ({ load });\n");
161
+ await write(file("apps/demo/ui/Heavy.ts"), 'export default "a heavy chunk";\n');
162
+ await write(
163
+ file("apps/demo/ui/cards.ts"),
164
+ 'import { lazy } from "./lazy";\nexport const Heavy = lazy(() => import("./Heavy"));\nexport const Card = "a card";\n',
165
+ );
166
+ await write(
167
+ file("apps/demo/page/home.tsx"),
168
+ [
169
+ 'import { Link } from "akanjs/ui";',
170
+ 'import { Card } from "../ui/cards";',
171
+ "const page = () => ({ app: (name: string, description: string, draw: () => unknown) => ({ render: (render: () => unknown) => [name, description, draw, render] }) });",
172
+ 'export default page().app("showHome", "Show home.", () => [Link, Card]).render(() => "drawn by the render");',
173
+ ].join("\n"),
174
+ );
175
+ const app = {
176
+ cwdPath: file("apps/demo"),
177
+ dist: { cwdPath: file("dist/apps/demo") },
178
+ workspace: { workspaceRoot: root },
179
+ getTsConfig: async () => ({ compilerOptions: {} }),
180
+ getPublicEnv: () => ({}),
181
+ logger: { warn: () => undefined },
182
+ };
183
+
184
+ await new McpAppViewBuilder(app as never, "build").build(
185
+ [{ key: "./home.tsx", moduleAbsPath: file("apps/demo/page/home.tsx") }],
186
+ { basePaths: [], cssAssets: {}, options: CsrArtifactBuilder.bundleOptions(app as never, "build") },
187
+ );
188
+
189
+ const view = await Bun.file(file("dist/apps/demo/mcp-apps/showHome.html")).text();
190
+ expect(view).toContain("a link");
191
+ expect(view).toContain("a card");
192
+ expect(view).not.toContain("a toolbox");
193
+ expect(view).not.toContain("drawn by the render");
194
+ expect(view).not.toContain("a heavy chunk");
195
+ });
196
+ });
197
+
135
198
  describe("McpAppViewBuilder.entryOf", () => {
136
199
  test("boots the one app with its page, the root layout and overrides it sits under, by relative path", () => {
137
200
  const view = {
@@ -78,7 +78,13 @@ export class McpAppViewBuilder {
78
78
  const pagePaths = [...new Set(views.map((view) => view.page.moduleAbsPath))];
79
79
  const result = await Bun.build({
80
80
  ...options,
81
- plugins: [...options.plugins, McpAppPageTrimmer.plugin(pagePaths), PureLazyMarker.plugin()],
81
+ plugins: [
82
+ ...options.plugins,
83
+ //? Bun runs one onLoad per module, so the one rewriting the barrel also trims the pages and marks `lazy()`; the
84
+ //? marker plugin takes the TypeScript that one leaves to Bun, a package's own outside node_modules/akanjs.
85
+ await CsrArtifactBuilder.barrelImportsPlugin(this.#app, McpAppPageTrimmer.pipe(pagePaths)),
86
+ PureLazyMarker.plugin(),
87
+ ],
82
88
  entrypoints: views.map((view) => path.join(generatedDir, `${view.name}.html`)),
83
89
  files,
84
90
  root: generatedDir,
@@ -0,0 +1,132 @@
1
+ import { describe, expect, spyOn, test } from "bun:test";
2
+ import { mkdir, symlink } from "node:fs/promises";
3
+ import path from "node:path";
4
+ import type { BunPlugin } from "bun";
5
+ import { resolveSsrPageEntries } from "../artifact/implicitRootLayout";
6
+ import { tempDirs, writeText as write } from "../testHelpers";
7
+ import { CsrArtifactBuilder } from "./csrArtifactBuilder";
8
+ import { VENDOR_SPECIFIERS } from "./vendorSpecifiers";
9
+
10
+ const makeTempRoot = tempDirs("akan-optional-peers-");
11
+ const akanjsRoot = path.resolve(import.meta.dir, "../../../akanjs");
12
+ const { peerDependenciesMeta } = (await Bun.file(path.join(akanjsRoot, "package.json")).json()) as {
13
+ peerDependenciesMeta: Record<string, { optional?: boolean }>;
14
+ };
15
+ //? The app installs these itself: they are peers so that it renders with one React.
16
+ const appRenderer = new Set(["react", "react-dom", "react-server-dom-webpack"]);
17
+ const optionalPeers = Object.entries(peerDependenciesMeta)
18
+ .filter(([name, meta]) => meta.optional && !appRenderer.has(name))
19
+ .map(([name]) => name);
20
+ const optionalPeerImport = new RegExp(
21
+ `^(?:${optionalPeers.map((name) => name.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&")).join("|")})(?:/|$)`,
22
+ );
23
+
24
+ const recordOptionalPeers = (reached: string[]): BunPlugin => ({
25
+ name: "record-optional-peers",
26
+ setup(build) {
27
+ build.onResolve({ filter: optionalPeerImport }, ({ path: specifier, importer }) => {
28
+ reached.push(`${path.relative(akanjsRoot, importer)} -> ${specifier}`);
29
+ return { path: specifier, external: true };
30
+ });
31
+ },
32
+ });
33
+
34
+ const linkPackage = async (root: string, name: string, dir: string) => {
35
+ await mkdir(path.join(root, "node_modules"), { recursive: true });
36
+ await symlink(dir, path.join(root, "node_modules", name));
37
+ };
38
+
39
+ const writeApp = async (indexPage: string) => {
40
+ const root = await makeTempRoot();
41
+ const appDir = path.join(root, "apps/demo");
42
+ await linkPackage(root, "akanjs", akanjsRoot);
43
+ await linkPackage(root, "react", path.dirname(Bun.resolveSync("react/package.json", akanjsRoot)));
44
+ await write(
45
+ path.join(root, "tsconfig.json"),
46
+ JSON.stringify({ compilerOptions: { paths: { "@apps/*": ["./apps/*"] } } }),
47
+ );
48
+ await write(path.join(appDir, "client.ts"), "export {};\n");
49
+ await write(path.join(appDir, "env/env.client.ts"), "export const env = {};\n");
50
+ await write(path.join(appDir, "lib/dict.ts"), "export const getAllDictionary = () => ({});\n");
51
+ await write(path.join(appDir, "page/_index.tsx"), indexPage);
52
+ await write(path.join(appDir, "page/_overrides.tsx"), "export default {};\n");
53
+ return { root, appDir, pageKeys: ["./_index.tsx", "./_overrides.tsx"] };
54
+ };
55
+
56
+ const buildCsr = async (indexPage: string) => {
57
+ const { root, appDir, pageKeys } = await writeApp(indexPage);
58
+ const app = {
59
+ name: "demo",
60
+ cwdPath: appDir,
61
+ dist: { cwdPath: path.join(root, "dist/apps/demo") },
62
+ workspace: { workspaceRoot: root },
63
+ getPageKeys: async () => pageKeys,
64
+ getConfig: async () => ({ basePaths: [], optimizeImports: [], barrelImports: [] }),
65
+ getTsConfig: async () => ({ compilerOptions: { paths: { "@apps/*": ["./apps/*"] } } }),
66
+ getPublicEnv: () => ({}),
67
+ log: () => undefined,
68
+ verbose: () => undefined,
69
+ logger: { warn: () => undefined },
70
+ };
71
+ const reached: string[] = [];
72
+ const build = Bun.build;
73
+ const recorded = spyOn(Bun, "build").mockImplementation((config) =>
74
+ build({ ...config, plugins: [recordOptionalPeers(reached), ...(config.plugins ?? [])] }),
75
+ );
76
+
77
+ try {
78
+ await new CsrArtifactBuilder(app as never, "build").build();
79
+ } finally {
80
+ recorded.mockRestore();
81
+ }
82
+ return { root, reached };
83
+ };
84
+
85
+ describe("optional peers of akanjs", () => {
86
+ test("the generated root layout, the _overrides.tsx wrapper and the runtime entries import none", async () => {
87
+ const { appDir, pageKeys } = await writeApp("export default function Page() {\n return null;\n}\n");
88
+ const generated = (await resolveSsrPageEntries({ appCwdPath: appDir, appName: "demo", pageKeys }))
89
+ .map(({ moduleAbsPath }) => moduleAbsPath)
90
+ .filter((file) => file.includes(`${path.sep}.akan${path.sep}`));
91
+ const runtime = [
92
+ path.join(akanjsRoot, "server/rscClient.tsx"),
93
+ path.join(akanjsRoot, "server/rscSegmentOutlet.tsx"),
94
+ path.join(akanjsRoot, "webkit/index.ts"),
95
+ ...VENDOR_SPECIFIERS.map((specifier) =>
96
+ path.join(akanjsRoot, "server/vendor", `${specifier.replaceAll("/", "-").replaceAll(".", "-")}.ts`),
97
+ ),
98
+ ];
99
+ expect(generated).toHaveLength(2);
100
+
101
+ //? No barrel rewrite, as a dev registry compiles every module an import names.
102
+ for (const target of ["browser", "bun"] as const) {
103
+ const reached: string[] = [];
104
+ const result = await Bun.build({
105
+ entrypoints: [...generated, ...runtime],
106
+ target,
107
+ throw: false,
108
+ plugins: [recordOptionalPeers(reached)],
109
+ });
110
+
111
+ expect(result.logs.map((log) => log.message)).toEqual([]);
112
+ expect(reached).toEqual([]);
113
+ }
114
+ });
115
+
116
+ test("the CSR shell build of a page drawing akanjs/ui components imports none", async () => {
117
+ const { reached } = await buildCsr(
118
+ 'import { Link, System } from "akanjs/ui";\n\nexport default function Page() {\n return (\n <Link href="/">\n <System.ThemeToggle />\n </Link>\n );\n}\n',
119
+ );
120
+
121
+ expect(reached).toEqual([]);
122
+ });
123
+
124
+ test("the MCP App view build of a page drawing akanjs/ui components imports none", async () => {
125
+ const { root, reached } = await buildCsr(
126
+ 'import { page } from "akanjs/client";\nimport { Link } from "akanjs/ui";\n\nexport default page()\n .app("showHome", "Show the home link.", () => <Link href="/">home</Link>)\n .render(() => null);\n',
127
+ );
128
+
129
+ expect(await Bun.file(path.join(root, "dist/apps/demo/mcp-apps/showHome.html")).exists()).toBe(true);
130
+ expect(reached).toEqual([]);
131
+ });
132
+ });
package/index.ts CHANGED
@@ -19,6 +19,7 @@ export type * from "./fileSys";
19
19
  export type * from "./frontendBuild";
20
20
  export type * from "./getRelatedCnsts";
21
21
  export type * from "./incrementalBuilder";
22
+ export type * from "./maintainabilityContract";
22
23
  export type * from "./mobile";
23
24
  export type * from "./packageExportsMap";
24
25
  export type * from "./prompter";
@@ -1,4 +1,5 @@
1
1
  import { afterAll, afterEach, beforeAll, describe, expect, test } from "bun:test";
2
+ import { AkanAppHost } from "../akanApp";
2
3
  import { DevGeneratedIndexSync } from "../frontendBuild";
3
4
  import { DevStabilityHarness, type DevStabilityHmrProbe } from "./devStabilityHarness";
4
5
 
@@ -467,7 +468,8 @@ export class FixtureService extends serve("fixture" as const, { serverMode: "bat
467
468
  const harness = await createHarness();
468
469
  const port = await harness.resolvePort();
469
470
  // Child 0's deterministic ws port, held the way an orphaned replica from a killed run would hold it.
470
- const blocker = Bun.serve({ port: port + 10_000, fetch: () => new Response("occupied") });
471
+ //? On the child's own 127.0.0.1: macOS lets a loopback bind share a port a wildcard listener holds.
472
+ const blocker = Bun.serve({ port: port + 10_000, hostname: "127.0.0.1", fetch: () => new Response("occupied") });
471
473
  try {
472
474
  const host = await harness.startHost();
473
475
  await host.waitForLog(/falling back to an ephemeral port/);
@@ -583,9 +585,9 @@ describe("dev resource budgets", () => {
583
585
  expect(await DevStabilityHarness.buildWorkerProcess(host.proc.pid)).toBeNull();
584
586
  });
585
587
 
586
- //? Between a fresh builder and one that has built a route, or the host stops enforcing it or never recycles.
587
- //? macOS keeps the bundler arenas (~380MiB built); Linux measured 101-115MiB fresh, 157-176MiB built and settled.
588
- const CEILING_MB = process.platform === "linux" ? 135 : 200;
588
+ //? Between a fresh builder and one that has built a route, or the host stops enforcing it or never recycles. Linux
589
+ //? measured 101-115MiB fresh, 157-176MiB built and settled; macOS's footprint (Bun 1.4.4) 65MiB fresh, 109MiB built.
590
+ const CEILING_MB = process.platform === "darwin" ? 87 : 135;
589
591
 
590
592
  budgetTest("recycles the builder at an unmeetable ceiling and keeps developing through it", async () => {
591
593
  const harness = await createHarness();
@@ -635,9 +637,11 @@ describe("dev resource budgets", () => {
635
637
  await host.waitForLogSince(mark, /pages-rebundle ok/, WAIT_MS).catch(() => undefined);
636
638
  }
637
639
  const settled = await DevStabilityHarness.builderProcess(host.proc.pid);
640
+ //? Read as the host reads it: on macOS `ps` also counts the pages the builder freed.
641
+ const settledRss = settled ? await AkanAppHost.readProcessRssBytes(settled.pid) : null;
638
642
  const warned = /ceiling costs about one boot build per interval/.test(host.logs.join("").slice(start));
639
643
  //? Past the interval (a slow machine) the next report recycles instead of warning: either answer settles it.
640
- if ((settled?.rssBytes ?? 0) >= CEILING_MB * MB && !warned)
644
+ if ((settledRss ?? 0) >= CEILING_MB * MB && !warned)
641
645
  await host.waitForLogSince(
642
646
  settledFrom,
643
647
  /ceiling costs about one boot build per interval|recycling builder pid=\d+|skipped: the builder fell to/,
@@ -1,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import fs from "node:fs";
2
3
  import os from "node:os";
3
4
  import path from "node:path";
@@ -93,6 +94,7 @@ export class SsrMemoryProbe {
93
94
  }
94
95
 
95
96
  readonly #options: SsrMemoryProbeOptions;
97
+ readonly #statusToken = randomBytes(32).toString("hex");
96
98
  #gatewayPid = 0;
97
99
 
98
100
  constructor(options: SsrMemoryProbeOptions) {
@@ -119,6 +121,8 @@ export class SsrMemoryProbe {
119
121
  SERVER_MODE: "federation",
120
122
  ...(basePaths ? { AKAN_PUBLIC_BASE_PATHS: basePaths } : {}),
121
123
  PORT: String(port),
124
+ // A built app run outside `akan start` keeps the health detail and metrics behind this token.
125
+ AKAN_STATUS_TOKEN: this.#statusToken,
122
126
  AKAN_MEMORY_LOG: "1",
123
127
  AKAN_MEMORY_LOG_INTERVAL_MS: String(this.#options.metricsIntervalMs),
124
128
  ...(cacheOff ? { AKAN_HTML_RESULT_CACHE: "0", AKAN_RSC_RESULT_CACHE: "0" } : {}),
@@ -315,6 +319,7 @@ export class SsrMemoryProbe {
315
319
  async #fetchChildMetrics(): Promise<Array<Record<string, number>>> {
316
320
  try {
317
321
  const res = await fetch(`http://localhost:${this.#options.port}/_akan/app/metrics`, {
322
+ headers: { authorization: `Bearer ${this.#statusToken}` },
318
323
  signal: AbortSignal.timeout(5_000),
319
324
  });
320
325
  if (!res.ok) return [];
@@ -393,6 +398,7 @@ export class SsrMemoryProbe {
393
398
  while (Date.now() < deadline) {
394
399
  try {
395
400
  const res = await fetch(`http://localhost:${this.#options.port}/_akan/app/health`, {
401
+ headers: { authorization: `Bearer ${this.#statusToken}` },
396
402
  signal: AbortSignal.timeout(2_000),
397
403
  });
398
404
  if (res.ok) {
@@ -0,0 +1,89 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import ts from "typescript";
3
+ import { KindBranchScanner } from "./kindBranchScanner";
4
+ import type { SourceFileInfo } from "./qualityScanner";
5
+
6
+ const filesOf = (files: Record<string, string>): SourceFileInfo[] =>
7
+ Object.entries(files).map(([file, content]) => ({
8
+ file,
9
+ absolutePath: `/repo/${file}`,
10
+ content,
11
+ sourceFile: ts.createSourceFile(
12
+ file,
13
+ content,
14
+ ts.ScriptTarget.Latest,
15
+ true,
16
+ file.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS,
17
+ ),
18
+ }));
19
+
20
+ const pinConstant = `import { enumOf } from "akanjs/base";
21
+ export class PinKind extends enumOf("pinKind", ["videoScene", "videoTake", "memo"] as const) {}
22
+ export class PinStatus extends enumOf("pinStatus", ["active", "videoTake"] as const) {}
23
+ `;
24
+ const comparing = (expression: string) => `export const isIt = (pin: { kind: string }) => ${expression};\n`;
25
+
26
+ describe("KindBranchScanner", () => {
27
+ test("warns on a kind value compared in three files outside its module, naming them and the table to declare", () => {
28
+ const warnings = new KindBranchScanner().scan(
29
+ filesOf({
30
+ "apps/demo/lib/pin/pin.constant.ts": pinConstant,
31
+ "apps/demo/lib/pin/Pin.Zone.tsx": comparing('pin.kind === "videoScene"'),
32
+ "apps/demo/lib/board/Board.Zone.tsx": comparing('pin.kind === "videoScene"'),
33
+ "apps/demo/lib/take/take.store.ts": comparing('"videoScene" !== pin.kind'),
34
+ "apps/demo/ui/PinCard.tsx": `export const label = (kind: string) => {\n switch (kind) {\n case "videoScene":\n return 1;\n }\n};\n`,
35
+ "apps/demo/ui/PinCard.test.tsx": comparing('pin.kind === "videoScene"'),
36
+ }),
37
+ );
38
+
39
+ expect(warnings).toHaveLength(1);
40
+ expect(warnings[0]).toMatchObject({
41
+ rule: "akan.global.kind-branch",
42
+ severity: "warning",
43
+ file: "apps/demo/lib/pin/pin.constant.ts",
44
+ line: 2,
45
+ message: '"videoScene" of cnst.PinKind is branched on in 3 files outside the enum\'s module.',
46
+ locations: [
47
+ { file: "apps/demo/lib/board/Board.Zone.tsx", line: 1 },
48
+ { file: "apps/demo/lib/take/take.store.ts", line: 1 },
49
+ { file: "apps/demo/ui/PinCard.tsx", line: 3 },
50
+ ],
51
+ });
52
+ expect(warnings[0]?.fix).toContain('{ [kind in cnst.PinKind["value"]]: … }');
53
+ });
54
+
55
+ test("reads only a subject named for a kind, and only enums named for one", () => {
56
+ const warnings = new KindBranchScanner().scan(
57
+ filesOf({
58
+ "apps/demo/lib/pin/pin.constant.ts": pinConstant,
59
+ "apps/demo/lib/a/A.Zone.tsx": comparing('pin.status === "active" || label === "videoScene"'),
60
+ "apps/demo/lib/b/B.Zone.tsx": comparing('pin.status === "active" || label === "videoScene"'),
61
+ "apps/demo/lib/c/C.Zone.tsx": comparing('pin.status === "active" || label === "videoScene"'),
62
+ }),
63
+ );
64
+
65
+ expect(warnings).toEqual([]);
66
+ });
67
+
68
+ test("groups the enums sharing a value, reached from the app's own lib or a lib, never another app or lib", () => {
69
+ const warnings = new KindBranchScanner().scan(
70
+ filesOf({
71
+ "apps/demo/lib/pin/pin.constant.ts": pinConstant,
72
+ "libs/media/lib/clip/clip.constant.ts": `export class ClipType extends enumOf("clipType", ["videoTake"] as const) {}\n`,
73
+ "apps/demo/lib/a/A.Zone.tsx": comparing('pin.kind === "videoTake"'),
74
+ "apps/demo/lib/b/B.Zone.tsx": comparing('pin.kind === "videoTake"'),
75
+ "apps/demo/lib/c/C.Zone.tsx": comparing('pin.kind === "videoTake"'),
76
+ "apps/other/lib/a/A.Zone.tsx": comparing('pin.kind === "videoTake"'),
77
+ "apps/other/lib/b/B.Zone.tsx": comparing('pin.kind === "videoTake"'),
78
+ "libs/shared/lib/a/A.Zone.tsx": comparing('pin.kind === "videoTake"'),
79
+ }),
80
+ );
81
+
82
+ expect(warnings.map(({ file, message }) => ({ file, message }))).toEqual([
83
+ {
84
+ file: "apps/demo/lib/pin/pin.constant.ts",
85
+ message: '"videoTake" of cnst.PinKind / cnst.ClipType is branched on in 3 files outside the enum\'s module.',
86
+ },
87
+ ]);
88
+ });
89
+ });
@@ -0,0 +1,158 @@
1
+ import path from "node:path";
2
+ import ts from "typescript";
3
+ import type { QualityWarning, SourceFileInfo } from "./qualityScanner";
4
+
5
+ interface EnumDeclaration {
6
+ name: string;
7
+ sys: string;
8
+ file: string;
9
+ line: number;
10
+ module: string;
11
+ values: string[];
12
+ }
13
+
14
+ interface BranchSite {
15
+ value: string;
16
+ file: string;
17
+ line: number;
18
+ }
19
+
20
+ //? Enums sharing a value form one group: short of the type checker, nothing tells which one a literal means.
21
+ export class KindBranchScanner {
22
+ static readonly rule = "akan.global.kind-branch";
23
+ static readonly minFileNum = 3;
24
+ static readonly #kindName = /(Kind|Type|Mode)$/;
25
+ static readonly #kindSubject = /^(kind|type|mode)$|[a-z](Kind|Type|Mode)$/;
26
+ static readonly #equalities = new Set([
27
+ ts.SyntaxKind.EqualsEqualsEqualsToken,
28
+ ts.SyntaxKind.ExclamationEqualsEqualsToken,
29
+ ts.SyntaxKind.EqualsEqualsToken,
30
+ ts.SyntaxKind.ExclamationEqualsToken,
31
+ ]);
32
+
33
+ scan(sourceFiles: SourceFileInfo[]): QualityWarning[] {
34
+ const enums = sourceFiles.flatMap((sourceFile) => KindBranchScanner.#enumsOf(sourceFile));
35
+ const ownersByValue = new Map<string, EnumDeclaration[]>();
36
+ for (const declaration of enums)
37
+ for (const value of declaration.values)
38
+ ownersByValue.set(value, [...(ownersByValue.get(value) ?? []), declaration]);
39
+ const sitesByGroup = new Map<string, { owners: EnumDeclaration[]; sites: BranchSite[] }>();
40
+ for (const sourceFile of sourceFiles) {
41
+ if (!/^(apps|libs)\//.test(sourceFile.file) || /\.(test|spec)\.tsx?$/.test(sourceFile.file)) continue;
42
+ const sys = KindBranchScanner.#sysOf(sourceFile.file);
43
+ //? An app reaches every lib's enums; a lib only its own, as nothing here knows which libs it depends on.
44
+ for (const site of KindBranchScanner.#sitesIn(sourceFile, ownersByValue)) {
45
+ const owners = (ownersByValue.get(site.value) ?? []).filter(
46
+ (owner) => owner.sys === sys || (owner.sys.startsWith("libs/") && sys.startsWith("apps/")),
47
+ );
48
+ if (!owners.length || owners.some(({ module }) => site.file.startsWith(module))) continue;
49
+ const group = owners.map(({ name, file }) => `${file}#${name}`).join(",");
50
+ const entry = sitesByGroup.get(group) ?? { owners, sites: [] };
51
+ entry.sites.push(site);
52
+ sitesByGroup.set(group, entry);
53
+ }
54
+ }
55
+ return [...sitesByGroup.values()].flatMap(({ owners, sites }) => KindBranchScanner.#warningOf(owners, sites));
56
+ }
57
+
58
+ static #warningOf(owners: EnumDeclaration[], sites: BranchSite[]): QualityWarning[] {
59
+ const filesByValue = new Map<string, Set<string>>();
60
+ for (const { value, file } of sites) filesByValue.set(value, (filesByValue.get(value) ?? new Set()).add(file));
61
+ const values = [...filesByValue]
62
+ .filter(([, files]) => files.size >= KindBranchScanner.minFileNum)
63
+ .map(([value]) => value)
64
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0));
65
+ const [owner] = owners;
66
+ if (!owner || !values.length) return [];
67
+ const branched = sites.filter(({ value }) => values.includes(value));
68
+ const fileNum = new Set(branched.map(({ file }) => file)).size;
69
+ const enumNames = owners.map(({ name }) => `cnst.${name}`).join(" / ");
70
+ return [
71
+ {
72
+ rule: KindBranchScanner.rule,
73
+ scope: "global",
74
+ severity: "warning",
75
+ file: owner.file,
76
+ line: owner.line,
77
+ message: `${values.map((value) => `"${value}"`).join(", ")} of ${enumNames} ${values.length > 1 ? "are" : "is"} branched on in ${fileNum} files outside the enum's module.`,
78
+ locations: branched.map(({ file, line }) => ({ file, line })),
79
+ fix: `Declare one table keyed by the kind, \`{ [kind in cnst.${owner.name}["value"]]: … }\` (a component, label or handler per kind), and look the kind up in it instead of comparing it in each file.`,
80
+ },
81
+ ];
82
+ }
83
+
84
+ static #sitesIn({ file, sourceFile }: SourceFileInfo, ownersByValue: Map<string, EnumDeclaration[]>) {
85
+ const sites: BranchSite[] = [];
86
+ const record = (literal: ts.Expression, subject: ts.Expression) => {
87
+ if (!ts.isStringLiteralLike(literal) || !ownersByValue.has(literal.text)) return;
88
+ if (!KindBranchScanner.#isKindSubject(subject)) return;
89
+ sites.push({
90
+ value: literal.text,
91
+ file,
92
+ line: sourceFile.getLineAndCharacterOfPosition(literal.getStart(sourceFile)).line + 1,
93
+ });
94
+ };
95
+ const visit = (node: ts.Node) => {
96
+ if (ts.isBinaryExpression(node) && KindBranchScanner.#equalities.has(node.operatorToken.kind)) {
97
+ record(node.left, node.right);
98
+ record(node.right, node.left);
99
+ } else if (ts.isCaseClause(node) && ts.isSwitchStatement(node.parent.parent))
100
+ record(node.expression, node.parent.parent.expression);
101
+ ts.forEachChild(node, visit);
102
+ };
103
+ visit(sourceFile);
104
+ return sites;
105
+ }
106
+
107
+ //? Only a subject named for a kind (`kind`, `pin.kind`, `viewMode`) counts: a bare `"active"` matches too much.
108
+ static #isKindSubject(subject: ts.Expression) {
109
+ let current = subject;
110
+ while (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current))
111
+ current = current.expression;
112
+ const name = ts.isIdentifier(current)
113
+ ? current.text
114
+ : ts.isPropertyAccessExpression(current)
115
+ ? current.name.text
116
+ : ts.isElementAccessExpression(current) && ts.isStringLiteralLike(current.argumentExpression)
117
+ ? current.argumentExpression.text
118
+ : "";
119
+ return KindBranchScanner.#kindSubject.test(name);
120
+ }
121
+
122
+ static #enumsOf({ file, sourceFile }: SourceFileInfo): EnumDeclaration[] {
123
+ if (!/^(apps|libs)\/[^/]+\/lib\/.+\.constant\.ts$/.test(file)) return [];
124
+ return sourceFile.statements.flatMap((statement) => {
125
+ if (!ts.isClassDeclaration(statement) || !statement.name) return [];
126
+ if (!KindBranchScanner.#kindName.test(statement.name.text)) return [];
127
+ const values = KindBranchScanner.#enumValuesOf(statement);
128
+ if (!values) return [];
129
+ return [
130
+ {
131
+ name: statement.name.text,
132
+ sys: KindBranchScanner.#sysOf(file),
133
+ file,
134
+ line: sourceFile.getLineAndCharacterOfPosition(statement.getStart(sourceFile)).line + 1,
135
+ module: `${path.posix.dirname(file)}/`,
136
+ values,
137
+ },
138
+ ];
139
+ });
140
+ }
141
+
142
+ static #sysOf(file: string) {
143
+ return file.split("/").slice(0, 2).join("/");
144
+ }
145
+
146
+ static #enumValuesOf(statement: ts.ClassDeclaration) {
147
+ for (const clause of statement.heritageClauses ?? [])
148
+ for (const { expression } of clause.types) {
149
+ if (!ts.isCallExpression(expression) || !ts.isIdentifier(expression.expression)) continue;
150
+ if (expression.expression.text !== "enumOf") continue;
151
+ let list = expression.arguments[1];
152
+ while (list && (ts.isAsExpression(list) || ts.isParenthesizedExpression(list))) list = list.expression;
153
+ if (!list || !ts.isArrayLiteralExpression(list)) return null;
154
+ return list.elements.filter(ts.isStringLiteralLike).map(({ text }) => text);
155
+ }
156
+ return null;
157
+ }
158
+ }
@@ -0,0 +1,18 @@
1
+ export const everyCourt = () => fetch.courtListInPublic(0, 10000); // @flag
2
+ export const devicesOfOrg = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 1000); // @flag
3
+ export const sortedDevices = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 1000, "latest"); // @flag
4
+ export const withPolicy = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 1000, null, { crystalize: false }); // @flag
5
+ export const rootList = () => fetch.deviceList(null, 501); // @flag
6
+ export const separated = () => fetch.deviceList(undefined, 10_000); // @flag
7
+ export const exponent = () => fetch.deviceList(0, 1e4); // @flag
8
+ export const namedSkip = (orgId: string, skip: number) => fetch.alertListInOrg(orgId, skip, 2000); // @flag
9
+ export const pagedSkip = (orgId: string, page: number) => fetch.alertListInOrg(orgId, (page - 1) * 20, 2000); // @flag
10
+ export const afterSliceNumbers = () => fetch.statListInYear(2026, 0, 600); // @flag
11
+ export const initInRoute = (orgId: string) => fetch.initDeviceInOrg(orgId, { limit: 1000 }); // @flag
12
+ export const initRoot = () => fetch.initDevice({ sort: "latest", limit: 9999 }); // @flag
13
+ export const getInit = (orgId: string) => fetch.getDeviceInitInOrg(orgId, { limit: 1000 }); // @flag
14
+ export const storeInit = (orgId: string) => st.do.initDeviceInOrg(orgId, { limit: 1000 }); // @flag
15
+ export const storeRefresh = () => st.do.refreshDeviceInOrg({ limit: 1000 }); // @flag
16
+ export const quotedKey = (orgId: string) => st.do.initDeviceInOrg(orgId, { "limit": 1000 }); // @flag
17
+ export const storeSetLimit = () => st.do.setLimitOfDeviceInOrg(1000); // @flag
18
+ export const Monitor = () => <button type="button" onClick={() => st.do.setLimitOfAlertInOrg(5000)}>All</button>; // @flag
@@ -0,0 +1,23 @@
1
+ export const atTheCap = () => fetch.courtListInPublic(0, 500); // @ok
2
+ export const onePage = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 20, "latest"); // @ok
3
+ export const variableLimit = (orgId: string, limit: number) => fetch.deviceListInOrg(orgId, 0, limit); // @ok
4
+ export const everyRowMode = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 0); // @ok
5
+ export const yearSlice = () => fetch.statListInYear(2026); // @ok
6
+ export const yearAfterId = (orgId: string) => fetch.statListInOrgYear(orgId, 2026); // @ok
7
+ export const monthAndYear = () => fetch.statListInMonthOfYear(11, 2026); // @ok
8
+ export const twoYears = () => fetch.statListInRange(2025, 2026); // @ok
9
+ export const numbersBeforePaging = () => fetch.statListInRange(0, 2026, 0, 20); // @ok
10
+ export const numbersWithoutPaging = () => fetch.statListInRange(0, 2026, 0); // @ok
11
+ export const notAList = () => fetch.device(0, 1000); // @ok
12
+ export const notFetch = () => api.deviceList(0, 1000); // @ok
13
+ export const lowercaseList = () => fetch.checklist(0, 1000); // @ok
14
+ export const initAtTheCap = (orgId: string) => fetch.initDeviceInOrg(orgId, { limit: 500 }); // @ok
15
+ export const rateLimit = (orgId: string) => fetch.initDeviceInOrg(orgId, { rateLimit: 1000 }); // @ok
16
+ export const nestedLimit = (orgId: string) => fetch.initDeviceInOrg(orgId, { default: { limit: 1000 } }); // @ok
17
+ export const mutationLimit = (id: string) => fetch.updateQuota(id, { limit: 1000 }); // @ok
18
+ export const storeWrite = () => st.do.setLimitOnQuota(1000); // @ok
19
+ export const storeSetSmallLimit = () => st.do.setLimitOfDeviceInOrg(100); // @ok
20
+ export const storeSetPage = () => st.do.setPageOfDeviceInOrg(1000); // @ok
21
+ export const policyOnList = (orgId: string) => fetch.deviceListInOrg(orgId, 0, 20, "latest", { limit: 1000 }); // @ok
22
+ export const Docs = () => <code>fetch.courtListInPublic(0, 10000)</code>; // @ok
23
+ export const sample = "fetch.courtListInPublic(0, 10000)"; // @ok