@akanjs/devkit 3.0.0-alpha.1 → 3.0.0-alpha.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.
- package/agentsIndex.ts +26 -0
- package/akanContext.ts +43 -31
- package/applicationBuildRunner.ts +1 -1
- package/biome.base.json +301 -0
- package/biomeBase.ts +9 -0
- package/capacitorApp.test.ts +8 -0
- package/capacitorApp.ts +112 -18
- package/executors.test.ts +3 -1
- package/executors.ts +2 -1
- package/frontendBuild/allRoutesBuilder.ts +27 -8
- package/frontendBuild/cssCandidateCache.test.ts +38 -0
- package/frontendBuild/cssCandidateCache.ts +15 -3
- package/frontendBuild/precompressArtifacts.ts +35 -7
- package/frontendBuild/routeClientBuilder.ts +14 -2
- package/lint/no-bang-comment-in-client.grit +20 -0
- package/lint/no-import-client-in-server.grit +48 -0
- package/lint/no-import-server-in-client.grit +45 -0
- package/lint/no-return-in-store-action.grit +35 -0
- package/package.json +3 -2
- package/qualityScanner.test.ts +14 -0
- package/qualityScanner.ts +5 -44
- package/repoIdentity.ts +42 -0
- package/routeSourceValidator.test.ts +41 -0
- package/routeSourceValidator.ts +2 -44
- package/scanInfo.ts +2 -43
- package/workspaceLayout.test.ts +26 -0
- package/workspaceLayout.ts +60 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
engine biome(1.0)
|
|
2
|
+
language js(typescript, jsx)
|
|
3
|
+
|
|
4
|
+
// Every prototype method of a `store(...)` class is registered as an action
|
|
5
|
+
// (StoreRegistry.register) and reaches callers only as `st.do.<action>`, which re-wraps it
|
|
6
|
+
// (StoreInstance.#extendAccessors) and is typed void by `VoidActions` in
|
|
7
|
+
// pkgs/akanjs/store/types.ts. A returned value is therefore unreachable from every call
|
|
8
|
+
// site — it reads like a contract while being dead code. Write the result into state with
|
|
9
|
+
// `this.set({ ... })` instead.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately not flagged, because none of these are actions: a bare `return;` guard, a
|
|
12
|
+
// `return` belonging to a nested callback, a getter, a `static` helper, and a class-property
|
|
13
|
+
// arrow (an instance field, so `register` never sees it).
|
|
14
|
+
`return $value;` as $return where {
|
|
15
|
+
not $value <: r"",
|
|
16
|
+
$return <: within JsClassDeclaration() as $storeClass where {
|
|
17
|
+
$storeClass <: contains JsExtendsClause() as $extends where {
|
|
18
|
+
$extends <: r"extends\s+store\([\s\S]*"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
$return <: within JsMethodClassMember() as $action where {
|
|
22
|
+
not $action <: r"static[\s\S]*"
|
|
23
|
+
},
|
|
24
|
+
not $return <: within JsArrowFunctionExpression(),
|
|
25
|
+
not $return <: within JsFunctionExpression(),
|
|
26
|
+
not $return <: within JsFunctionDeclaration(),
|
|
27
|
+
not $return <: within JsMethodObjectMember(),
|
|
28
|
+
not $return <: within JsGetterObjectMember(),
|
|
29
|
+
not $return <: within JsSetterObjectMember(),
|
|
30
|
+
register_diagnostic(
|
|
31
|
+
span = $return,
|
|
32
|
+
message = "Store actions dispatch as void — st.do.<action>() never exposes this value. Write the result into state with this.set({ ... }) and drop the returned value; a bare 'return;' guard clause is fine.",
|
|
33
|
+
severity = "error"
|
|
34
|
+
)
|
|
35
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akanjs/devkit",
|
|
3
|
-
"version": "3.0.0-alpha.
|
|
3
|
+
"version": "3.0.0-alpha.10",
|
|
4
4
|
"sourceType": "module",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"publishConfig": {
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
"default": "./index.ts"
|
|
24
24
|
},
|
|
25
25
|
"./package.json": "./package.json",
|
|
26
|
+
"./biome.base.json": "./biome.base.json",
|
|
26
27
|
"./akanApp": "./akanApp/index.ts",
|
|
27
28
|
"./akanConfig": "./akanConfig/index.ts",
|
|
28
29
|
"./artifact": "./artifact/index.ts",
|
|
@@ -44,7 +45,7 @@
|
|
|
44
45
|
"@langchain/openai": "^1.4.6",
|
|
45
46
|
"@tailwindcss/node": "^4.3.0",
|
|
46
47
|
"@trapezedev/project": "^7.1.4",
|
|
47
|
-
"akanjs": "3.0.0-alpha.
|
|
48
|
+
"akanjs": "3.0.0-alpha.10",
|
|
48
49
|
"chalk": "^5.6.2",
|
|
49
50
|
"commander": "^14.0.3",
|
|
50
51
|
"dayjs": "^1.11.20",
|
package/qualityScanner.test.ts
CHANGED
|
@@ -183,3 +183,17 @@ describe("AkanQualityScanner ssr rules", () => {
|
|
|
183
183
|
expect(ssrBalance[2]).toMatchObject({ scope: "workspace", serverMass: 3, clientMass: 1 });
|
|
184
184
|
});
|
|
185
185
|
});
|
|
186
|
+
|
|
187
|
+
describe("AkanQualityScanner layout rules", () => {
|
|
188
|
+
test("flags an unknown app root file but not a facet entrypoint", async () => {
|
|
189
|
+
const root = await makeWorkspace({
|
|
190
|
+
"apps/demo/client.ts": "export const client = 1;\n",
|
|
191
|
+
"apps/demo/helper.ts": "export const helper = 1;\n",
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
const warnings = rulesOf(await new AkanQualityScanner().scan(root), "akan.layout.app-root-file");
|
|
195
|
+
|
|
196
|
+
expect(warnings).toHaveLength(1);
|
|
197
|
+
expect(warnings[0]?.file).toBe("apps/demo/helper.ts");
|
|
198
|
+
});
|
|
199
|
+
});
|
package/qualityScanner.ts
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { RESERVED_ROUTE_CONFIG_EXPORTS } from "akanjs/common";
|
|
4
5
|
import ignore from "ignore";
|
|
5
6
|
import ts from "typescript";
|
|
6
7
|
import { AbstractDoc } from "./abstractDoc";
|
|
7
8
|
import { formatSsrBalance, type SsrBalanceEntry, SsrScanner } from "./ssrScanner";
|
|
9
|
+
import { appRootAllowedFiles, libFacetRootAllowedFiles } from "./workspaceLayout";
|
|
8
10
|
|
|
9
11
|
type QualitySeverity = "warning";
|
|
10
12
|
type QualityScope = "global" | "file" | "convention" | "layout" | "ssr";
|
|
@@ -90,29 +92,6 @@ const SUGGESTED_RULES = [
|
|
|
90
92
|
"Avoid large mixed-purpose class files; class export files should import helpers from neighboring utility files instead of declaring them inline.",
|
|
91
93
|
];
|
|
92
94
|
|
|
93
|
-
const APP_ROOT_FILES = new Set([
|
|
94
|
-
"akan.app.json",
|
|
95
|
-
"akan.config.ts",
|
|
96
|
-
"capacitor.config.ts",
|
|
97
|
-
"client.ts",
|
|
98
|
-
"main.ts",
|
|
99
|
-
"package.json",
|
|
100
|
-
"server.ts",
|
|
101
|
-
"tsconfig.json",
|
|
102
|
-
]);
|
|
103
|
-
|
|
104
|
-
const LIB_ROOT_FILES = new Set([
|
|
105
|
-
"cnst.ts",
|
|
106
|
-
"db.ts",
|
|
107
|
-
"dict.ts",
|
|
108
|
-
"option.ts",
|
|
109
|
-
"sig.ts",
|
|
110
|
-
"srv.ts",
|
|
111
|
-
"st.ts",
|
|
112
|
-
"useClient.ts",
|
|
113
|
-
"useServer.ts",
|
|
114
|
-
]);
|
|
115
|
-
|
|
116
95
|
const CONVENTION_SUFFIXES = [
|
|
117
96
|
".constant.ts",
|
|
118
97
|
".dictionary.ts",
|
|
@@ -122,24 +101,6 @@ const CONVENTION_SUFFIXES = [
|
|
|
122
101
|
".store.ts",
|
|
123
102
|
] as const;
|
|
124
103
|
|
|
125
|
-
// Non-PascalCase exports the framework recognizes on page/layout route modules (see PageModule/LayoutModule
|
|
126
|
-
// in pkgs/akanjs/client/csrTypes.ts). PascalCase route exports (Loading, NotFound, Error) pass the component
|
|
127
|
-
// check, and the `default` export is handled separately.
|
|
128
|
-
const PAGE_RESERVED_EXPORTS = new Set([
|
|
129
|
-
"pageConfig",
|
|
130
|
-
"head",
|
|
131
|
-
"metadata",
|
|
132
|
-
"generateHead",
|
|
133
|
-
"generateMetadata",
|
|
134
|
-
"fonts",
|
|
135
|
-
"manifest",
|
|
136
|
-
"theme",
|
|
137
|
-
"reconnect",
|
|
138
|
-
"wsConnect",
|
|
139
|
-
"layoutStyle",
|
|
140
|
-
"gaTrackingId",
|
|
141
|
-
]);
|
|
142
|
-
|
|
143
104
|
// How to remediate each rule, keyed by rule id. Surfaced as a `fix:` line per warning (text + JSON output)
|
|
144
105
|
// so the scan result tells the reader what to do, not just what is wrong.
|
|
145
106
|
const RULE_FIXES: Record<string, string> = {
|
|
@@ -449,7 +410,7 @@ export class AkanQualityScanner {
|
|
|
449
410
|
#scanLayoutQuality(sourceFile: SourceFileInfo): QualityWarning[] {
|
|
450
411
|
const segments = sourceFile.file.split("/");
|
|
451
412
|
const warnings: QualityWarning[] = [];
|
|
452
|
-
if (segments[0] === "apps" && segments.length === 3 && !
|
|
413
|
+
if (segments[0] === "apps" && segments.length === 3 && !appRootAllowedFiles.has(segments[2])) {
|
|
453
414
|
warnings.push({
|
|
454
415
|
rule: "akan.layout.app-root-file",
|
|
455
416
|
scope: "layout",
|
|
@@ -460,7 +421,7 @@ export class AkanQualityScanner {
|
|
|
460
421
|
}
|
|
461
422
|
|
|
462
423
|
const libRootFile = getLibRootFile(sourceFile.file);
|
|
463
|
-
if (libRootFile && !
|
|
424
|
+
if (libRootFile && !libFacetRootAllowedFiles.has(libRootFile)) {
|
|
464
425
|
warnings.push({
|
|
465
426
|
rule: "akan.layout.lib-root-file",
|
|
466
427
|
scope: "layout",
|
|
@@ -691,7 +652,7 @@ function isRestrictedInternalKind(kind: ComponentFileDeclaration["kind"]) {
|
|
|
691
652
|
|
|
692
653
|
function isAllowedComponentExport(declaration: ComponentFileDeclaration, isPage: boolean) {
|
|
693
654
|
if (isComponentValueKind(declaration.kind) && isPascalCaseName(declaration.name)) return true;
|
|
694
|
-
return isPage &&
|
|
655
|
+
return isPage && RESERVED_ROUTE_CONFIG_EXPORTS.has(declaration.name);
|
|
695
656
|
}
|
|
696
657
|
|
|
697
658
|
function isPascalCaseName(name: string) {
|
package/repoIdentity.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
// Resolved once per root: every CLI command builds a WorkspaceExecutor, and this would otherwise fork git on each.
|
|
5
|
+
const resolved = new Map<string, string>();
|
|
6
|
+
|
|
7
|
+
const readRemoteName = (workspaceRoot: string): string | null => {
|
|
8
|
+
try {
|
|
9
|
+
const url = execFileSync("git", ["config", "--get", "remote.origin.url"], {
|
|
10
|
+
cwd: workspaceRoot,
|
|
11
|
+
encoding: "utf-8",
|
|
12
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
13
|
+
}).trim();
|
|
14
|
+
// Both remote spellings end in the repository: git@host:owner/name.git and https://host/owner/name.git.
|
|
15
|
+
return (
|
|
16
|
+
url
|
|
17
|
+
.replace(/\.git$/, "")
|
|
18
|
+
.split(/[/:]/)
|
|
19
|
+
.pop() || null
|
|
20
|
+
);
|
|
21
|
+
} catch {
|
|
22
|
+
// No git, no origin, or no git binary — a fresh `akan workspace` before its first commit lands here.
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The repository's own name.
|
|
29
|
+
*
|
|
30
|
+
* Deriving it from the working directory made every generated file that names the repo — the AGENTS.md title, its
|
|
31
|
+
* `- Repo:` line — depend on what each person happened to call the folder they cloned into, so one commit rendered
|
|
32
|
+
* a different guide per developer and the diff never settled. The origin remote is the one identity every clone
|
|
33
|
+
* shares. `AKAN_PUBLIC_REPO_NAME` is deliberately not consulted: that is a deployment namespace (queue prefixes,
|
|
34
|
+
* cache keys, secret paths) which a monorepo hosting several products legitimately points somewhere else.
|
|
35
|
+
*/
|
|
36
|
+
export const resolveRepoName = (workspaceRoot: string): string => {
|
|
37
|
+
const cached = resolved.get(workspaceRoot);
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
const repoName = readRemoteName(workspaceRoot) ?? path.basename(workspaceRoot);
|
|
40
|
+
resolved.set(workspaceRoot, repoName);
|
|
41
|
+
return repoName;
|
|
42
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { RouteSourceValidator } from "./routeSourceValidator";
|
|
4
|
+
|
|
5
|
+
const validate = (source: string, kind: "page" | "layout", rootLayout = false) =>
|
|
6
|
+
RouteSourceValidator.validateRouteSourceExports(source, `page/_${kind}.tsx`, kind, { rootLayout });
|
|
7
|
+
|
|
8
|
+
describe("RouteSourceValidator", () => {
|
|
9
|
+
test("accepts every root-layout config export the route tree honors", () => {
|
|
10
|
+
const source = [
|
|
11
|
+
"export default function Layout() { return null; }",
|
|
12
|
+
"export const fonts = [];",
|
|
13
|
+
"export const manifest = {};",
|
|
14
|
+
'export const theme = "dark";',
|
|
15
|
+
"export const reconnect = true;",
|
|
16
|
+
"export const wsConnect = true;",
|
|
17
|
+
"export const layoutStyle = {};",
|
|
18
|
+
'export const gaTrackingId = "G-1";',
|
|
19
|
+
].join("\n");
|
|
20
|
+
|
|
21
|
+
expect(() => validate(source, "layout", true)).not.toThrow();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("rejects root-layout-only exports on a nested layout and on a page", () => {
|
|
25
|
+
const source = ["export default function Layout() { return null; }", "export const wsConnect = true;"].join("\n");
|
|
26
|
+
|
|
27
|
+
expect(() => validate(source, "layout")).toThrow('unsupported export "wsConnect"');
|
|
28
|
+
expect(() => validate(source, "page")).toThrow('unsupported export "wsConnect"');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("reads devOnly off pageConfig without evaluating the module", () => {
|
|
32
|
+
const source = [
|
|
33
|
+
"export default function Page() { return null; }",
|
|
34
|
+
"export const pageConfig = { devOnly: true };",
|
|
35
|
+
].join("\n");
|
|
36
|
+
|
|
37
|
+
expect(RouteSourceValidator.validateRouteSourceExports(source, "page/_index.tsx", "page")).toEqual({
|
|
38
|
+
devOnly: true,
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
});
|
package/routeSourceValidator.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { getRouteExports } from "akanjs/common";
|
|
1
2
|
import ts from "typescript";
|
|
2
3
|
|
|
3
4
|
/** What the build needs out of a route module without evaluating it. */
|
|
@@ -16,44 +17,6 @@ export interface RouteSourceInfo {
|
|
|
16
17
|
* validate a route source stay lean.
|
|
17
18
|
*/
|
|
18
19
|
export class RouteSourceValidator {
|
|
19
|
-
static readonly #pageExports = new Set([
|
|
20
|
-
"default",
|
|
21
|
-
"pageConfig",
|
|
22
|
-
"head",
|
|
23
|
-
"metadata",
|
|
24
|
-
"generateHead",
|
|
25
|
-
"generateMetadata",
|
|
26
|
-
"Loading",
|
|
27
|
-
]);
|
|
28
|
-
static readonly #rootLayoutExports = new Set([
|
|
29
|
-
"default",
|
|
30
|
-
"pageConfig",
|
|
31
|
-
"head",
|
|
32
|
-
"metadata",
|
|
33
|
-
"generateHead",
|
|
34
|
-
"generateMetadata",
|
|
35
|
-
"fonts",
|
|
36
|
-
"manifest",
|
|
37
|
-
"theme",
|
|
38
|
-
"reconnect",
|
|
39
|
-
"layoutStyle",
|
|
40
|
-
"gaTrackingId",
|
|
41
|
-
"Loading",
|
|
42
|
-
"NotFound",
|
|
43
|
-
"Error",
|
|
44
|
-
]);
|
|
45
|
-
static readonly #layoutExports = new Set([
|
|
46
|
-
"default",
|
|
47
|
-
"pageConfig",
|
|
48
|
-
"head",
|
|
49
|
-
"metadata",
|
|
50
|
-
"generateHead",
|
|
51
|
-
"generateMetadata",
|
|
52
|
-
"Loading",
|
|
53
|
-
"NotFound",
|
|
54
|
-
"Error",
|
|
55
|
-
]);
|
|
56
|
-
|
|
57
20
|
static validateRouteSourceExports(
|
|
58
21
|
source: string,
|
|
59
22
|
filePath: string,
|
|
@@ -61,12 +24,7 @@ export class RouteSourceValidator {
|
|
|
61
24
|
options: { rootLayout?: boolean } = {},
|
|
62
25
|
): RouteSourceInfo {
|
|
63
26
|
const sourceFile = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
64
|
-
const allowed =
|
|
65
|
-
kind === "page"
|
|
66
|
-
? RouteSourceValidator.#pageExports
|
|
67
|
-
: options.rootLayout
|
|
68
|
-
? RouteSourceValidator.#rootLayoutExports
|
|
69
|
-
: RouteSourceValidator.#layoutExports;
|
|
27
|
+
const allowed = getRouteExports(kind, { rootLayout: options.rootLayout });
|
|
70
28
|
const exported = new Set<string>();
|
|
71
29
|
const assertExport = (name: string) => {
|
|
72
30
|
if (!allowed.has(name)) {
|
package/scanInfo.ts
CHANGED
|
@@ -11,6 +11,7 @@ import type {
|
|
|
11
11
|
} from "./akanConfig";
|
|
12
12
|
|
|
13
13
|
import { AppExecutor, LibExecutor, PkgExecutor, WorkspaceExecutor } from "./executors";
|
|
14
|
+
import { appRootAllowedDirs, appRootAllowedFiles, libFacetRootAllowedFiles } from "./workspaceLayout";
|
|
14
15
|
|
|
15
16
|
const scalarFileTypes = ["constant", "dictionary", "document", "template", "unit", "util", "view", "zone"] as const;
|
|
16
17
|
type ScalarFileType = (typeof scalarFileTypes)[number];
|
|
@@ -43,49 +44,7 @@ type DatabaseFileType = (typeof databaseFileTypes)[number];
|
|
|
43
44
|
|
|
44
45
|
type ModuleKind = "database" | "service" | "scalar";
|
|
45
46
|
|
|
46
|
-
const appRootAllowedFiles = new Set([
|
|
47
|
-
// 스코프 에이전트 가이드 — scan(write) 이 유지하는 색인 + 마커 밖 hand-written 내용 (agentsIndex.ts)
|
|
48
|
-
"AGENTS.md",
|
|
49
|
-
"CLAUDE.md",
|
|
50
|
-
"akan.app.json",
|
|
51
|
-
"akan.config.ts",
|
|
52
|
-
"capacitor.config.ts",
|
|
53
|
-
"client.ts",
|
|
54
|
-
"main.ts",
|
|
55
|
-
"package.json",
|
|
56
|
-
"server.ts",
|
|
57
|
-
"tsconfig.json",
|
|
58
|
-
"tsconfig.tsbuildinfo",
|
|
59
|
-
]);
|
|
60
47
|
const generatedRootCapacitorConfigFiles = ["capacitor.config.js", "capacitor.config.json"] as const;
|
|
61
|
-
const appRootAllowedDirs = new Set([
|
|
62
|
-
".akan",
|
|
63
|
-
"android",
|
|
64
|
-
"env",
|
|
65
|
-
"ios",
|
|
66
|
-
"lib",
|
|
67
|
-
"mobile",
|
|
68
|
-
"page",
|
|
69
|
-
"private",
|
|
70
|
-
"public",
|
|
71
|
-
"script",
|
|
72
|
-
"ui",
|
|
73
|
-
"srvkit",
|
|
74
|
-
"webkit",
|
|
75
|
-
"common",
|
|
76
|
-
"secrets",
|
|
77
|
-
]);
|
|
78
|
-
const libRootAllowedFiles = new Set([
|
|
79
|
-
"cnst.ts",
|
|
80
|
-
"db.ts",
|
|
81
|
-
"dict.ts",
|
|
82
|
-
"option.ts",
|
|
83
|
-
"sig.ts",
|
|
84
|
-
"srv.ts",
|
|
85
|
-
"st.ts",
|
|
86
|
-
"useClient.ts",
|
|
87
|
-
"useServer.ts",
|
|
88
|
-
]);
|
|
89
48
|
const internalLibDirs = new Set(["__lib", "__scalar"]);
|
|
90
49
|
const moduleNonUiFileTypes = {
|
|
91
50
|
database: new Set(["constant", "dictionary", "document", "service", "signal", "store"]),
|
|
@@ -107,7 +66,7 @@ const createDependencyScanner = async (exec: AppExecutor | LibExecutor | PkgExec
|
|
|
107
66
|
|
|
108
67
|
const isAllowedTestFile = (filename: string) => testFilePattern.test(filename);
|
|
109
68
|
const isAllowedLibRootFile = (filename: string) =>
|
|
110
|
-
|
|
69
|
+
libFacetRootAllowedFiles.has(filename) || rootSignalTestFilePattern.test(filename);
|
|
111
70
|
const getScanPath = (exec: AppExecutor | LibExecutor, relativePath: string) =>
|
|
112
71
|
path.posix.join(`${exec.type}s`, exec.name, relativePath.split(path.sep).join("/"));
|
|
113
72
|
async function clearGeneratedRootCapacitorConfigs(exec: AppExecutor | LibExecutor) {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { appRootAllowedDirs, appRootAllowedFiles, isScannedAppRootEntry } from "./workspaceLayout";
|
|
3
|
+
|
|
4
|
+
describe("app root layout allowlist", () => {
|
|
5
|
+
test("admits the scoped agent guides sync writes into every app", () => {
|
|
6
|
+
expect(appRootAllowedFiles.has("AGENTS.md")).toBe(true);
|
|
7
|
+
expect(appRootAllowedFiles.has("CLAUDE.md")).toBe(true);
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("admits every documented app root folder", () => {
|
|
11
|
+
for (const dirname of ["mobile", "plugin", "secrets", "srvkit", "webkit"]) {
|
|
12
|
+
expect(appRootAllowedDirs.has(dirname)).toBe(true);
|
|
13
|
+
}
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("rejects an app root entry no facet owns", () => {
|
|
17
|
+
expect(appRootAllowedFiles.has("helper.ts")).toBe(false);
|
|
18
|
+
expect(appRootAllowedDirs.has("base")).toBe(false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("skips dotfile artifacts the sync glob never sees, but keeps .akan", () => {
|
|
22
|
+
expect(isScannedAppRootEntry(".DS_Store")).toBe(false);
|
|
23
|
+
expect(isScannedAppRootEntry(".akan")).toBe(true);
|
|
24
|
+
expect(isScannedAppRootEntry("lib")).toBe(true);
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App/lib 루트 레이아웃 허용 목록 — 단일 소스.
|
|
3
|
+
*
|
|
4
|
+
* 같은 규칙을 scanInfo(`akan sync`, hard error) · akanContext(`akan doctor`, diagnostic) ·
|
|
5
|
+
* qualityScanner(`akan quality scan`, warning) 세 곳이 각자 복사해 두면서 실제로 어긋났다
|
|
6
|
+
* (스코프 AGENTS.md/CLAUDE.md 는 sync 만 허용, `plugin` 은 문서에만, `secrets` 는 doctor 만 거부).
|
|
7
|
+
* 규칙을 추가할 때는 이 파일만 고치고, 루트 AGENTS.md 의 목록도 같이 갱신한다.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
export const appRootAllowedFiles = new Set([
|
|
11
|
+
// 스코프 에이전트 가이드 — scan(write) 이 유지하는 색인 + 마커 밖 hand-written 내용 (agentsIndex.ts)
|
|
12
|
+
"AGENTS.md",
|
|
13
|
+
"CLAUDE.md",
|
|
14
|
+
"akan.app.json",
|
|
15
|
+
"akan.config.ts",
|
|
16
|
+
"capacitor.config.ts",
|
|
17
|
+
"client.ts",
|
|
18
|
+
"main.ts",
|
|
19
|
+
"package.json",
|
|
20
|
+
"server.ts",
|
|
21
|
+
"tsconfig.json",
|
|
22
|
+
"tsconfig.tsbuildinfo",
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const appRootAllowedDirs = new Set([
|
|
26
|
+
".akan",
|
|
27
|
+
"android",
|
|
28
|
+
"common",
|
|
29
|
+
"env",
|
|
30
|
+
"ios",
|
|
31
|
+
"lib",
|
|
32
|
+
"mobile",
|
|
33
|
+
"page",
|
|
34
|
+
"plugin",
|
|
35
|
+
"private",
|
|
36
|
+
"public",
|
|
37
|
+
"script",
|
|
38
|
+
"secrets",
|
|
39
|
+
"srvkit",
|
|
40
|
+
"ui",
|
|
41
|
+
"webkit",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
export const libFacetRootAllowedFiles = new Set([
|
|
45
|
+
"cnst.ts",
|
|
46
|
+
"db.ts",
|
|
47
|
+
"dict.ts",
|
|
48
|
+
"option.ts",
|
|
49
|
+
"sig.ts",
|
|
50
|
+
"srv.ts",
|
|
51
|
+
"st.ts",
|
|
52
|
+
"useClient.ts",
|
|
53
|
+
"useServer.ts",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* scanSync 는 앱 루트를 `Bun.Glob("*")` 로 읽어 dotfile 을 아예 보지 못한다. 디렉터리를 직접 읽는
|
|
58
|
+
* doctor 가 그 차이만큼 `.DS_Store` 같은 툴 산출물을 에러로 올리므로 같은 기준으로 걸러낸다.
|
|
59
|
+
*/
|
|
60
|
+
export const isScannedAppRootEntry = (name: string) => !name.startsWith(".") || appRootAllowedDirs.has(name);
|