arkgate 2.6.0 → 2.7.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.
- package/CHANGELOG.md +97 -0
- package/README.md +98 -70
- package/bin/ark-check.mjs +240 -1001
- package/bin/ark-layer-match.mjs +153 -147
- package/bin/ark-mcp.mjs +102 -5
- package/bin/ark-shared.mjs +304 -165
- package/bin/ark.mjs +44 -34
- package/bin/lib/agent-gates.mjs +448 -15
- package/bin/lib/architecture-scan.mjs +279 -0
- package/bin/lib/ast-scan.mjs +199 -0
- package/bin/lib/baseline-key.mjs +23 -0
- package/bin/lib/config-warnings.mjs +228 -0
- package/bin/lib/doctor-plan.mjs +11 -4
- package/bin/lib/graph-cycles.mjs +56 -0
- package/bin/lib/presets.mjs +75 -4
- package/bin/lib/remediation.mjs +150 -0
- package/bin/lib/scan-files.mjs +69 -0
- package/bin/lib/ts-resolve.mjs +215 -0
- package/bin/lib/violations.mjs +3 -9
- package/dist/eslint/index.cjs +21 -3
- package/dist/eslint/index.cjs.map +1 -1
- package/dist/eslint/index.d.cts +5 -3
- package/dist/eslint/index.d.ts +5 -3
- package/dist/eslint/index.js +21 -3
- package/dist/eslint/index.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.cjs +1 -1
- package/dist/nestjs/index.cjs.map +1 -1
- package/dist/nestjs/index.d.cts +1 -1
- package/dist/nestjs/index.d.ts +1 -1
- package/dist/nestjs/index.js +1 -1
- package/dist/nestjs/index.js.map +1 -1
- package/dist/runtime/index.cjs +3080 -0
- package/dist/runtime/index.cjs.map +1 -0
- package/dist/runtime/index.d.cts +2 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2998 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/{types-DpdVN7Lm.d.cts → types-CP3KkwZt.d.cts} +1 -1
- package/dist/{types-DpdVN7Lm.d.ts → types-CP3KkwZt.d.ts} +1 -1
- package/docs/agent-guide.md +67 -1
- package/docs/migrate-from-ark-runtime-kernel.md +4 -2
- package/docs/package-surface.md +72 -0
- package/docs/production-hardening.md +3 -0
- package/package.json +11 -1
- package/server.json +2 -2
- package/templates/skills/ark-adopt.md +43 -87
- package/templates/skills/ark-autopilot.md +39 -77
- package/templates/skills/ark-contract.md +43 -84
- package/templates/skills/ark-coverage.md +62 -83
- package/templates/skills/ark-fix.md +45 -90
- package/templates/skills/ark-loop.md +44 -66
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript module resolution + per-file scan cache for ark-check.
|
|
3
|
+
* Extracted from ark-check entry (R3).
|
|
4
|
+
*/
|
|
5
|
+
import crypto from 'node:crypto';
|
|
6
|
+
import fs from 'node:fs';
|
|
7
|
+
import path from 'node:path';
|
|
8
|
+
|
|
9
|
+
export function createModuleResolutionHost(ts) {
|
|
10
|
+
const sys = ts?.sys;
|
|
11
|
+
const fileExists = (f) => {
|
|
12
|
+
if (sys?.fileExists) return sys.fileExists(f);
|
|
13
|
+
return fs.existsSync(f);
|
|
14
|
+
};
|
|
15
|
+
const readFile = (f) => {
|
|
16
|
+
if (sys?.readFile) return sys.readFile(f);
|
|
17
|
+
try {
|
|
18
|
+
return fs.readFileSync(f, 'utf8');
|
|
19
|
+
} catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const directoryExists = (d) => {
|
|
24
|
+
if (sys?.directoryExists) return sys.directoryExists(d);
|
|
25
|
+
try {
|
|
26
|
+
return fs.statSync(d).isDirectory();
|
|
27
|
+
} catch {
|
|
28
|
+
return false;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
fileExists,
|
|
33
|
+
readFile,
|
|
34
|
+
directoryExists,
|
|
35
|
+
getCurrentDirectory: () =>
|
|
36
|
+
sys?.getCurrentDirectory ? sys.getCurrentDirectory() : process.cwd(),
|
|
37
|
+
getDirectories: (d) => {
|
|
38
|
+
if (sys?.getDirectories) return sys.getDirectories(d);
|
|
39
|
+
try {
|
|
40
|
+
return fs
|
|
41
|
+
.readdirSync(d, { withFileTypes: true })
|
|
42
|
+
.filter((e) => e.isDirectory())
|
|
43
|
+
.map((e) => e.name);
|
|
44
|
+
} catch {
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
realpath: sys?.realpath ? (p) => sys.realpath(p) : undefined,
|
|
49
|
+
useCaseSensitiveFileNames: sys?.useCaseSensitiveFileNames ?? true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseTsconfig(ts, configPath) {
|
|
54
|
+
const host = createModuleResolutionHost(ts);
|
|
55
|
+
const read = ts.readConfigFile(configPath, host.readFile);
|
|
56
|
+
if (read.error) return {};
|
|
57
|
+
// parseJsonConfigFileContent wants a ParseConfigHost-like object; our resolution host
|
|
58
|
+
// is enough for option extraction.
|
|
59
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
60
|
+
read.config,
|
|
61
|
+
{
|
|
62
|
+
useCaseSensitiveFileNames: host.useCaseSensitiveFileNames,
|
|
63
|
+
readDirectory: ts.sys?.readDirectory
|
|
64
|
+
? (...args) => ts.sys.readDirectory(...args)
|
|
65
|
+
: () => [],
|
|
66
|
+
fileExists: host.fileExists,
|
|
67
|
+
readFile: host.readFile,
|
|
68
|
+
},
|
|
69
|
+
path.dirname(configPath)
|
|
70
|
+
);
|
|
71
|
+
return parsed.options;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Compiler options for a given source file. With --tsconfig every file uses that one
|
|
76
|
+
* config; otherwise each file uses the NEAREST tsconfig.json above it (like tsc does),
|
|
77
|
+
* so monorepo packages with per-package path aliases resolve correctly under one --root.
|
|
78
|
+
*/
|
|
79
|
+
export function createCompilerOptionsLookup(ts, root, tsconfigArg) {
|
|
80
|
+
if (tsconfigArg) {
|
|
81
|
+
const configPath = path.isAbsolute(tsconfigArg) ? tsconfigArg : path.join(root, tsconfigArg);
|
|
82
|
+
const options = fs.existsSync(configPath) ? parseTsconfig(ts, configPath) : {};
|
|
83
|
+
return () => options;
|
|
84
|
+
}
|
|
85
|
+
const byDir = new Map();
|
|
86
|
+
const byConfig = new Map();
|
|
87
|
+
return (file) => {
|
|
88
|
+
const dir = path.dirname(file);
|
|
89
|
+
if (byDir.has(dir)) return byDir.get(dir);
|
|
90
|
+
const configPath = ts.findConfigFile(dir, ts.sys.fileExists, 'tsconfig.json');
|
|
91
|
+
let options = {};
|
|
92
|
+
if (configPath) {
|
|
93
|
+
if (!byConfig.has(configPath)) byConfig.set(configPath, parseTsconfig(ts, configPath));
|
|
94
|
+
options = byConfig.get(configPath);
|
|
95
|
+
}
|
|
96
|
+
byDir.set(dir, options);
|
|
97
|
+
return options;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Per-file scan cache. A cache entry stores the parsed file's content-derived results:
|
|
103
|
+
* content violations (forbidden globals, publish checks, intent references) and the list
|
|
104
|
+
* of module-edge specifiers. Edges are NEVER cached as violations — they are re-resolved
|
|
105
|
+
* against the live filesystem every run, because resolution depends on files and tsconfigs
|
|
106
|
+
* outside the cached file. The whole cache is keyed by the config+manifest contents, so
|
|
107
|
+
* any rule change invalidates everything.
|
|
108
|
+
*/
|
|
109
|
+
export function scanCachePath(root) {
|
|
110
|
+
return path.join(root, 'node_modules', '.cache', 'ark-check.json');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function scanCacheKey(root, args) {
|
|
114
|
+
const read = (p) => {
|
|
115
|
+
try {
|
|
116
|
+
return fs.readFileSync(p, 'utf8');
|
|
117
|
+
} catch {
|
|
118
|
+
return '';
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
const configPath = path.isAbsolute(args.config) ? args.config : path.join(root, args.config);
|
|
122
|
+
const manifestPath = args.manifest
|
|
123
|
+
? path.isAbsolute(args.manifest)
|
|
124
|
+
? args.manifest
|
|
125
|
+
: path.join(root, args.manifest)
|
|
126
|
+
: undefined;
|
|
127
|
+
// Bump this schema tag whenever the cached scan shape changes, so a warm cache from an
|
|
128
|
+
// older Ark can't feed stale entries to new logic. v2: typeOnly on edges. v3: per-file
|
|
129
|
+
// exportsOnlyTypes (target-module type-only export detection for plan classifier).
|
|
130
|
+
return crypto
|
|
131
|
+
.createHash('sha1')
|
|
132
|
+
.update(`ark-check-cache-v3\0${read(configPath)}\0${manifestPath ? read(manifestPath) : ''}`)
|
|
133
|
+
.digest('hex');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function loadScanCache(root, key) {
|
|
137
|
+
try {
|
|
138
|
+
const data = JSON.parse(fs.readFileSync(scanCachePath(root), 'utf8'));
|
|
139
|
+
return data.key === key && data.files && typeof data.files === 'object' ? data.files : undefined;
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function saveScanCache(root, key, files) {
|
|
146
|
+
try {
|
|
147
|
+
const target = scanCachePath(root);
|
|
148
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
149
|
+
fs.writeFileSync(target, JSON.stringify({ key, files }));
|
|
150
|
+
} catch {
|
|
151
|
+
// cache is best-effort: read-only filesystems just re-parse every run
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Fallback resolver for extensionless relative imports whose on-disk target uses an
|
|
157
|
+
* extension `ts.resolveModuleName` won't resolve without a matching tsconfig
|
|
158
|
+
* (notably `.mts`/`.cts`). Mirrors the classic candidate list.
|
|
159
|
+
*/
|
|
160
|
+
export function isFile(candidate) {
|
|
161
|
+
try {
|
|
162
|
+
return fs.statSync(candidate).isFile();
|
|
163
|
+
} catch {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function resolveRelativeFallback(fromFile, specifier) {
|
|
169
|
+
const base = path.resolve(path.dirname(fromFile), specifier);
|
|
170
|
+
const candidates = [
|
|
171
|
+
base, // only used when the specifier already carries an extension (isFile filters dirs)
|
|
172
|
+
`${base}.ts`,
|
|
173
|
+
`${base}.tsx`,
|
|
174
|
+
`${base}.mts`,
|
|
175
|
+
`${base}.cts`,
|
|
176
|
+
`${base}.js`,
|
|
177
|
+
`${base}.jsx`,
|
|
178
|
+
`${base}.mjs`,
|
|
179
|
+
`${base}.cjs`,
|
|
180
|
+
path.join(base, 'index.ts'),
|
|
181
|
+
path.join(base, 'index.tsx'),
|
|
182
|
+
path.join(base, 'index.mts'),
|
|
183
|
+
path.join(base, 'index.cts'),
|
|
184
|
+
];
|
|
185
|
+
// isFile (not existsSync) so a directory named like the specifier never shadows the
|
|
186
|
+
// real module file — e.g. `./foo` must not resolve to a `foo/` directory before `foo.mts`.
|
|
187
|
+
return candidates.find(isFile);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Resolve any import specifier (relative, tsconfig path-alias, or package) to a source
|
|
192
|
+
* file using TypeScript's module resolver, returning the resolved file (or undefined for
|
|
193
|
+
* unresolved / declaration-only targets).
|
|
194
|
+
*
|
|
195
|
+
* ark-check governs one project rooted at --root. A resolved target is skipped when its
|
|
196
|
+
* path RELATIVE TO ROOT either escapes the root (leading `..`) or contains a `node_modules`
|
|
197
|
+
* segment. Using the root-relative path (not an absolute substring) means a project that
|
|
198
|
+
* itself lives under a node_modules segment is still governed, while a broad catch-all
|
|
199
|
+
* pattern (`**`) can't false-flag vendored deps or files outside the project. Monorepos can
|
|
200
|
+
* run under a single --root (per-package tsconfigs are honored via the nearest-tsconfig
|
|
201
|
+
* lookup); edges that resolve outside the root are still skipped.
|
|
202
|
+
*/
|
|
203
|
+
export function resolveImport(ts, specifier, containingFile, options, host, root) {
|
|
204
|
+
const res = ts.resolveModuleName(specifier, containingFile, options, host);
|
|
205
|
+
let file = res.resolvedModule?.resolvedFileName;
|
|
206
|
+
if (!file && specifier.startsWith('.')) {
|
|
207
|
+
file = resolveRelativeFallback(containingFile, specifier);
|
|
208
|
+
}
|
|
209
|
+
if (!file) return undefined;
|
|
210
|
+
if (file.endsWith('.d.ts')) return undefined;
|
|
211
|
+
const abs = path.resolve(file);
|
|
212
|
+
const segments = path.relative(root, abs).split(path.sep);
|
|
213
|
+
if (segments[0] === '..' || segments.includes('node_modules')) return undefined;
|
|
214
|
+
return abs;
|
|
215
|
+
}
|
package/bin/lib/violations.mjs
CHANGED
|
@@ -10,15 +10,9 @@ const color = {
|
|
|
10
10
|
bold: (s) => (useColor ? `\x1b[1m${s}\x1b[0m` : s),
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
violation.file,
|
|
17
|
-
violation.fromLayer ?? '',
|
|
18
|
-
violation.toLayer ?? '',
|
|
19
|
-
violation.target ?? '',
|
|
20
|
-
].join('|');
|
|
21
|
-
}
|
|
13
|
+
/** Canonical: src/domain/baselineKey.ts → bin/lib/baseline-key.mjs (R4). */
|
|
14
|
+
import { baselineKey } from './baseline-key.mjs';
|
|
15
|
+
export { baselineKey };
|
|
22
16
|
|
|
23
17
|
export function readBaseline(root, baselinePath) {
|
|
24
18
|
const fullPath = path.isAbsolute(baselinePath) ? baselinePath : path.join(root, baselinePath);
|
package/dist/eslint/index.cjs
CHANGED
|
@@ -53,6 +53,24 @@ var regexpCache = /* @__PURE__ */ new Map();
|
|
|
53
53
|
function escapeLiteral(ch) {
|
|
54
54
|
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
55
55
|
}
|
|
56
|
+
function normalizeGlobSeparators(pattern) {
|
|
57
|
+
let out = "";
|
|
58
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
59
|
+
const c = pattern[i];
|
|
60
|
+
if (c === "\\" && i + 1 < pattern.length) {
|
|
61
|
+
const next = pattern[i + 1];
|
|
62
|
+
if ("*?{}[],".includes(next) || next === "\\") {
|
|
63
|
+
out += "\\" + next;
|
|
64
|
+
i += 1;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out += "/";
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
out += c;
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
56
74
|
function bracesBalanced(glob) {
|
|
57
75
|
let depth = 0;
|
|
58
76
|
for (let i = 0; i < glob.length; i += 1) {
|
|
@@ -72,7 +90,7 @@ function bracesBalanced(glob) {
|
|
|
72
90
|
function globToRegExp(pattern) {
|
|
73
91
|
const cached = regexpCache.get(pattern);
|
|
74
92
|
if (cached) return cached;
|
|
75
|
-
const glob = pattern
|
|
93
|
+
const glob = normalizeGlobSeparators(pattern);
|
|
76
94
|
const useBraces = bracesBalanced(glob);
|
|
77
95
|
let out = "";
|
|
78
96
|
let braceDepth = 0;
|
|
@@ -112,14 +130,14 @@ function globToRegExp(pattern) {
|
|
|
112
130
|
return re;
|
|
113
131
|
}
|
|
114
132
|
function patternSpecificity(pattern) {
|
|
115
|
-
const glob = String(pattern)
|
|
133
|
+
const glob = normalizeGlobSeparators(String(pattern));
|
|
116
134
|
const beforeWildcard = glob.split("*")[0];
|
|
117
135
|
const literalSegments = beforeWildcard.split("/").filter(Boolean).length;
|
|
118
136
|
const literalLength = glob.replace(/\*/g, "").length;
|
|
119
137
|
return literalSegments * 1e4 + literalLength;
|
|
120
138
|
}
|
|
121
139
|
function layerForRelativePath(relPath, layers) {
|
|
122
|
-
const rel = String(relPath).split(
|
|
140
|
+
const rel = String(relPath).split(/[/\\]/).join("/");
|
|
123
141
|
let bestName;
|
|
124
142
|
let bestScore = -1;
|
|
125
143
|
for (const layer of layers ?? []) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/eslint/index.ts","../../src/domain/layerMatch.ts"],"sourcesContent":["/**\n * arkgate/eslint — editor-side architecture gate.\n *\n * Layer / import / forbidden-globals rules load `ark.config.json` from the linted\n * project (walk-up from the file) and use the same glob specificity + edge semantics\n * as ark-check. Matching primitives come from `bin/ark-layer-match.mjs` (bundled) so\n * CLI and editor share one implementation — no Kernel imports.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n globToRegExp,\n patternSpecificity,\n layerForRelativePath,\n isEdgeDenied,\n} from '../domain/layerMatch';\n\nexport { globToRegExp, patternSpecificity, layerForRelativePath, isEdgeDenied };\n\ntype RuleContext = {\n report(descriptor: Record<string, unknown>): void;\n /** ESLint 9+ / 10: preferred path on the context object. */\n filename?: string;\n /** ESLint 8-style physical path when linting with processors / virtual files. */\n physicalFilename?: string;\n /** ESLint ≤8 API — still present on some hosts; removed in ESLint 10. */\n getFilename?: () => string;\n options?: unknown[];\n};\n\n/** Resolve the file path being linted across ESLint 8–10 context shapes. */\nfunction lintedFilename(context: RuleContext): string {\n if (typeof context.physicalFilename === 'string' && context.physicalFilename.length > 0) {\n return context.physicalFilename;\n }\n if (typeof context.filename === 'string' && context.filename.length > 0) {\n return context.filename;\n }\n if (typeof context.getFilename === 'function') {\n try {\n const name = context.getFilename();\n if (typeof name === 'string' && name.length > 0) return name;\n } catch {\n /* ignore */\n }\n }\n return '';\n}\n\ntype RuleListener = Record<string, (node: AstNode) => void>;\n\ntype AstNode = {\n type?: string;\n name?: string;\n value?: unknown;\n source?: AstNode;\n callee?: AstNode;\n object?: AstNode;\n property?: AstNode;\n key?: AstNode;\n arguments?: AstNode[];\n properties?: AstNode[];\n importKind?: string;\n specifiers?: AstNode[];\n};\n\ntype ArkRule = {\n meta: {\n type: 'problem';\n docs: { description: string };\n messages: Record<string, string>;\n schema: unknown[];\n };\n create(context: RuleContext): RuleListener;\n};\n\ntype ArkEslintPlugin = {\n rules: Record<string, ArkRule>;\n configs?: Record<string, unknown>;\n};\n\ntype LayerConfig = {\n name: string;\n patterns?: string[];\n exclude?: string[];\n forbiddenGlobals?: string[];\n};\n\ntype EdgeRule = { from: string; to: string; allowed?: boolean };\n\ntype ArkConfig = {\n layers?: LayerConfig[];\n rules?: EdgeRule[];\n};\n\n// ── Config I/O (editor-only; matching primitives come from ark-layer-match.mjs) ──\n\nexport function findConfigPath(startFile: string): string | null {\n if (!startFile || startFile === '<input>' || startFile.startsWith('stdin')) return null;\n let dir = path.dirname(path.resolve(startFile));\n for (;;) {\n const candidate = path.join(dir, 'ark.config.json');\n if (fs.existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nconst _configCache = new Map<string, ArkConfig | null>();\n\nexport function loadArkConfig(configPath: string): ArkConfig | null {\n if (_configCache.has(configPath)) return _configCache.get(configPath) ?? null;\n try {\n const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as ArkConfig;\n _configCache.set(configPath, raw);\n return raw;\n } catch {\n _configCache.set(configPath, null);\n return null;\n }\n}\n\n/** Resolve relative import specifier to an absolute path candidate (TS-oriented). */\nexport function resolveRelativeImport(fromFile: string, specifier: string): string | null {\n if (!specifier.startsWith('.')) return null;\n const base = path.resolve(path.dirname(fromFile), specifier);\n const candidates = [\n base,\n `${base}.ts`,\n `${base}.tsx`,\n `${base}.mts`,\n `${base}.cts`,\n `${base}.js`,\n `${base}.jsx`,\n path.join(base, 'index.ts'),\n path.join(base, 'index.tsx'),\n path.join(base, 'index.js'),\n ];\n for (const c of candidates) {\n try {\n if (fs.existsSync(c) && fs.statSync(c).isFile()) return c;\n } catch {\n /* continue */\n }\n }\n // Prefer .ts for layer matching when the target is not on disk yet (editor typing).\n return `${base}.ts`;\n}\n\n// ── AST helpers ────────────────────────────────────────────────────────────\n\nfunction stringValue(node: AstNode | undefined): string | undefined {\n return typeof node?.value === 'string' ? node.value : undefined;\n}\n\nfunction propertyName(node: AstNode | undefined): string | undefined {\n return node?.name ?? stringValue(node);\n}\n\nfunction calleePropertyName(node: AstNode): string | undefined {\n return propertyName(node.callee?.property);\n}\n\nfunction objectProperty(node: AstNode | undefined, name: string): AstNode | undefined {\n return node?.properties?.find((property) => propertyName(property.key) === name);\n}\n\nfunction objectHasProperty(node: AstNode | undefined, name: string): boolean {\n return objectProperty(node, name) !== undefined;\n}\n\nfunction objectHasMetadataSource(node: AstNode | undefined): boolean {\n const metadata = objectProperty(node, 'metadata')?.value as AstNode | undefined;\n return objectHasProperty(metadata, 'source');\n}\n\nfunction looksLikeIntent(value: string): boolean {\n return /^(Domain|Application|Adapter|Workflow|Job|Presentation|Reporting|Metadata|Security|Audit|Observability|Kernel)\\.[A-Za-z0-9_.]+$/.test(\n value\n );\n}\n\nfunction isPublishCall(node: AstNode): boolean {\n return calleePropertyName(node) === 'publish';\n}\n\n/** Heuristic fallback when no ark.config.json (pre-contract projects). */\nfunction isDomainFileHeuristic(filename: string): boolean {\n const normalized = filename.split('\\\\').join('/').toLowerCase();\n return normalized.includes('/domain/') || normalized.endsWith('/domain.ts');\n}\n\nfunction isInfraImportHeuristic(specifier: string): boolean {\n const normalized = specifier.toLowerCase();\n return [\n 'adapter',\n 'adapters',\n 'infrastructure',\n 'persistence',\n 'repository',\n 'repositories',\n 'integration',\n 'database',\n 'db',\n ].some((token) => normalized.includes(token));\n}\n\nconst DEFAULT_FORBIDDEN_GLOBALS = ['fetch', 'process', 'Date.now', 'Math.random'];\n\n// ── Rules ──────────────────────────────────────────────────────────────────\n\n/**\n * Config-driven layer import boundary (primary editor gate).\n * Replaces path-token domain/infra heuristics when ark.config.json is present.\n * Rule id kept as `no-domain-infra-imports` for recommended-config / upgrade stability.\n */\nexport const noDomainInfraImports: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow imports that violate ark.config.json layer rules (same contract as arkgate-check). Falls back to domain→infra path heuristics when no config is found.',\n },\n messages: {\n forbiddenImport:\n 'Architecture: {{fromLayer}} must not import {{toLayer}} (ark.config.json). Specifier: {{specifier}}',\n forbiddenImportHeuristic:\n 'Domain code must not import infrastructure, adapters, repositories, or database modules.',\n },\n schema: [],\n },\n create(context) {\n const filename = lintedFilename(context);\n const configPath = findConfigPath(filename);\n const config = configPath ? loadArkConfig(configPath) : null;\n const root = configPath ? path.dirname(configPath) : null;\n\n const check = (node: AstNode) => {\n const source = stringValue(node.source);\n if (!source) return;\n\n if (config && root && filename) {\n const absFile = path.isAbsolute(filename) ? filename : path.resolve(filename);\n const relFile = path.relative(root, absFile).split(path.sep).join('/');\n const fromLayer = layerForRelativePath(relFile, config.layers);\n if (!fromLayer) return;\n\n const targetAbs = resolveRelativeImport(absFile, source);\n if (!targetAbs) return; // package import — CI resolves via TS; editor skips non-relative\n\n const relTarget = path.relative(root, targetAbs).split(path.sep).join('/');\n // Outside project or up-and-out: skip\n if (relTarget.startsWith('..')) return;\n\n const toLayer = layerForRelativePath(relTarget, config.layers);\n if (!toLayer) return;\n if (isEdgeDenied(config.rules, fromLayer, toLayer)) {\n context.report({\n node,\n messageId: 'forbiddenImport',\n data: { fromLayer, toLayer, specifier: source },\n });\n }\n return;\n }\n\n // No contract: legacy heuristic so bare domain folders still get a signal.\n if (isDomainFileHeuristic(filename) && isInfraImportHeuristic(source)) {\n context.report({ node, messageId: 'forbiddenImportHeuristic' });\n }\n };\n\n return {\n ImportDeclaration: check,\n ExportNamedDeclaration: check,\n ExportAllDeclaration: check,\n };\n },\n};\n\nexport const noRawEventPublish: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require event bus publish calls to use registered intent creators instead of raw event objects or intent strings.',\n },\n messages: {\n rawPublish:\n 'Publish through a registered intent creator; raw event objects or intent strings bypass Ark contracts.',\n },\n schema: [],\n },\n create(context) {\n return {\n CallExpression(node) {\n if (!isPublishCall(node)) return;\n const firstArg = node.arguments?.[0];\n const firstValue = stringValue(firstArg);\n if ((firstValue && looksLikeIntent(firstValue)) || objectHasProperty(firstArg, 'intent')) {\n context.report({ node, messageId: 'rawPublish' });\n }\n },\n };\n },\n};\n\nexport const requirePublishSource: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require event bus publish calls to include source metadata.',\n },\n messages: {\n missingSource: 'Strict Ark publish calls must include metadata.source.',\n },\n schema: [],\n },\n create(context) {\n return {\n CallExpression(node) {\n if (!isPublishCall(node)) return;\n const firstArg = node.arguments?.[0];\n const metadataArg = node.arguments?.[2];\n if (objectHasMetadataSource(firstArg) || objectHasProperty(metadataArg, 'source')) {\n return;\n }\n context.report({ node, messageId: 'missingSource' });\n },\n };\n },\n};\n\nexport const noForbiddenGlobals: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow ambient globals from the layer’s forbiddenGlobals in ark.config.json (same purity surface as arkgate-check). Option `globals` overrides. Without config, defaults apply only on domain-like paths.',\n },\n messages: {\n forbiddenGlobal:\n 'Ambient global \"{{name}}\" is forbidden in {{layer}} (ark.config.json); inject the capability through a port instead.',\n forbiddenGlobalDefault:\n 'Ambient global \"{{name}}\" is forbidden here; inject the capability through a port instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n globals: { type: 'array', items: { type: 'string' } },\n },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const filename = lintedFilename(context);\n const option = context.options?.[0] as { globals?: string[] } | undefined;\n const configPath = findConfigPath(filename);\n const config = configPath ? loadArkConfig(configPath) : null;\n const root = configPath ? path.dirname(configPath) : null;\n\n let globals: Set<string> | null = null;\n let layerName = 'this layer';\n\n if (option?.globals) {\n globals = new Set(option.globals);\n } else if (config && root && filename) {\n const absFile = path.isAbsolute(filename) ? filename : path.resolve(filename);\n const relFile = path.relative(root, absFile).split(path.sep).join('/');\n const layer = config.layers?.find(\n (l) => l.name === layerForRelativePath(relFile, config.layers)\n );\n if (layer?.forbiddenGlobals?.length) {\n globals = new Set(layer.forbiddenGlobals);\n layerName = layer.name;\n } else {\n // Layer has no purity list — do not invent defaults (matches CI).\n globals = null;\n }\n } else if (isDomainFileHeuristic(filename)) {\n globals = new Set(DEFAULT_FORBIDDEN_GLOBALS);\n }\n\n if (!globals) {\n return {} as RuleListener;\n }\n\n const report = (node: AstNode, name: string) =>\n context.report({\n node,\n messageId: config ? 'forbiddenGlobal' : 'forbiddenGlobalDefault',\n data: { name, layer: layerName },\n });\n\n return {\n MemberExpression(node) {\n const base = node.object?.type === 'Identifier' ? node.object.name : undefined;\n if (!base) return;\n const dotted = `${base}.${propertyName(node.property) ?? ''}`;\n if (globals!.has(dotted)) report(node, dotted);\n else if (globals!.has(base)) report(node, base);\n },\n CallExpression(node) {\n const callee = node.callee?.type === 'Identifier' ? node.callee.name : undefined;\n if (callee && globals!.has(callee)) report(node, callee);\n },\n NewExpression(node) {\n const callee = node.callee?.type === 'Identifier' ? node.callee.name : undefined;\n if (callee && globals!.has(callee)) report(node, callee);\n },\n };\n },\n};\n\nconst rules = {\n 'no-domain-infra-imports': noDomainInfraImports,\n 'no-raw-event-publish': noRawEventPublish,\n 'require-publish-source': requirePublishSource,\n 'no-forbidden-globals': noForbiddenGlobals,\n};\n\nconst plugin: ArkEslintPlugin = { rules };\n\nplugin.configs = {\n recommended: {\n plugins: { ark: plugin },\n rules: {\n 'ark/no-domain-infra-imports': 'error',\n 'ark/no-raw-event-publish': 'error',\n 'ark/require-publish-source': 'error',\n 'ark/no-forbidden-globals': 'error',\n },\n },\n};\n\nexport { plugin };\nexport default plugin;\n","/**\n * Pure layer-glob matching for ark.config.json.\n * Single TypeScript source of truth for the library (eslint / kernel consumers).\n * CLI re-exports the identical algorithm from `bin/ark-layer-match.mjs` — keep in\n * lockstep via tests/unit/static-check/layerMatchParity.test.ts.\n */\n\nexport type LayerConfig = {\n name: string;\n patterns?: string[];\n exclude?: string[];\n forbiddenGlobals?: string[];\n};\n\nexport type EdgeRule = { from: string; to: string; allowed?: boolean };\n\nconst regexpCache = new Map<string, RegExp>();\n\nfunction escapeLiteral(ch: string): string {\n return /[.*+?^${}()|[\\]\\\\]/.test(ch) ? `\\\\${ch}` : ch;\n}\n\nfunction bracesBalanced(glob: string): boolean {\n let depth = 0;\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i];\n if (c === '\\\\') {\n i += 1;\n continue;\n }\n if (c === '{') depth += 1;\n else if (c === '}') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n return depth === 0;\n}\n\nexport function globToRegExp(pattern: string): RegExp {\n const cached = regexpCache.get(pattern);\n if (cached) return cached;\n\n const glob = pattern.split('\\\\').join('/');\n const useBraces = bracesBalanced(glob);\n let out = '';\n let braceDepth = 0;\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i];\n if (c === '\\\\' && i + 1 < glob.length) {\n out += escapeLiteral(glob[i + 1]);\n i += 1;\n } else if (c === '*') {\n if (glob[i + 1] === '*') {\n if (glob[i + 2] === '/') {\n out += '(?:.*/)?';\n i += 2;\n } else {\n out += '.*';\n i += 1;\n }\n } else {\n out += '[^/]*';\n }\n } else if (c === '?') {\n out += '[^/]';\n } else if (c === '{' && useBraces) {\n out += '(?:';\n braceDepth += 1;\n } else if (c === '}' && useBraces && braceDepth > 0) {\n out += ')';\n braceDepth -= 1;\n } else if (c === ',' && useBraces && braceDepth > 0) {\n out += '|';\n } else {\n out += escapeLiteral(c);\n }\n }\n const re = new RegExp(`^${out}$`);\n regexpCache.set(pattern, re);\n return re;\n}\n\nexport function patternSpecificity(pattern: string): number {\n const glob = String(pattern).split('\\\\').join('/');\n const beforeWildcard = glob.split('*')[0];\n const literalSegments = beforeWildcard.split('/').filter(Boolean).length;\n const literalLength = glob.replace(/\\*/g, '').length;\n return literalSegments * 10000 + literalLength;\n}\n\nexport function layerForRelativePath(\n relPath: string,\n layers: LayerConfig[] | undefined\n): string | undefined {\n const rel = String(relPath).split('\\\\').join('/');\n let bestName: string | undefined;\n let bestScore = -1;\n for (const layer of layers ?? []) {\n if ((layer.exclude ?? []).some((pattern) => globToRegExp(pattern).test(rel))) {\n continue;\n }\n for (const pattern of layer.patterns ?? []) {\n if (globToRegExp(pattern).test(rel)) {\n const score = patternSpecificity(pattern);\n if (score > bestScore) {\n bestScore = score;\n bestName = layer.name;\n }\n }\n }\n }\n return bestName;\n}\n\nexport function isEdgeDenied(\n rules: EdgeRule[] | undefined,\n from: string,\n to: string\n): boolean {\n if (from === to) return false;\n const hit = (rules ?? []).find((r) => r.from === from && r.to === to);\n return hit?.allowed === false;\n}\n"],"mappings":"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","names":["rules","path","fs"]}
|
|
1
|
+
{"version":3,"sources":["../../src/eslint/index.ts","../../src/domain/layerMatch.ts"],"sourcesContent":["/**\n * arkgate/eslint — editor-side architecture gate.\n *\n * Layer / import / forbidden-globals rules load `ark.config.json` from the linted\n * project (walk-up from the file) and use the same glob specificity + edge semantics\n * as ark-check. Matching primitives come from the canonical\n * `src/domain/layerMatch.ts` (CLI loads the generated `bin/ark-layer-match.mjs`) —\n * no Kernel imports.\n */\nimport fs from 'node:fs';\nimport path from 'node:path';\nimport {\n globToRegExp,\n patternSpecificity,\n layerForRelativePath,\n isEdgeDenied,\n} from '../domain/layerMatch';\n\nexport { globToRegExp, patternSpecificity, layerForRelativePath, isEdgeDenied };\n\ntype RuleContext = {\n report(descriptor: Record<string, unknown>): void;\n /** ESLint 9+ / 10: preferred path on the context object. */\n filename?: string;\n /** ESLint 8-style physical path when linting with processors / virtual files. */\n physicalFilename?: string;\n /** ESLint ≤8 API — still present on some hosts; removed in ESLint 10. */\n getFilename?: () => string;\n options?: unknown[];\n};\n\n/** Resolve the file path being linted across ESLint 8–10 context shapes. */\nfunction lintedFilename(context: RuleContext): string {\n if (typeof context.physicalFilename === 'string' && context.physicalFilename.length > 0) {\n return context.physicalFilename;\n }\n if (typeof context.filename === 'string' && context.filename.length > 0) {\n return context.filename;\n }\n if (typeof context.getFilename === 'function') {\n try {\n const name = context.getFilename();\n if (typeof name === 'string' && name.length > 0) return name;\n } catch {\n /* ignore */\n }\n }\n return '';\n}\n\ntype RuleListener = Record<string, (node: AstNode) => void>;\n\ntype AstNode = {\n type?: string;\n name?: string;\n value?: unknown;\n source?: AstNode;\n callee?: AstNode;\n object?: AstNode;\n property?: AstNode;\n key?: AstNode;\n arguments?: AstNode[];\n properties?: AstNode[];\n importKind?: string;\n specifiers?: AstNode[];\n};\n\ntype ArkRule = {\n meta: {\n type: 'problem';\n docs: { description: string };\n messages: Record<string, string>;\n schema: unknown[];\n };\n create(context: RuleContext): RuleListener;\n};\n\ntype ArkEslintPlugin = {\n rules: Record<string, ArkRule>;\n configs?: Record<string, unknown>;\n};\n\ntype LayerConfig = {\n name: string;\n patterns?: string[];\n exclude?: string[];\n forbiddenGlobals?: string[];\n};\n\ntype EdgeRule = { from: string; to: string; allowed?: boolean };\n\ntype ArkConfig = {\n layers?: LayerConfig[];\n rules?: EdgeRule[];\n};\n\n// ── Config I/O (editor-only; matching primitives come from ark-layer-match.mjs) ──\n\nexport function findConfigPath(startFile: string): string | null {\n if (!startFile || startFile === '<input>' || startFile.startsWith('stdin')) return null;\n let dir = path.dirname(path.resolve(startFile));\n for (;;) {\n const candidate = path.join(dir, 'ark.config.json');\n if (fs.existsSync(candidate)) return candidate;\n const parent = path.dirname(dir);\n if (parent === dir) return null;\n dir = parent;\n }\n}\n\nconst _configCache = new Map<string, ArkConfig | null>();\n\nexport function loadArkConfig(configPath: string): ArkConfig | null {\n if (_configCache.has(configPath)) return _configCache.get(configPath) ?? null;\n try {\n const raw = JSON.parse(fs.readFileSync(configPath, 'utf8')) as ArkConfig;\n _configCache.set(configPath, raw);\n return raw;\n } catch {\n _configCache.set(configPath, null);\n return null;\n }\n}\n\n/** Resolve relative import specifier to an absolute path candidate (TS-oriented). */\nexport function resolveRelativeImport(fromFile: string, specifier: string): string | null {\n if (!specifier.startsWith('.')) return null;\n const base = path.resolve(path.dirname(fromFile), specifier);\n const candidates = [\n base,\n `${base}.ts`,\n `${base}.tsx`,\n `${base}.mts`,\n `${base}.cts`,\n `${base}.js`,\n `${base}.jsx`,\n path.join(base, 'index.ts'),\n path.join(base, 'index.tsx'),\n path.join(base, 'index.js'),\n ];\n for (const c of candidates) {\n try {\n if (fs.existsSync(c) && fs.statSync(c).isFile()) return c;\n } catch {\n /* continue */\n }\n }\n // Prefer .ts for layer matching when the target is not on disk yet (editor typing).\n return `${base}.ts`;\n}\n\n// ── AST helpers ────────────────────────────────────────────────────────────\n\nfunction stringValue(node: AstNode | undefined): string | undefined {\n return typeof node?.value === 'string' ? node.value : undefined;\n}\n\nfunction propertyName(node: AstNode | undefined): string | undefined {\n return node?.name ?? stringValue(node);\n}\n\nfunction calleePropertyName(node: AstNode): string | undefined {\n return propertyName(node.callee?.property);\n}\n\nfunction objectProperty(node: AstNode | undefined, name: string): AstNode | undefined {\n return node?.properties?.find((property) => propertyName(property.key) === name);\n}\n\nfunction objectHasProperty(node: AstNode | undefined, name: string): boolean {\n return objectProperty(node, name) !== undefined;\n}\n\nfunction objectHasMetadataSource(node: AstNode | undefined): boolean {\n const metadata = objectProperty(node, 'metadata')?.value as AstNode | undefined;\n return objectHasProperty(metadata, 'source');\n}\n\nfunction looksLikeIntent(value: string): boolean {\n return /^(Domain|Application|Adapter|Workflow|Job|Presentation|Reporting|Metadata|Security|Audit|Observability|Kernel)\\.[A-Za-z0-9_.]+$/.test(\n value\n );\n}\n\nfunction isPublishCall(node: AstNode): boolean {\n return calleePropertyName(node) === 'publish';\n}\n\n/** Heuristic fallback when no ark.config.json (pre-contract projects). */\nfunction isDomainFileHeuristic(filename: string): boolean {\n const normalized = filename.split('\\\\').join('/').toLowerCase();\n return normalized.includes('/domain/') || normalized.endsWith('/domain.ts');\n}\n\nfunction isInfraImportHeuristic(specifier: string): boolean {\n const normalized = specifier.toLowerCase();\n return [\n 'adapter',\n 'adapters',\n 'infrastructure',\n 'persistence',\n 'repository',\n 'repositories',\n 'integration',\n 'database',\n 'db',\n ].some((token) => normalized.includes(token));\n}\n\nconst DEFAULT_FORBIDDEN_GLOBALS = ['fetch', 'process', 'Date.now', 'Math.random'];\n\n// ── Rules ──────────────────────────────────────────────────────────────────\n\n/**\n * Config-driven layer import boundary (primary editor gate).\n * Replaces path-token domain/infra heuristics when ark.config.json is present.\n * Rule id kept as `no-domain-infra-imports` for recommended-config / upgrade stability.\n */\nexport const noDomainInfraImports: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow imports that violate ark.config.json layer rules (same contract as arkgate-check). Falls back to domain→infra path heuristics when no config is found.',\n },\n messages: {\n forbiddenImport:\n 'Architecture: {{fromLayer}} must not import {{toLayer}} (ark.config.json). Specifier: {{specifier}}',\n forbiddenImportHeuristic:\n 'Domain code must not import infrastructure, adapters, repositories, or database modules.',\n },\n schema: [],\n },\n create(context) {\n const filename = lintedFilename(context);\n const configPath = findConfigPath(filename);\n const config = configPath ? loadArkConfig(configPath) : null;\n const root = configPath ? path.dirname(configPath) : null;\n\n const check = (node: AstNode) => {\n const source = stringValue(node.source);\n if (!source) return;\n\n if (config && root && filename) {\n const absFile = path.isAbsolute(filename) ? filename : path.resolve(filename);\n const relFile = path.relative(root, absFile).split(path.sep).join('/');\n const fromLayer = layerForRelativePath(relFile, config.layers);\n if (!fromLayer) return;\n\n const targetAbs = resolveRelativeImport(absFile, source);\n if (!targetAbs) return; // package import — CI resolves via TS; editor skips non-relative\n\n const relTarget = path.relative(root, targetAbs).split(path.sep).join('/');\n // Outside project or up-and-out: skip\n if (relTarget.startsWith('..')) return;\n\n const toLayer = layerForRelativePath(relTarget, config.layers);\n if (!toLayer) return;\n if (isEdgeDenied(config.rules, fromLayer, toLayer)) {\n context.report({\n node,\n messageId: 'forbiddenImport',\n data: { fromLayer, toLayer, specifier: source },\n });\n }\n return;\n }\n\n // No contract: legacy heuristic so bare domain folders still get a signal.\n if (isDomainFileHeuristic(filename) && isInfraImportHeuristic(source)) {\n context.report({ node, messageId: 'forbiddenImportHeuristic' });\n }\n };\n\n return {\n ImportDeclaration: check,\n ExportNamedDeclaration: check,\n ExportAllDeclaration: check,\n };\n },\n};\n\nexport const noRawEventPublish: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Require event bus publish calls to use registered intent creators instead of raw event objects or intent strings.',\n },\n messages: {\n rawPublish:\n 'Publish through a registered intent creator; raw event objects or intent strings bypass Ark contracts.',\n },\n schema: [],\n },\n create(context) {\n return {\n CallExpression(node) {\n if (!isPublishCall(node)) return;\n const firstArg = node.arguments?.[0];\n const firstValue = stringValue(firstArg);\n if ((firstValue && looksLikeIntent(firstValue)) || objectHasProperty(firstArg, 'intent')) {\n context.report({ node, messageId: 'rawPublish' });\n }\n },\n };\n },\n};\n\nexport const requirePublishSource: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description: 'Require event bus publish calls to include source metadata.',\n },\n messages: {\n missingSource: 'Strict Ark publish calls must include metadata.source.',\n },\n schema: [],\n },\n create(context) {\n return {\n CallExpression(node) {\n if (!isPublishCall(node)) return;\n const firstArg = node.arguments?.[0];\n const metadataArg = node.arguments?.[2];\n if (objectHasMetadataSource(firstArg) || objectHasProperty(metadataArg, 'source')) {\n return;\n }\n context.report({ node, messageId: 'missingSource' });\n },\n };\n },\n};\n\nexport const noForbiddenGlobals: ArkRule = {\n meta: {\n type: 'problem',\n docs: {\n description:\n 'Disallow ambient globals from the layer’s forbiddenGlobals in ark.config.json (same purity surface as arkgate-check). Option `globals` overrides. Without config, defaults apply only on domain-like paths.',\n },\n messages: {\n forbiddenGlobal:\n 'Ambient global \"{{name}}\" is forbidden in {{layer}} (ark.config.json); inject the capability through a port instead.',\n forbiddenGlobalDefault:\n 'Ambient global \"{{name}}\" is forbidden here; inject the capability through a port instead.',\n },\n schema: [\n {\n type: 'object',\n properties: {\n globals: { type: 'array', items: { type: 'string' } },\n },\n additionalProperties: false,\n },\n ],\n },\n create(context) {\n const filename = lintedFilename(context);\n const option = context.options?.[0] as { globals?: string[] } | undefined;\n const configPath = findConfigPath(filename);\n const config = configPath ? loadArkConfig(configPath) : null;\n const root = configPath ? path.dirname(configPath) : null;\n\n let globals: Set<string> | null = null;\n let layerName = 'this layer';\n\n if (option?.globals) {\n globals = new Set(option.globals);\n } else if (config && root && filename) {\n const absFile = path.isAbsolute(filename) ? filename : path.resolve(filename);\n const relFile = path.relative(root, absFile).split(path.sep).join('/');\n const layer = config.layers?.find(\n (l) => l.name === layerForRelativePath(relFile, config.layers)\n );\n if (layer?.forbiddenGlobals?.length) {\n globals = new Set(layer.forbiddenGlobals);\n layerName = layer.name;\n } else {\n // Layer has no purity list — do not invent defaults (matches CI).\n globals = null;\n }\n } else if (isDomainFileHeuristic(filename)) {\n globals = new Set(DEFAULT_FORBIDDEN_GLOBALS);\n }\n\n if (!globals) {\n return {} as RuleListener;\n }\n\n const report = (node: AstNode, name: string) =>\n context.report({\n node,\n messageId: config ? 'forbiddenGlobal' : 'forbiddenGlobalDefault',\n data: { name, layer: layerName },\n });\n\n return {\n MemberExpression(node) {\n const base = node.object?.type === 'Identifier' ? node.object.name : undefined;\n if (!base) return;\n const dotted = `${base}.${propertyName(node.property) ?? ''}`;\n if (globals!.has(dotted)) report(node, dotted);\n else if (globals!.has(base)) report(node, base);\n },\n CallExpression(node) {\n const callee = node.callee?.type === 'Identifier' ? node.callee.name : undefined;\n if (callee && globals!.has(callee)) report(node, callee);\n },\n NewExpression(node) {\n const callee = node.callee?.type === 'Identifier' ? node.callee.name : undefined;\n if (callee && globals!.has(callee)) report(node, callee);\n },\n };\n },\n};\n\nconst rules = {\n 'no-domain-infra-imports': noDomainInfraImports,\n 'no-raw-event-publish': noRawEventPublish,\n 'require-publish-source': requirePublishSource,\n 'no-forbidden-globals': noForbiddenGlobals,\n};\n\nconst plugin: ArkEslintPlugin = { rules };\n\nplugin.configs = {\n recommended: {\n plugins: { ark: plugin },\n rules: {\n 'ark/no-domain-infra-imports': 'error',\n 'ark/no-raw-event-publish': 'error',\n 'ark/require-publish-source': 'error',\n 'ark/no-forbidden-globals': 'error',\n },\n },\n};\n\nexport { plugin };\nexport default plugin;\n","/**\n * Pure layer-glob matching for ark.config.json.\n *\n * **Canonical algorithm** for CLI, ESLint, and library consumers.\n * The CLI load path uses the generated `bin/ark-layer-match.mjs`\n * (`npm run generate:layer-match` / `npm run check:layer-match`).\n * Behavioral parity tests remain a safety net.\n */\n\nexport type LayerConfig = {\n name: string;\n patterns?: string[];\n exclude?: string[];\n forbiddenGlobals?: string[];\n};\n\nexport type EdgeRule = { from: string; to: string; allowed?: boolean };\n\nconst regexpCache = new Map<string, RegExp>();\n\nfunction escapeLiteral(ch: string): string {\n return /[.*+?^${}()|[\\]\\\\]/.test(ch) ? `\\\\${ch}` : ch;\n}\n\n/**\n * Normalize path separators to `/` without destroying glob escape sequences.\n * `src\\domain\\x` → `src/domain/x` (Windows paths); `src/\\{legacy\\}/**` keeps `\\{` / `\\}`.\n * A plain `pattern.split('\\\\').join('/')` would eat those escapes.\n */\nfunction normalizeGlobSeparators(pattern: string): string {\n let out = '';\n for (let i = 0; i < pattern.length; i += 1) {\n const c = pattern[i];\n if (c === '\\\\' && i + 1 < pattern.length) {\n const next = pattern[i + 1];\n // Keep escapes for glob metacharacters (and escaped backslash).\n if ('*?{}[],'.includes(next) || next === '\\\\') {\n out += '\\\\' + next;\n i += 1;\n continue;\n }\n // Otherwise treat `\\` as a path separator (Windows).\n out += '/';\n continue;\n }\n out += c;\n }\n return out;\n}\n\nfunction bracesBalanced(glob: string): boolean {\n let depth = 0;\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i];\n if (c === '\\\\') {\n i += 1;\n continue;\n }\n if (c === '{') depth += 1;\n else if (c === '}') {\n depth -= 1;\n if (depth < 0) return false;\n }\n }\n return depth === 0;\n}\n\nexport function globToRegExp(pattern: string): RegExp {\n const cached = regexpCache.get(pattern);\n if (cached) return cached;\n\n const glob = normalizeGlobSeparators(pattern);\n const useBraces = bracesBalanced(glob);\n let out = '';\n let braceDepth = 0;\n for (let i = 0; i < glob.length; i += 1) {\n const c = glob[i];\n if (c === '\\\\' && i + 1 < glob.length) {\n out += escapeLiteral(glob[i + 1]);\n i += 1;\n } else if (c === '*') {\n if (glob[i + 1] === '*') {\n if (glob[i + 2] === '/') {\n out += '(?:.*/)?';\n i += 2;\n } else {\n out += '.*';\n i += 1;\n }\n } else {\n out += '[^/]*';\n }\n } else if (c === '?') {\n out += '[^/]';\n } else if (c === '{' && useBraces) {\n out += '(?:';\n braceDepth += 1;\n } else if (c === '}' && useBraces && braceDepth > 0) {\n out += ')';\n braceDepth -= 1;\n } else if (c === ',' && useBraces && braceDepth > 0) {\n out += '|';\n } else {\n out += escapeLiteral(c);\n }\n }\n const re = new RegExp(`^${out}$`);\n regexpCache.set(pattern, re);\n return re;\n}\n\nexport function patternSpecificity(pattern: string): number {\n const glob = normalizeGlobSeparators(String(pattern));\n const beforeWildcard = glob.split('*')[0];\n const literalSegments = beforeWildcard.split('/').filter(Boolean).length;\n const literalLength = glob.replace(/\\*/g, '').length;\n return literalSegments * 10000 + literalLength;\n}\n\nexport function layerForRelativePath(\n relPath: string,\n layers: LayerConfig[] | undefined\n): string | undefined {\n // File paths (not globs): any OS separator → posix relative.\n const rel = String(relPath).split(/[/\\\\]/).join('/');\n let bestName: string | undefined;\n let bestScore = -1;\n for (const layer of layers ?? []) {\n if ((layer.exclude ?? []).some((pattern) => globToRegExp(pattern).test(rel))) {\n continue;\n }\n for (const pattern of layer.patterns ?? []) {\n if (globToRegExp(pattern).test(rel)) {\n const score = patternSpecificity(pattern);\n if (score > bestScore) {\n bestScore = score;\n bestName = layer.name;\n }\n }\n }\n }\n return bestName;\n}\n\nexport function isEdgeDenied(\n rules: EdgeRule[] | undefined,\n from: string,\n to: string\n): boolean {\n if (from === to) return false;\n const hit = (rules ?? []).find((r) => r.from === from && r.to === to);\n return hit?.allowed === false;\n}\n\n/** Codegen globs skipped by default scan (emitted into the CLI derived matcher). */\nexport const DEFAULT_GENERATED_FILE_GLOBS = [\n '**/*.gen.ts',\n '**/*.gen.tsx',\n '**/*.generated.ts',\n '**/*.generated.tsx',\n];\n\nexport type ScanExcludeConfig = {\n exclude?: string[];\n excludeGenerated?: boolean;\n};\n\nexport function scanExcludePatterns(config?: ScanExcludeConfig | null): string[] {\n const custom = Array.isArray(config?.exclude)\n ? config!.exclude!.filter((p) => typeof p === 'string')\n : [];\n const generated =\n config?.excludeGenerated === false ? [] : DEFAULT_GENERATED_FILE_GLOBS;\n return [...generated, ...custom];\n}\n\nexport function isScanExcludedRelative(\n relPath: string,\n config?: ScanExcludeConfig | null\n): boolean {\n const rel = String(relPath).split(/[/\\\\]/).join('/');\n return scanExcludePatterns(config).some((pattern) => globToRegExp(pattern).test(rel));\n}\n"],"mappings":"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","names":["rules","path","fs"]}
|
package/dist/eslint/index.d.cts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure layer-glob matching for ark.config.json.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* **Canonical algorithm** for CLI, ESLint, and library consumers.
|
|
5
|
+
* The CLI load path uses the generated `bin/ark-layer-match.mjs`
|
|
6
|
+
* (`npm run generate:layer-match` / `npm run check:layer-match`).
|
|
7
|
+
* Behavioral parity tests remain a safety net.
|
|
6
8
|
*/
|
|
7
9
|
type LayerConfig$1 = {
|
|
8
10
|
name: string;
|
package/dist/eslint/index.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure layer-glob matching for ark.config.json.
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
*
|
|
4
|
+
* **Canonical algorithm** for CLI, ESLint, and library consumers.
|
|
5
|
+
* The CLI load path uses the generated `bin/ark-layer-match.mjs`
|
|
6
|
+
* (`npm run generate:layer-match` / `npm run check:layer-match`).
|
|
7
|
+
* Behavioral parity tests remain a safety net.
|
|
6
8
|
*/
|
|
7
9
|
type LayerConfig$1 = {
|
|
8
10
|
name: string;
|
package/dist/eslint/index.js
CHANGED
|
@@ -7,6 +7,24 @@ var regexpCache = /* @__PURE__ */ new Map();
|
|
|
7
7
|
function escapeLiteral(ch) {
|
|
8
8
|
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
9
9
|
}
|
|
10
|
+
function normalizeGlobSeparators(pattern) {
|
|
11
|
+
let out = "";
|
|
12
|
+
for (let i = 0; i < pattern.length; i += 1) {
|
|
13
|
+
const c = pattern[i];
|
|
14
|
+
if (c === "\\" && i + 1 < pattern.length) {
|
|
15
|
+
const next = pattern[i + 1];
|
|
16
|
+
if ("*?{}[],".includes(next) || next === "\\") {
|
|
17
|
+
out += "\\" + next;
|
|
18
|
+
i += 1;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
out += "/";
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
out += c;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
10
28
|
function bracesBalanced(glob) {
|
|
11
29
|
let depth = 0;
|
|
12
30
|
for (let i = 0; i < glob.length; i += 1) {
|
|
@@ -26,7 +44,7 @@ function bracesBalanced(glob) {
|
|
|
26
44
|
function globToRegExp(pattern) {
|
|
27
45
|
const cached = regexpCache.get(pattern);
|
|
28
46
|
if (cached) return cached;
|
|
29
|
-
const glob = pattern
|
|
47
|
+
const glob = normalizeGlobSeparators(pattern);
|
|
30
48
|
const useBraces = bracesBalanced(glob);
|
|
31
49
|
let out = "";
|
|
32
50
|
let braceDepth = 0;
|
|
@@ -66,14 +84,14 @@ function globToRegExp(pattern) {
|
|
|
66
84
|
return re;
|
|
67
85
|
}
|
|
68
86
|
function patternSpecificity(pattern) {
|
|
69
|
-
const glob = String(pattern)
|
|
87
|
+
const glob = normalizeGlobSeparators(String(pattern));
|
|
70
88
|
const beforeWildcard = glob.split("*")[0];
|
|
71
89
|
const literalSegments = beforeWildcard.split("/").filter(Boolean).length;
|
|
72
90
|
const literalLength = glob.replace(/\*/g, "").length;
|
|
73
91
|
return literalSegments * 1e4 + literalLength;
|
|
74
92
|
}
|
|
75
93
|
function layerForRelativePath(relPath, layers) {
|
|
76
|
-
const rel = String(relPath).split(
|
|
94
|
+
const rel = String(relPath).split(/[/\\]/).join("/");
|
|
77
95
|
let bestName;
|
|
78
96
|
let bestScore = -1;
|
|
79
97
|
for (const layer of layers ?? []) {
|