@starci/eslint-canon-be 3.0.7 → 3.0.9
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 +13 -1
- package/README.md +13 -7
- package/ambient-id.mjs +90 -0
- package/cli.mjs +181 -0
- package/comments.mjs +18 -21
- package/config-owner.mjs +20 -4
- package/connections.mjs +12 -12
- package/cookies.mjs +106 -0
- package/cqrs.mjs +22 -26
- package/data-access.mjs +9 -28
- package/default-deny.mjs +2 -1
- package/e2e-flow.mjs +4 -4
- package/event-bus.mjs +237 -0
- package/feature-thin.mjs +237 -0
- package/idempotency.mjs +3 -1
- package/index.mjs +22 -1
- package/infra-owner.mjs +1 -1
- package/injection.mjs +67 -22
- package/input-bounds.mjs +2 -1
- package/jobs.mjs +214 -0
- package/lib/ast.mjs +7 -0
- package/lib/config.mjs +1 -1
- package/lib/db-writes.mjs +1 -1
- package/lib/doors.mjs +81 -0
- package/lib/edition.mjs +45 -0
- package/lib/entity-writes.mjs +35 -0
- package/lib/hfs.mjs +3 -3
- package/lib/path.mjs +0 -6
- package/lib/persistence.mjs +11 -18
- package/lib/ports.mjs +5 -2
- package/lib/specifier.mjs +27 -0
- package/lib/transactions.mjs +37 -0
- package/lib/transport-slots.mjs +8 -3
- package/lib/unit-spec.mjs +63 -2
- package/module-layering.mjs +3 -24
- package/module-shape.mjs +2 -2
- package/naming.mjs +2 -2
- package/package.json +5 -3
- package/project-graph.mjs +8 -5
- package/projections.mjs +117 -0
- package/query-safety.mjs +1 -1
- package/realtime.mjs +181 -0
- package/runtime/engine/config.mjs +494 -0
- package/runtime/engine/invalid-config.mjs +15 -0
- package/runtime/engine/orca-config.mjs +27 -0
- package/runtime/engine/runtime-root.mjs +5 -0
- package/runtime/engine/yaml.mjs +3 -3
- package/runtime/knowledge/hfs/canon-pins.yaml +163 -32
- package/runtime/knowledge/hfs/rules.yaml +2059 -0
- package/runtime/knowledge/hfs/slots.yaml +915 -123
- package/runtime/knowledge/patterns/fe/folder.yaml +133 -27
- package/runtime/knowledge/sonar-gate.yaml +53 -19
- package/runtime/modules/models/registry.yaml +591 -0
- package/runtime/modules/models/runtimes.yaml +701 -0
- package/runtime/packages/hfs/templates/app/package-scripts/package.json +3 -10
- package/runtime/scripts/api/git/config-get.mjs +10 -0
- package/runtime/scripts/api/git/lib.mjs +17 -27
- package/runtime/scripts/api/git/ls-files.mjs +6 -0
- package/runtime/scripts/api/git/recorded-lines.mjs +19 -0
- package/runtime/scripts/api/git/remote.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse-query.mjs +5 -0
- package/runtime/scripts/{lib/hfs-allows.mjs → hfs/allows.mjs} +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +220 -0
- package/runtime/scripts/{checks → hfs}/architecture/backend.mjs +17 -24
- package/runtime/scripts/{checks → hfs}/architecture/background-unowned.mjs +16 -34
- package/runtime/scripts/{checks → hfs}/architecture/client-reaches-server.mjs +3 -1
- package/runtime/scripts/{checks → hfs}/architecture/clones.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/config.mjs +11 -8
- package/runtime/scripts/{checks → hfs}/architecture/connection-map.mjs +21 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +88 -0
- package/runtime/scripts/hfs/architecture/context-map.mjs +237 -0
- package/runtime/scripts/hfs/architecture/context-owner.mjs +82 -0
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +47 -0
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +60 -0
- package/runtime/scripts/{checks → hfs}/architecture/contract-fixture-guard.mjs +5 -11
- package/runtime/scripts/{checks → hfs}/architecture/contracts.mjs +33 -162
- package/runtime/scripts/{checks → hfs}/architecture/dead-exports.mjs +58 -29
- package/runtime/scripts/{checks → hfs}/architecture/doc-language.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/entrypoint.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/fe-slot-allows.mjs +4 -4
- package/runtime/scripts/{checks → hfs}/architecture/feature-shape.mjs +6 -3
- package/runtime/scripts/{checks → hfs}/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/frontend.mjs +17 -43
- package/runtime/scripts/{checks → hfs}/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/hfs.mjs +27 -42
- package/runtime/scripts/{checks → hfs}/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/{checks → hfs}/architecture/index.mjs +17 -4
- package/runtime/scripts/{checks → hfs}/architecture/machine-ast.mjs +44 -11
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +27 -0
- package/runtime/scripts/{checks → hfs}/architecture/module-per-transport.mjs +30 -6
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +96 -0
- package/runtime/scripts/{checks → hfs}/architecture/next-data.mjs +40 -168
- package/runtime/scripts/{checks → hfs}/architecture/owners.mjs +35 -15
- package/runtime/scripts/{checks → hfs}/architecture/register-once.mjs +2 -1
- package/runtime/scripts/{checks → hfs}/architecture/registration.mjs +23 -81
- package/runtime/scripts/{checks → hfs}/architecture/required-files.mjs +21 -13
- package/runtime/scripts/{checks → hfs}/architecture/schema-owner.mjs +17 -114
- package/runtime/scripts/hfs/architecture/server-action.mjs +24 -0
- package/runtime/scripts/hfs/architecture/side-boundary.mjs +16 -0
- package/runtime/scripts/{checks → hfs}/architecture/source-names.mjs +27 -147
- package/runtime/scripts/{checks → hfs}/architecture/sql-owner.mjs +59 -18
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +64 -0
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +111 -0
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +131 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +42 -0
- package/runtime/scripts/hfs/architecture/supabase.mjs +434 -0
- package/runtime/scripts/{checks → hfs}/architecture/surface.mjs +10 -4
- package/runtime/scripts/{checks → hfs}/architecture/test-world-files.mjs +11 -21
- package/runtime/scripts/{checks → hfs}/architecture/tiers.mjs +47 -5
- package/runtime/scripts/{checks → hfs}/architecture/transport-owner.mjs +3 -3
- package/runtime/scripts/{checks → hfs}/architecture/typescript.mjs +13 -27
- package/runtime/scripts/hfs/declaration-shape.mjs +62 -0
- package/runtime/scripts/hfs/declaration-slots.mjs +75 -0
- package/runtime/scripts/hfs/edition-slots.mjs +66 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +247 -0
- package/runtime/scripts/hfs/param-names.mjs +18 -0
- package/runtime/scripts/{lib/hfs-path-findings.mjs → hfs/path-findings.mjs} +7 -7
- package/runtime/scripts/{lib → hfs}/project-graph.mjs +0 -0
- package/runtime/scripts/{lib → hfs}/project-rule.mjs +3 -3
- package/runtime/scripts/{lib → hfs}/repo-identity.mjs +9 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +50 -0
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/fe-no-tests.mjs +6 -5
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/read.mjs +1 -1
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/secrets.mjs +4 -4
- package/runtime/scripts/{lib/hfs-rules → hfs/rules}/spec-placement.mjs +1 -1
- package/runtime/scripts/{lib/hfs-slots.mjs → hfs/slots.mjs} +114 -150
- package/runtime/scripts/hfs/test-world-slot.mjs +6 -0
- package/runtime/scripts/{checks → hfs}/typescript-programs.mjs +0 -5
- package/runtime/scripts/{lib/hfs-view.mjs → hfs/view.mjs} +6 -5
- package/runtime/scripts/lib/dockerfile.mjs +124 -0
- package/runtime/scripts/lib/git.mjs +36 -0
- package/runtime/scripts/lib/i18n.mjs +86 -0
- package/runtime/scripts/lib/json.mjs +13 -3
- package/runtime/scripts/lib/language.mjs +26 -7
- package/runtime/scripts/lib/line-count.mjs +9 -0
- package/runtime/scripts/lib/list.mjs +20 -0
- package/runtime/scripts/lib/path-key.mjs +59 -2
- package/runtime/scripts/lib/read-text.mjs +34 -0
- package/runtime/scripts/lib/regex.mjs +4 -0
- package/runtime/scripts/lib/secret-patterns.mjs +4 -3
- package/runtime/scripts/lib/sops-envelope.mjs +58 -0
- package/runtime/scripts/lib/stack-services.mjs +7 -31
- package/runtime/scripts/lib/ts-ast.mjs +35 -0
- package/runtime/scripts/{checks/common.mjs → lib/walk.mjs} +5 -15
- package/schema-authority.mjs +14 -12
- package/service-deps.mjs +7 -13
- package/service-isolation.mjs +72 -0
- package/size-budget.mjs +3 -2
- package/test-world.mjs +5 -4
- package/testing.mjs +27 -24
- package/transport.mjs +12 -9
- package/unit-spec.mjs +1 -1
- package/user-copy.mjs +5 -4
- package/webhooks.mjs +173 -0
- package/runtime/scripts/api/sops/decrypt.mjs +0 -16
- package/runtime/scripts/lib/recorded-lines.mjs +0 -27
- package/runtime/scripts/lib/test-secrets.mjs +0 -116
- /package/runtime/scripts/{checks → hfs}/architecture/config-unread.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/constructor-deps.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/cross-app-duplicate.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/default-deny.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-codes.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/error-masked.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/hooks-are-hooks.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/injection-token-exported.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/package-shape.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/reachability.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/route-files-thin.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/sql-tokens.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/symbols.mjs +0 -0
- /package/runtime/scripts/{checks → hfs}/architecture/unit-spec-providers.mjs +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { canonical } from './config.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import { referencedExports, relativePath, unwrapExpression } from './typescript.mjs';
|
|
4
|
+
import { commonJsRequireReasons, decoratorCallee, moduleExportsOf, mutableDecoratorKind, normalizedSymbol, normalizedSymbolValue, programSourcesOf, selectedNode, valueSymbol } from './ast-walks.mjs';
|
|
5
|
+
import { sourceLocation } from '../../lib/ts-ast.mjs';
|
|
4
6
|
|
|
5
7
|
const REGISTRATION_RULE_IDS = ['BE_MODULE_HANDLER_REGISTRATION', 'BE_MODULE_PROVIDER_REREGISTRATION'];
|
|
6
8
|
const FRAMEWORK = new Map([
|
|
@@ -9,106 +11,46 @@ const FRAMEWORK = new Map([
|
|
|
9
11
|
['QueryHandler', '@nestjs/cqrs'],
|
|
10
12
|
]);
|
|
11
13
|
|
|
12
|
-
function normalizedSymbolValue(ts, checker, value) {
|
|
13
|
-
let symbol = value ?? null;
|
|
14
|
-
const seen = new Set();
|
|
15
|
-
while (symbol && (symbol.flags & ts.SymbolFlags.Alias) && !seen.has(symbol)) {
|
|
16
|
-
seen.add(symbol);
|
|
17
|
-
const target = checker.getAliasedSymbol(symbol);
|
|
18
|
-
if (!target || target === symbol) break;
|
|
19
|
-
symbol = target;
|
|
20
|
-
}
|
|
21
|
-
return symbol;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function normalizedSymbol(ts, checker, node) {
|
|
25
|
-
return normalizedSymbolValue(ts, checker, checker?.getSymbolAtLocation(node) ?? null);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function valueSymbol(ts, checker, node, seen = new Set()) {
|
|
29
|
-
const selected = selectedNode(ts, node) ?? node;
|
|
30
|
-
const symbol = normalizedSymbol(ts, checker, selected);
|
|
31
|
-
if (!symbol || seen.has(symbol)) return symbol;
|
|
32
|
-
seen.add(symbol);
|
|
33
|
-
const declarations = symbol.getDeclarations?.() ?? [];
|
|
34
|
-
if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
|
|
35
|
-
&& (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
|
|
36
|
-
return valueSymbol(ts, checker, declarations[0].initializer, seen);
|
|
37
|
-
}
|
|
38
|
-
return symbol;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function calledExpression(ts, decorator) {
|
|
42
|
-
const expression = decorator.expression;
|
|
43
|
-
return ts.isCallExpression(expression) ? expression.expression : expression;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function selectedNode(ts, expression) {
|
|
47
|
-
expression = unwrapExpression(ts, expression);
|
|
48
|
-
if (ts.isIdentifier(expression)) return expression;
|
|
49
|
-
if (ts.isPropertyAccessExpression(expression)) return expression.name;
|
|
50
|
-
if (ts.isElementAccessExpression(expression) && ts.isStringLiteralLike(expression.argumentExpression)) return expression.argumentExpression;
|
|
51
|
-
return null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
14
|
function frameworkDecorator(ts, checker, decorator, targets) {
|
|
55
|
-
const selected = selectedNode(ts,
|
|
15
|
+
const selected = selectedNode(ts, decoratorCallee(ts, decorator));
|
|
56
16
|
if (!selected) return null;
|
|
57
17
|
const symbol = valueSymbol(ts, checker, selected);
|
|
58
18
|
for (const [name, target] of targets) if (symbol === target) return name;
|
|
59
19
|
return null;
|
|
60
20
|
}
|
|
61
21
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
for (const [name, expected] of targets) if (target === expected) return name;
|
|
70
|
-
return null;
|
|
71
|
-
}
|
|
22
|
+
/* This module's framework map keys decorator names, not symbols - invert it (first name wins, as the old
|
|
23
|
+
* `for...of` scan did) for the shared symbol-keyed lookup. */
|
|
24
|
+
const mutableFrameworkAlias = (ts, checker, decorator, targets) => {
|
|
25
|
+
const bySymbol = new Map();
|
|
26
|
+
for (const [name, symbol] of targets) if (!bySymbol.has(symbol)) bySymbol.set(symbol, name);
|
|
27
|
+
return mutableDecoratorKind(ts, checker, decorator, bySymbol);
|
|
28
|
+
};
|
|
72
29
|
|
|
73
30
|
function frameworkTargets(config, context, checker, localFiles) {
|
|
74
31
|
const targets = new Map();
|
|
75
32
|
const reasons = [];
|
|
76
|
-
const
|
|
77
|
-
if (!
|
|
78
|
-
for (const sourceFile of
|
|
79
|
-
if (!localFiles.has(canonical(sourceFile.fileName))) continue;
|
|
33
|
+
const files = programSourcesOf(context, checker, localFiles);
|
|
34
|
+
if (!files) return { targets, reasons: ['a TypeScript program checker could not be associated with its source'] };
|
|
35
|
+
for (const sourceFile of files) {
|
|
80
36
|
for (const statement of sourceFile.statements) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
const
|
|
84
|
-
&& statement.moduleReference.expression && context.ts.isStringLiteralLike(statement.moduleReference.expression)
|
|
85
|
-
? statement.moduleReference.expression : null;
|
|
86
|
-
const specifier = (moduleSpecifier ?? importEquals)?.text;
|
|
87
|
-
if (!specifier || ![...FRAMEWORK.values()].includes(specifier)) continue;
|
|
88
|
-
const expected = new Set([...FRAMEWORK].filter(([, packageName]) => packageName === specifier).map(([name]) => name));
|
|
37
|
+
const bound = moduleExportsOf(context.ts, checker, statement);
|
|
38
|
+
if (!bound || ![...FRAMEWORK.values()].includes(bound.specifier)) continue;
|
|
39
|
+
const expected = new Set([...FRAMEWORK].filter(([, packageName]) => packageName === bound.specifier).map(([name]) => name));
|
|
89
40
|
const selected = referencedExports(context.ts, statement, expected);
|
|
90
41
|
if (!selected.length) continue;
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${specifier}`);
|
|
42
|
+
if (!bound.symbol) {
|
|
43
|
+
reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${bound.specifier}`);
|
|
94
44
|
continue;
|
|
95
45
|
}
|
|
96
|
-
const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
|
|
97
46
|
for (const name of selected) {
|
|
98
|
-
const target = exports.get(name);
|
|
47
|
+
const target = bound.exports.get(name);
|
|
99
48
|
if (target) targets.set(name, target);
|
|
100
|
-
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${specifier}`);
|
|
49
|
+
else reasons.push(`${relativePath(config.root, sourceFile.fileName)} cannot resolve ${name} from ${bound.specifier}`);
|
|
101
50
|
}
|
|
102
51
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
&& node.arguments.length === 1 && context.ts.isStringLiteralLike(node.arguments[0])
|
|
106
|
-
&& [...FRAMEWORK.values()].includes(node.arguments[0].text)) {
|
|
107
|
-
reasons.push(`${relativePath(config.root, sourceFile.fileName)} uses a CommonJS Nest framework binding whose decorator identity cannot be proved`);
|
|
108
|
-
}
|
|
109
|
-
context.ts.forEachChild(node, visitRequire);
|
|
110
|
-
};
|
|
111
|
-
visitRequire(sourceFile);
|
|
52
|
+
commonJsRequireReasons(context.ts, checker, sourceFile, (specifier) => [...FRAMEWORK.values()].includes(specifier),
|
|
53
|
+
reasons, relativePath(config.root, sourceFile.fileName), 'whose decorator identity cannot be proved', 'Nest framework binding');
|
|
112
54
|
}
|
|
113
55
|
return { targets, reasons };
|
|
114
56
|
}
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { lsFiles } from '../../api/git/ls-files.mjs';
|
|
4
|
+
import { revParseQuery } from '../../api/git/rev-parse-query.mjs';
|
|
5
|
+
import { gitOutputOf } from '../../lib/git.mjs';
|
|
4
6
|
|
|
5
7
|
/**
|
|
6
8
|
* HFS check 5: the files and directories the slot manifest requires (knowledge/hfs/slots.yaml `requires`,
|
|
7
9
|
* `requiredInstances`, `minInstances`, required app kinds) must exist in the repository tree. Everything is read through the
|
|
8
|
-
* resolver of scripts/
|
|
10
|
+
* resolver of scripts/hfs/slots.mjs; nothing here names a path.
|
|
9
11
|
*
|
|
10
|
-
* BE_REQUIRED_MODULE_MISSING a backend app, feature, domain, integrations or platform instance lacks a required file or
|
|
11
|
-
* directory, or a required platform instance (config, logging, errors, primitives) is absent
|
|
12
12
|
* FE_ERROR_BOUNDARY_MISSING a fe.app.next app lacks global-error, error, not-found or loading under src/app
|
|
13
|
-
* HFS_REQUIRED_FILE_MISSING any other required file or directory (
|
|
14
|
-
*
|
|
13
|
+
* HFS_REQUIRED_FILE_MISSING any other required file or directory of any slot (backend app, feature, domain, integrations and
|
|
14
|
+
* platform instances, a required platform instance such as config or logging, packages, fe modules,
|
|
15
|
+
* i18n, api ...), and a minimum (no api app, no feature, no next app) which is reported once at hfs.json
|
|
15
16
|
*
|
|
16
17
|
* Slots of tier none (README, root config, .starciwork, .starcistacks) belong to the slot check; fe.app.next is the one
|
|
17
18
|
* tier-none slot judged here. The tree is the tracked one (`git ls-files` from the repository root, minus files deleted
|
|
18
19
|
* on disk), else the filesystem without node_modules, .git, dist and .next.
|
|
19
20
|
*/
|
|
20
|
-
export const REQUIRED_FILE_RULE_IDS = ['
|
|
21
|
+
export const REQUIRED_FILE_RULE_IDS = ['FE_ERROR_BOUNDARY_MISSING', 'HFS_REQUIRED_FILE_MISSING'];
|
|
21
22
|
|
|
22
23
|
const SKIPPED_DIRECTORIES = new Set(['node_modules', '.git', 'dist', '.next']);
|
|
23
24
|
const ERROR_BOUNDARY_FILE = /^(?:global-error|error|not-found|loading)\.tsx$/u;
|
|
24
|
-
const BACKEND_MODULE_SLOT = /^be\.(?:app|feature|transport|domain|integrations|platform)(?:\.|$)/u;
|
|
25
25
|
const ALSO_JUDGED_TIER_NONE = new Set(['fe.app.next']);
|
|
26
26
|
|
|
27
27
|
function gitFiles(root) {
|
|
28
28
|
try {
|
|
29
|
-
const run = args =>
|
|
30
|
-
run(
|
|
31
|
-
const deleted = new Set(run(
|
|
32
|
-
return run(
|
|
29
|
+
const run = (call, args) => gitOutputOf(call(args, { cwd: root, maxBuffer: 256 * 1024 * 1024 }));
|
|
30
|
+
run(revParseQuery, ['--is-inside-work-tree']);
|
|
31
|
+
const deleted = new Set(run(lsFiles, ['--deleted', '-z', '--', '.']).split('\0').filter(Boolean));
|
|
32
|
+
return run(lsFiles, ['--cached', '-z', '--', '.']).split('\0').filter(file => file && !deleted.has(file));
|
|
33
33
|
} catch {
|
|
34
34
|
return null;
|
|
35
35
|
}
|
|
@@ -60,12 +60,20 @@ export function treeOf(root) {
|
|
|
60
60
|
return { files, directories };
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* The prelude every tree-walking architecture checker shares: the input's config/graph/context, the compiler,
|
|
65
|
+
* the slot resolver and the tracked repository tree.
|
|
66
|
+
*/
|
|
67
|
+
export function checkerScope({ config, graph, context }) {
|
|
68
|
+
return { config, graph, context, ts: context.ts, resolver: graph.resolver, tree: treeOf(config.root) };
|
|
69
|
+
}
|
|
70
|
+
|
|
63
71
|
const strip = entry => entry.replace(/\/+$/u, '');
|
|
64
72
|
const rootOfTarget = target => (target.endsWith('/') ? strip(target) : (path.posix.dirname(target) === '.' ? '' : path.posix.dirname(target)));
|
|
65
73
|
|
|
66
74
|
function ruleFor(slotId, target, rootExists) {
|
|
67
75
|
if (slotId === 'fe.app.next' && rootExists && ERROR_BOUNDARY_FILE.test(path.posix.basename(strip(target)))) return 'FE_ERROR_BOUNDARY_MISSING';
|
|
68
|
-
return
|
|
76
|
+
return 'HFS_REQUIRED_FILE_MISSING';
|
|
69
77
|
}
|
|
70
78
|
|
|
71
79
|
export function checkRequiredFiles({ config, graph }) {
|
|
@@ -2,6 +2,7 @@ import path from 'node:path';
|
|
|
2
2
|
import { machineKit, pascal } from './machine-ast.mjs';
|
|
3
3
|
import { treeOf } from './required-files.mjs';
|
|
4
4
|
import { DATABASE_DIR } from './connection-map.mjs';
|
|
5
|
+
import { collectRegistrations, isPerConnection, persistenceOfFactory } from './context-map.mjs';
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* R35 `schema-owner` (BE_SCHEMA_OWNER, BE-CONVENTION 1.4.6). Schema lives with the capability that owns the table:
|
|
@@ -11,7 +12,7 @@ import { DATABASE_DIR } from './connection-map.mjs';
|
|
|
11
12
|
* folder named `entities` or `migrations` outside be.persistence is refused too;
|
|
12
13
|
* - `persistence/connection.ts` declares `<c>Entities` / `<c>Migrations`, the arrays the apps register; a capability has no
|
|
13
14
|
* `CONNECTION` alias (C0 lead decision 2026-09-30): its connection is the one those arrays are REGISTERED on, in the
|
|
14
|
-
* `entities` / `migrations` of a connection literal of `DatabaseModule.register` and of the
|
|
15
|
+
* `entities` / `migrations` of a connection literal of `DatabaseModule.register` and of the cli app. The arrays are
|
|
15
16
|
* registered under exactly one connection across all apps, that connection is declared in hfs.json, and every
|
|
16
17
|
* `Inject<Conn>EntityManager()` used inside the capability is that same connection;
|
|
17
18
|
* - the capability's `index.ts` exports `<c>Entities` when it has entities and `<c>Migrations` when it has migrations
|
|
@@ -36,13 +37,7 @@ export function checkSchemaOwner(input) {
|
|
|
36
37
|
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
37
38
|
const plain = (rel, message, extra = {}) => violations.push({ ruleId: RULE, path: rel, line: 1, column: 1, message, ...extra });
|
|
38
39
|
|
|
39
|
-
|
|
40
|
-
const persistenceOf = rel => {
|
|
41
|
-
const classified = resolver.classifyPath(rel);
|
|
42
|
-
if (classified.slot !== PERSISTENCE_SLOT) return null;
|
|
43
|
-
const below = rel.slice(classified.root.length + 1).split('/');
|
|
44
|
-
return { root: classified.root, capability: path.posix.basename(path.posix.dirname(classified.root)), folder: below.length > 1 ? below[0] : '', below };
|
|
45
|
-
};
|
|
40
|
+
const persistenceOf = persistenceOfFactory(resolver);
|
|
46
41
|
|
|
47
42
|
let entities = 0;
|
|
48
43
|
let migrations = 0;
|
|
@@ -80,7 +75,10 @@ export function checkSchemaOwner(input) {
|
|
|
80
75
|
const isMigration = heritage(statement).some(expression => kit.isImportOf(checker, expression, 'MigrationInterface', 'typeorm'));
|
|
81
76
|
if (isEntity) {
|
|
82
77
|
entities += 1;
|
|
83
|
-
|
|
78
|
+
// The table of a projection belongs to the projection that alone writes it: `<name>.projection-entity.ts` of slot be.projections.
|
|
79
|
+
const ownProjectionTable = resolver.classifyPath(file.rel).slot === 'be.projections' && file.rel.endsWith('.projection-entity.ts');
|
|
80
|
+
if (ownProjectionTable) note({ root: path.posix.join(path.posix.dirname(file.rel), 'persistence'), capability: path.posix.basename(path.posix.dirname(file.rel)) }, 'entities');
|
|
81
|
+
else if (persistence?.folder !== 'entities') report(file, statement.name, `Entity ${statement.name.text} is declared outside persistence/entities/ of a domain or platform capability; an entity lives with the capability that owns its table, in src/modules/{domain,platform}/<capability>/persistence/entities/<table>.entity.ts.`, { class: statement.name.text });
|
|
84
82
|
else note(persistence, 'entities');
|
|
85
83
|
}
|
|
86
84
|
if (!isMigration) continue;
|
|
@@ -100,124 +98,28 @@ export function checkSchemaOwner(input) {
|
|
|
100
98
|
}
|
|
101
99
|
}
|
|
102
100
|
|
|
103
|
-
|
|
104
|
-
const
|
|
105
|
-
const returned = declaration => {
|
|
106
|
-
const fn = ts.isVariableDeclaration(declaration) && declaration.initializer ? unparen(declaration.initializer) : declaration;
|
|
107
|
-
if (!(ts.isArrowFunction(fn) || ts.isFunctionDeclaration(fn) || ts.isFunctionExpression(fn) || ts.isMethodDeclaration(fn)) || !fn.body) return null;
|
|
108
|
-
if (!ts.isBlock(fn.body)) return fn.body;
|
|
109
|
-
const statements = fn.body.statements.filter(ts.isReturnStatement);
|
|
110
|
-
return statements.length === 1 ? statements[0].expression ?? null : null;
|
|
111
|
-
};
|
|
112
|
-
const propertySources = new Map(); // property name -> [{declaration, initializer, checker}] over the program
|
|
113
|
-
const assignmentsOf = name => {
|
|
114
|
-
if (propertySources.has(name)) return propertySources.get(name);
|
|
115
|
-
const list = [];
|
|
116
|
-
for (const file of graph.files.values()) {
|
|
117
|
-
if (!file.sourceFile.text.includes(name)) continue;
|
|
118
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
119
|
-
kit.walk(file.sourceFile, node => {
|
|
120
|
-
if (ts.isPropertyAssignment(node) && kit.propertyNameText(node.name) === name && ts.isObjectLiteralExpression(node.parent)) {
|
|
121
|
-
// the property of the type the literal is checked against, so a typed const and a typed return meet the same declaration
|
|
122
|
-
const contextual = checker.getContextualType(node.parent)?.getProperty(name);
|
|
123
|
-
const symbol = contextual ?? checker.getSymbolAtLocation(node.name);
|
|
124
|
-
list.push({ declaration: symbol?.declarations?.[0] ?? null, initializer: node.initializer, checker });
|
|
125
|
-
}
|
|
126
|
-
return true;
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
propertySources.set(name, list);
|
|
130
|
-
return list;
|
|
131
|
-
};
|
|
132
|
-
const nameOfLiteral = (checker, literal, depth) => {
|
|
133
|
-
const own = kit.propertyOf(literal, 'name');
|
|
134
|
-
if (own) return kit.stringValue(checker, kit.valueOfProperty(own));
|
|
135
|
-
for (const property of literal.properties) {
|
|
136
|
-
if (!ts.isSpreadAssignment(property)) continue;
|
|
137
|
-
const value = nameOfExpression(checker, property.expression, depth + 1);
|
|
138
|
-
if (value) return value;
|
|
139
|
-
}
|
|
140
|
-
return null;
|
|
141
|
-
};
|
|
142
|
-
const nameOfExpression = (checker, expression, depth) => {
|
|
143
|
-
const node = unparen(expression);
|
|
144
|
-
if (!node || depth > 5) return null;
|
|
145
|
-
if (ts.isObjectLiteralExpression(node)) return nameOfLiteral(checker, node, depth);
|
|
146
|
-
if (ts.isCallExpression(node)) {
|
|
147
|
-
for (const declaration of kit.declarationsOf(checker, node.expression)) {
|
|
148
|
-
const body = returned(declaration);
|
|
149
|
-
const value = body ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), body, depth + 1) : null;
|
|
150
|
-
if (value) return value;
|
|
151
|
-
}
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
if (ts.isIdentifier(node)) {
|
|
155
|
-
const declaration = kit.declarationsOf(checker, node)[0];
|
|
156
|
-
return declaration && ts.isVariableDeclaration(declaration) && declaration.initializer ? nameOfExpression(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, depth + 1) : null;
|
|
157
|
-
}
|
|
158
|
-
if (ts.isPropertyAccessExpression(node)) {
|
|
159
|
-
const target = kit.declarationsOf(checker, node.name)[0];
|
|
160
|
-
if (!target) return null;
|
|
161
|
-
const names = new Set(assignmentsOf(node.name.text).filter(item => item.declaration === target).map(item => nameOfExpression(item.checker, item.initializer, depth + 1)).filter(Boolean));
|
|
162
|
-
return names.size === 1 ? [...names][0] : null;
|
|
163
|
-
}
|
|
164
|
-
return null;
|
|
165
|
-
};
|
|
166
|
-
/** The capability arrays (`<c>Entities` / `<c>Migrations` of persistence/connection.ts) an expression lists. */
|
|
167
|
-
const arraysIn = (checker, expression, out, depth) => {
|
|
168
|
-
const node = unparen(expression);
|
|
169
|
-
if (!node || depth > 6) return;
|
|
170
|
-
if (ts.isArrayLiteralExpression(node)) { for (const element of node.elements) arraysIn(checker, ts.isSpreadElement(element) ? element.expression : element, out, depth + 1); return; }
|
|
171
|
-
if (!ts.isIdentifier(node) && !ts.isPropertyAccessExpression(node)) return;
|
|
172
|
-
const declaration = kit.declarationsOf(checker, ts.isPropertyAccessExpression(node) ? node.name : node)[0];
|
|
173
|
-
if (!declaration || !ts.isVariableDeclaration(declaration) || !ts.isIdentifier(declaration.name)) return;
|
|
174
|
-
const rel = kit.graphPath(declaration);
|
|
175
|
-
const owner = rel ? persistenceOf(rel) : null;
|
|
176
|
-
if (owner && owner.below.length === 1 && owner.below[0] === 'connection.ts' && [`${camel(owner.capability)}Entities`, `${camel(owner.capability)}Migrations`].includes(declaration.name.text)) {
|
|
177
|
-
out.push({ key: path.posix.dirname(owner.root), name: owner.capability });
|
|
178
|
-
} else if (declaration.initializer) arraysIn(kit.checkerOf(declaration.getSourceFile()), declaration.initializer, out, depth + 1);
|
|
179
|
-
};
|
|
180
|
-
const registered = new Map(); // capability root -> {name, connections: Map(connection -> first site)}
|
|
181
|
-
let registrations = 0;
|
|
182
|
-
for (const file of graph.files.values()) {
|
|
183
|
-
const checker = kit.checkerOf(file.sourceFile);
|
|
184
|
-
kit.walk(file.sourceFile, node => {
|
|
185
|
-
if (!ts.isObjectLiteralExpression(node)) return true;
|
|
186
|
-
const arrays = [];
|
|
187
|
-
for (const key of ['entities', 'migrations']) {
|
|
188
|
-
const property = kit.propertyOf(node, key);
|
|
189
|
-
if (property) arraysIn(checker, kit.valueOfProperty(property), arrays, 0);
|
|
190
|
-
}
|
|
191
|
-
if (!arrays.length) return true;
|
|
192
|
-
registrations += 1;
|
|
193
|
-
const connection = nameOfLiteral(checker, node, 0);
|
|
194
|
-
if (connection === null) return true;
|
|
195
|
-
for (const { key, name } of new Map(arrays.map(item => [item.key, item])).values()) {
|
|
196
|
-
if (!registered.has(key)) registered.set(key, { name, connections: new Map() });
|
|
197
|
-
const sites = registered.get(key).connections;
|
|
198
|
-
if (!sites.has(connection)) sites.set(connection, { file, node });
|
|
199
|
-
}
|
|
200
|
-
return true;
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
for (const { name, connections: sites } of registered.values()) {
|
|
101
|
+
const { registered, registrations } = collectRegistrations({ kit, graph, persistenceOf });
|
|
102
|
+
for (const [key, { name, connections: sites }] of registered) {
|
|
204
103
|
for (const [connection, site] of sites) {
|
|
205
104
|
if (!connections.has(connection)) report(site.file, site.node, `The ${name} capability is registered on connection "${connection}", which hfs.json does not declare (declared: ${[...connections].sort().join(', ') || 'none'}); the tables of a capability live on one declared connection.`, { connection, capability: name });
|
|
206
105
|
}
|
|
207
|
-
if (sites.size > 1) {
|
|
106
|
+
if (sites.size > 1 && !isPerConnection(resolver, key, name)) {
|
|
208
107
|
const all = [...sites.keys()].sort().join(' and ');
|
|
209
108
|
for (const [connection, site] of [...sites].slice(1)) report(site.file, site.node, `The ${name} capability's entities and migrations are registered on connections ${all}; a capability's tables live on exactly one connection, so register its arrays under one connection across every app.`, { connection, capability: name });
|
|
210
109
|
}
|
|
211
110
|
}
|
|
212
111
|
for (const [key, kinds] of found) {
|
|
213
|
-
if (!registered.get(key)?.connections.size) plain(`${key}/persistence/connection.ts`, `The ${kinds.name} capability has entities or migrations but no app registers them; add ${camel(kinds.name)}Entities and ${camel(kinds.name)}Migrations to a connection of DatabaseModule.register and of the
|
|
112
|
+
if (!registered.get(key)?.connections.size) plain(`${key}/persistence/connection.ts`, `The ${kinds.name} capability has entities or migrations but no app registers them; add ${camel(kinds.name)}Entities and ${camel(kinds.name)}Migrations to a connection of DatabaseModule.register and of the cli app.`, { capability: kinds.name });
|
|
214
113
|
const connectionFile = graph.files.get(`${key}/persistence/connection.ts`);
|
|
215
114
|
const declared = new Set((connectionFile?.sourceFile.statements.filter(ts.isVariableStatement).flatMap(statement => statement.declarationList.declarations) ?? []).map(item => item.name.text));
|
|
216
115
|
for (const [kind, has] of [['Entities', kinds.entities], ['Migrations', kinds.migrations]]) {
|
|
217
116
|
if (has && !declared.has(`${camel(kinds.name)}${kind}`)) plain(`${key}/persistence/connection.ts`, `${key}/persistence/connection.ts must export ${camel(kinds.name)}${kind}, the ${kind.toLowerCase()} of the ${kinds.name} capability, the one file that lists them for the apps.`, { capability: kinds.name });
|
|
218
117
|
}
|
|
219
118
|
const only = registered.get(key)?.connections;
|
|
220
|
-
if (!only || only.size
|
|
119
|
+
if (!only || only.size < 1) continue;
|
|
120
|
+
// A per-connection platform capability (event-bus, jobs) lives on every connection that uses it; any other on exactly one (reported above).
|
|
121
|
+
const perConnection = only.size > 1 && isPerConnection(resolver, key, kinds.name);
|
|
122
|
+
if (only.size > 1 && !perConnection) continue;
|
|
221
123
|
const home = [...only.keys()][0];
|
|
222
124
|
for (const file of graph.files.values()) {
|
|
223
125
|
if (!file.rel.startsWith(`${key}/`)) continue;
|
|
@@ -227,7 +129,8 @@ export function checkSchemaOwner(input) {
|
|
|
227
129
|
const declaration = kit.declarationsOf(checker, node.expression)[0];
|
|
228
130
|
const rel = declaration ? kit.graphPath(declaration) : null;
|
|
229
131
|
const used = rel ? [...connections].find(name => rel === `${DATABASE_DIR}/${name}.decorators.ts`) : null;
|
|
230
|
-
if (used &&
|
|
132
|
+
if (used && perConnection && !only.has(used)) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${[...only.keys()].sort().join(' and ')}; register the capability on ${used} too or inject one of its connections.`, { connection: used, expected: [...only.keys()].sort().join(','), capability: kinds.name });
|
|
133
|
+
else if (used && !perConnection && used !== home) report(file, node, `${file.rel} injects the entity manager of connection ${used}, but the ${kinds.name} capability's tables are registered on ${home}; a capability reads only its own database, so use Inject${pascal(home)}EntityManager or move the tables.`, { connection: used, expected: home, capability: kinds.name });
|
|
231
134
|
return true;
|
|
232
135
|
});
|
|
233
136
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The value of a directive-prologue statement, or null once the prologue has ended. */
|
|
2
|
+
function directiveValue(ts, sourceFile, statement) {
|
|
3
|
+
const expressionStatement = ts?.isExpressionStatement
|
|
4
|
+
? ts.isExpressionStatement(statement)
|
|
5
|
+
: Boolean(statement?.expression);
|
|
6
|
+
if (!expressionStatement) return null;
|
|
7
|
+
const expression = statement.expression;
|
|
8
|
+
if (ts?.isStringLiteral) return ts.isStringLiteral(expression) ? expression.text : null;
|
|
9
|
+
const raw = expression?.getText?.(sourceFile);
|
|
10
|
+
const quote = raw?.[0];
|
|
11
|
+
return typeof expression?.text === 'string' && (quote === "'" || quote === '"') && raw.at(-1) === quote
|
|
12
|
+
? expression.text
|
|
13
|
+
: null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** True when a TypeScript source file declares `"use server"` in its directive prologue. */
|
|
17
|
+
export function isServerActionModule(ts, sourceFile) {
|
|
18
|
+
for (const statement of sourceFile?.statements ?? []) {
|
|
19
|
+
const value = directiveValue(ts, sourceFile, statement);
|
|
20
|
+
if (value === null) return false;
|
|
21
|
+
if (value === 'use server') return true;
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// side-boundary.mjs - what counts as an import that leaves a side of an app (ARCH_INTERNAL_IMPORT_OUTSIDE).
|
|
2
|
+
// A side (be/ or fe/) imports nothing of the app outside itself: the root holds no source and the other side is another program; a
|
|
3
|
+
// declared read of the OTHER side (be/contracts/) is codegen input, never an import. A declared read of a tree the APP ROOT owns
|
|
4
|
+
// (supabase/types/, sides.<side>.reads of the slot manifest) is the one thing both sides do import: the generated database types.
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { isInside, slash } from './config.mjs';
|
|
7
|
+
|
|
8
|
+
const OTHER_SIDE = /^(be|fe)\//;
|
|
9
|
+
|
|
10
|
+
/** True when `target` (an absolute path) lies in a tree of the app root that the side declares it reads (`reads` of the side declaration). */
|
|
11
|
+
const readsAppTree = (config, target) => (config.hfs?.repo?.reads ?? [])
|
|
12
|
+
.some((read) => !OTHER_SIDE.test(read) && isInside(path.join(config.packageRoot, read), target));
|
|
13
|
+
|
|
14
|
+
/** Whether an import of `target` leaves the side the machine judges (config.root) for the app (config.packageRoot), declared app-root reads excepted. */
|
|
15
|
+
export const crossesSide = (config, target) => config.packageRoot !== undefined && config.packageRoot !== config.root
|
|
16
|
+
&& isInside(config.packageRoot, target) && !isInside(config.root, target) && !slash(target).includes('/node_modules/') && !readsAppTree(config, target);
|