@starci/hfs 4.0.9 → 4.1.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 +1 -1
- package/LICENSE +21 -0
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +82 -8
- package/package.json +3 -2
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +187 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +247 -273
- package/runtime/engine/invalid-config.mjs +5 -5
- package/runtime/engine/model-config.mjs +90 -0
- package/runtime/engine/orca-config.mjs +3 -1
- package/runtime/engine/removed-vocabulary.mjs +28 -0
- package/runtime/engine/resources-config.mjs +52 -0
- package/runtime/engine/runtime-root.mjs +17 -0
- package/runtime/engine/secrets.mjs +134 -0
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +18 -18
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +102 -69
- package/runtime/knowledge/hfs/slots.yaml +11 -14
- package/runtime/knowledge/patterns/be/api.yaml +7 -24
- package/runtime/knowledge/patterns/be/cli.yaml +3 -12
- package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
- package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
- package/runtime/modules/kernel/failure-codes.yaml +1 -1
- package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
- package/runtime/modules/models/registry.yaml +25 -397
- package/runtime/modules/models/runtimes.yaml +92 -262
- package/runtime/modules/models/tiers.yaml +65 -0
- package/runtime/scripts/api/fs/claim-file.mjs +30 -0
- package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
- package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
- package/runtime/scripts/api/fs/lib.mjs +6 -0
- package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +36 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +52 -0
- package/runtime/scripts/api/process/run-program.mjs +8 -0
- package/runtime/scripts/api/sops/decrypt.mjs +9 -16
- package/runtime/scripts/api/sops/lib.mjs +309 -11
- package/runtime/scripts/api/sops/seal.mjs +8 -4
- package/runtime/scripts/hfs/allows.mjs +3 -3
- package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
- package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
- package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
- package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
- package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
- package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
- package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
- package/runtime/scripts/hfs/architecture/config.mjs +95 -80
- package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
- package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
- package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
- package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
- package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
- package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
- package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
- package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
- package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
- package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
- package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
- package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
- package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
- package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
- package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
- package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
- package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +3 -2
- package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
- package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
- package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
- package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
- package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
- package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
- package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
- package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
- package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
- package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
- package/runtime/scripts/hfs/architecture/index.mjs +158 -102
- package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
- package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
- package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
- package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
- package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
- package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
- package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
- package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
- package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
- package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
- package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
- package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
- package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
- package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
- package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
- package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
- package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
- package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
- package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
- package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
- package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
- package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
- package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
- package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
- package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
- package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
- package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
- package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
- package/runtime/scripts/hfs/architecture.mjs +3 -4
- package/runtime/scripts/hfs/check.mjs +61 -70
- package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
- package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
- package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
- package/runtime/scripts/hfs/edition-slots.mjs +5 -3
- package/runtime/scripts/hfs/linear-text.mjs +30 -0
- package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
- package/runtime/scripts/hfs/path-findings.mjs +58 -38
- package/runtime/scripts/hfs/pin-findings.mjs +31 -0
- package/runtime/scripts/hfs/repo-identity.mjs +5 -2
- package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
- package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
- package/runtime/scripts/hfs/rules/cli.mjs +5 -1
- package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
- package/runtime/scripts/hfs/rules/contract.mjs +35 -27
- package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
- package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
- package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
- package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
- package/runtime/scripts/hfs/rules/database.mjs +36 -23
- package/runtime/scripts/hfs/rules/deps.mjs +64 -32
- package/runtime/scripts/hfs/rules/docker.mjs +113 -65
- package/runtime/scripts/hfs/rules/edition.mjs +118 -111
- package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
- package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
- package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
- package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
- package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
- package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
- package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
- package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
- package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
- package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
- package/runtime/scripts/hfs/rules/saga.mjs +128 -61
- package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
- package/runtime/scripts/hfs/rules/services.mjs +34 -16
- package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
- package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
- package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
- package/runtime/scripts/hfs/secret.mjs +90 -45
- package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
- package/runtime/scripts/hfs/slot-classify.mjs +65 -0
- package/runtime/scripts/hfs/slot-errors.mjs +11 -0
- package/runtime/scripts/hfs/slot-imports.mjs +76 -0
- package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
- package/runtime/scripts/hfs/slot-match.mjs +153 -0
- package/runtime/scripts/hfs/slot-path.mjs +8 -0
- package/runtime/scripts/hfs/slot-required.mjs +76 -0
- package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
- package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
- package/runtime/scripts/hfs/slots.mjs +72 -693
- package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
- package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
- package/runtime/scripts/hfs/tree.mjs +19 -14
- package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
- package/runtime/scripts/hfs/view.mjs +1 -1
- package/runtime/scripts/lib/dockerfile.mjs +56 -33
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- package/runtime/scripts/lib/fs-kind.mjs +14 -4
- package/runtime/scripts/lib/git.mjs +2 -2
- package/runtime/scripts/lib/graphql-contract.mjs +158 -322
- package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
- package/runtime/scripts/lib/i18n.mjs +3 -2
- package/runtime/scripts/lib/in-order.mjs +71 -0
- package/runtime/scripts/lib/language.mjs +3 -3
- package/runtime/scripts/lib/list.mjs +8 -1
- package/runtime/scripts/lib/mutation-fence.mjs +15 -0
- package/runtime/scripts/lib/path-key.mjs +42 -9
- package/runtime/scripts/lib/pid-alive.mjs +7 -0
- package/runtime/scripts/lib/regex.mjs +1 -1
- package/runtime/scripts/lib/same-text.mjs +1 -1
- package/runtime/scripts/lib/secret-patterns.mjs +6 -2
- package/runtime/scripts/lib/sleep-sync.mjs +2 -2
- package/runtime/scripts/lib/sops-envelope.mjs +103 -2
- package/runtime/scripts/lib/stack-services.mjs +1 -2
- package/runtime/scripts/lib/ts-ast.mjs +9 -0
- package/runtime/scripts/lib/walk.mjs +6 -1
- package/scaffold/add-cli-lite.mjs +2 -1
- package/scaffold/add-table.mjs +2 -2
- package/scaffold/add.mjs +3 -2
- package/scaffold/app.mjs +4 -2
- package/scaffold/edition-gate.mjs +25 -25
- package/scaffold/lite-exports.mjs +2 -1
- package/scaffold/service.mjs +5 -4
- package/sync/hygiene.mjs +15 -9
- package/sync/index.mjs +7 -3
- package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
- package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
- package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
- package/templates/app/ci-workflows/sonar-steps.yml +2 -2
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
- package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
- package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
- package/templates/app/starciwork.gitignore +1 -1
- package/templates/be/image/api/Dockerfile +1 -1
- package/templates/be/image/cli/Dockerfile +1 -1
- package/templates/be/image/worker/Dockerfile +1 -1
- package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
- package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
- package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
- package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
- package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
- package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
- package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
- package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/fe/image/next/Dockerfile +2 -1
- package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
- package/upgrade/index.mjs +9 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isServerActionModule } from './server-action.mjs';
|
|
2
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* HFS checks 1 and 2 (knowledge/hfs/slots.yaml `tiers`):
|
|
@@ -21,40 +22,61 @@ const REASON_TEXT = {
|
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
/** Strongly connected components (Tarjan, iterative) of `adjacency` (Map<node, Set<node>>); components of one node are dropped. */
|
|
25
|
+
function startTraversal(state, adjacency, node) {
|
|
26
|
+
const at = state.counter;
|
|
27
|
+
state.counter += 1;
|
|
28
|
+
state.index.set(node, at);
|
|
29
|
+
state.low.set(node, at);
|
|
30
|
+
state.stack.push(node);
|
|
31
|
+
state.onStack.add(node);
|
|
32
|
+
state.work.push({ node, iterator: adjacency.get(node)[Symbol.iterator]() });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function visitTarget(state, adjacency, frame, target) {
|
|
36
|
+
if (!adjacency.has(target)) return;
|
|
37
|
+
if (!state.index.has(target)) {
|
|
38
|
+
startTraversal(state, adjacency, target);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (state.onStack.has(target)) state.low.set(frame.node, Math.min(state.low.get(frame.node), state.index.get(target)));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function closeComponent(state, frame) {
|
|
45
|
+
const component = [];
|
|
46
|
+
let member;
|
|
47
|
+
do {
|
|
48
|
+
member = state.stack.pop();
|
|
49
|
+
state.onStack.delete(member);
|
|
50
|
+
component.push(member);
|
|
51
|
+
} while (member !== frame.node);
|
|
52
|
+
if (component.length > 1) state.result.push(component);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function finishFrame(state, frame) {
|
|
56
|
+
state.work.pop();
|
|
57
|
+
if (state.work.length) {
|
|
58
|
+
const parent = state.work.at(-1).node;
|
|
59
|
+
state.low.set(parent, Math.min(state.low.get(parent), state.low.get(frame.node)));
|
|
60
|
+
}
|
|
61
|
+
if (state.low.get(frame.node) === state.index.get(frame.node)) closeComponent(state, frame);
|
|
62
|
+
}
|
|
63
|
+
|
|
24
64
|
export function stronglyConnected(adjacency) {
|
|
25
|
-
|
|
26
|
-
const index = new Map();
|
|
27
|
-
const low = new Map();
|
|
28
|
-
const onStack = new Set();
|
|
29
|
-
const stack = [];
|
|
30
|
-
const result = [];
|
|
65
|
+
const state = { counter: 0, index: new Map(), low: new Map(), onStack: new Set(), stack: [], result: [], work: [] };
|
|
31
66
|
for (const start of adjacency.keys()) {
|
|
32
|
-
if (index.has(start)) continue;
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const frame = work.at(-1);
|
|
67
|
+
if (state.index.has(start)) continue;
|
|
68
|
+
startTraversal(state, adjacency, start);
|
|
69
|
+
while (state.work.length) {
|
|
70
|
+
const frame = state.work.at(-1);
|
|
37
71
|
const next = frame.iterator.next();
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
if (!adjacency.has(target)) continue;
|
|
41
|
-
if (!index.has(target)) {
|
|
42
|
-
index.set(target, counter); low.set(target, counter); counter += 1; stack.push(target); onStack.add(target);
|
|
43
|
-
work.push({ node: target, iterator: adjacency.get(target)[Symbol.iterator]() });
|
|
44
|
-
} else if (onStack.has(target)) low.set(frame.node, Math.min(low.get(frame.node), index.get(target)));
|
|
72
|
+
if (next.done) {
|
|
73
|
+
finishFrame(state, frame);
|
|
45
74
|
continue;
|
|
46
75
|
}
|
|
47
|
-
|
|
48
|
-
if (work.length) { const parent = work.at(-1).node; low.set(parent, Math.min(low.get(parent), low.get(frame.node))); }
|
|
49
|
-
if (low.get(frame.node) === index.get(frame.node)) {
|
|
50
|
-
const component = [];
|
|
51
|
-
let member;
|
|
52
|
-
do { member = stack.pop(); onStack.delete(member); component.push(member); } while (member !== frame.node);
|
|
53
|
-
if (component.length > 1) result.push(component);
|
|
54
|
-
}
|
|
76
|
+
visitTarget(state, adjacency, frame, next.value);
|
|
55
77
|
}
|
|
56
78
|
}
|
|
57
|
-
return result;
|
|
79
|
+
return state.result;
|
|
58
80
|
}
|
|
59
81
|
|
|
60
82
|
/** A shortest cycle through `origin` inside `members`, as a node path that starts and ends at origin. */
|
|
@@ -95,47 +117,57 @@ const blockCallsServerAction = (graph, edge) => {
|
|
|
95
117
|
return from.slot === 'fe.components' && from.kind === 'blocks' && from.role === 'entry';
|
|
96
118
|
};
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
120
|
+
function reportDirectServerAction(graph, edge, verdict, actionTarget, tierRule, counts, violations) {
|
|
121
|
+
if (!(verdict.allowed || verdict.reason === 'notPublicEntry') || !actionTarget
|
|
122
|
+
|| graph.unit(edge.from) === graph.unit(edge.to) || blockCallsServerAction(graph, edge)) return false;
|
|
123
|
+
counts.tierDirection += 1;
|
|
124
|
+
violations.push({
|
|
125
|
+
ruleId: tierRule,
|
|
126
|
+
path: edge.from, line: edge.line, column: edge.column,
|
|
127
|
+
specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
|
|
128
|
+
fromTier: graph.resolver.tierOf(edge.from), toTier: graph.resolver.tierOf(edge.to),
|
|
129
|
+
message: `${edge.from} -> ${edge.to}: a db Server Action is called directly only by a connected block entry; hooks and other tiers use their declared data path${edge.runtime ? '' : ' (type-only imports count)'}.`,
|
|
130
|
+
});
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function reportReasonedTierViolation(graph, edge, verdict, tierRule, counts, violations) {
|
|
135
|
+
if (verdict.allowed || (verdict.reason === 'tierDirection' && blockCallsServerAction(graph, edge)) || !REASON_TEXT[verdict.reason]) return;
|
|
136
|
+
counts[verdict.reason] += 1;
|
|
137
|
+
const featureToFeature = graph.profile === 'be' && verdict.reason === 'tierDirection' && verdict.fromTier === 'feature' && verdict.toTier === 'feature';
|
|
138
|
+
const fromKind = featureToFeature ? triggerOf(graph.resolver, edge.from) : null;
|
|
139
|
+
const toKind = featureToFeature ? triggerOf(graph.resolver, edge.to) : null;
|
|
140
|
+
const crossKind = fromKind !== null && toKind !== null && fromKind !== toKind;
|
|
141
|
+
let ruleId = tierRule;
|
|
142
|
+
if (featureToFeature) ruleId = 'BE_FEATURE_IMPORTS_FEATURE';
|
|
143
|
+
if (crossKind) ruleId = 'BE_KIND_ISOLATION';
|
|
144
|
+
if (verdict.reason === 'crossApp') ruleId = 'FE_APP_ISOLATION';
|
|
145
|
+
const typeOnly = edge.runtime ? '' : ' (type-only imports count)';
|
|
146
|
+
const message = crossKind
|
|
147
|
+
? `${edge.from} -> ${edge.to}: a ${fromKind} feature imports a ${toKind} feature; no kind imports another, every cross-kind call is an event published with eventBus.publish(event, tx) from a domain service${typeOnly}.`
|
|
148
|
+
: `${edge.from} -> ${edge.to}: ${REASON_TEXT[verdict.reason](verdict)}${typeOnly}.`;
|
|
149
|
+
violations.push({
|
|
150
|
+
ruleId,
|
|
151
|
+
path: edge.from, line: edge.line, column: edge.column,
|
|
152
|
+
specifier: edge.specifier, resolvedPath: edge.to, typeOnly: !edge.runtime,
|
|
153
|
+
fromTier: verdict.fromTier ?? null, toTier: verdict.toTier ?? null,
|
|
154
|
+
message,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function inspectTierEdge(graph, edge, tierRule, counts, violations, coverage) {
|
|
159
|
+
const verdict = graph.resolver.importAllowed(edge.from, edge.to);
|
|
160
|
+
if (verdict.reason === 'unowned' || verdict.reason?.startsWith('slot')) {
|
|
161
|
+
coverage.unclassifiedEdges += 1;
|
|
162
|
+
return;
|
|
136
163
|
}
|
|
164
|
+
coverage.edgesChecked += 1;
|
|
165
|
+
const actionTarget = serverActionTarget(graph, edge);
|
|
166
|
+
if (reportDirectServerAction(graph, edge, verdict, actionTarget, tierRule, counts, violations)) return;
|
|
167
|
+
reportReasonedTierViolation(graph, edge, verdict, tierRule, counts, violations);
|
|
168
|
+
}
|
|
137
169
|
|
|
138
|
-
|
|
170
|
+
function ownerAdjacency(graph, resolver) {
|
|
139
171
|
const adjacency = new Map();
|
|
140
172
|
const witness = new Map();
|
|
141
173
|
for (const edge of graph.edges) {
|
|
@@ -148,25 +180,44 @@ export function checkTiers(graph) {
|
|
|
148
180
|
adjacency.get(a).add(b);
|
|
149
181
|
if (!witness.has(`${a}\0${b}`)) witness.set(`${a}\0${b}`, edge);
|
|
150
182
|
}
|
|
183
|
+
return { adjacency, witness };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function addOwnerCycleViolations(graph, adjacency, witness, violations) {
|
|
151
187
|
const components = stronglyConnected(adjacency);
|
|
152
188
|
for (const component of components) {
|
|
153
189
|
const members = new Set(component);
|
|
154
|
-
const origin = [...component].sort()[0];
|
|
190
|
+
const origin = [...component].sort(byCodeUnit)[0];
|
|
155
191
|
const cycle = cycleThrough(adjacency, members, origin);
|
|
156
192
|
const hops = cycle.slice(0, -1).map((unit, i) => witness.get(`${unit}\0${cycle[i + 1]}`));
|
|
157
193
|
const label = unit => unit.slice(unit.indexOf(':') + 1) || unit;
|
|
158
194
|
const first = hops[0];
|
|
195
|
+
const connectedOwners = component.length > cycle.length - 1 ? ` (strongly connected with ${component.length} owners)` : '';
|
|
159
196
|
violations.push({
|
|
160
197
|
ruleId: 'ARCH_OWNER_CYCLE',
|
|
161
198
|
path: first.from, line: first.line, column: first.column,
|
|
162
199
|
cycle: cycle.map(label),
|
|
163
200
|
cycleImports: hops.map(hop => ({ path: hop.from, line: hop.line, specifier: hop.specifier, typeOnly: !hop.runtime })),
|
|
164
201
|
componentSize: component.length,
|
|
165
|
-
message: `Owner cycle: ${cycle.map(label).join(' -> ')}${
|
|
202
|
+
message: `Owner cycle: ${cycle.map(label).join(' -> ')}${connectedOwners}; type-only imports count.`,
|
|
166
203
|
});
|
|
167
204
|
}
|
|
205
|
+
return components.length;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function checkTiers(graph) {
|
|
209
|
+
const { resolver, profile } = graph;
|
|
210
|
+
const violations = [];
|
|
211
|
+
const tierRule = profile === 'be' ? 'BE_TIER_DIRECTION' : 'FE_TIER_DIRECTION';
|
|
212
|
+
const coverage = { edgesChecked: 0, unclassifiedEdges: 0 };
|
|
213
|
+
const counts = { tierDirection: 0, layerOrder: 0, crossApp: 0 };
|
|
214
|
+
for (const edge of graph.edges) inspectTierEdge(graph, edge, tierRule, counts, violations, coverage);
|
|
215
|
+
|
|
216
|
+
// Owner cycles: nodes are owner units, edges the imports between two distinct units.
|
|
217
|
+
const { adjacency, witness } = ownerAdjacency(graph, resolver);
|
|
218
|
+
const cycles = addOwnerCycleViolations(graph, adjacency, witness, violations);
|
|
168
219
|
return {
|
|
169
220
|
violations,
|
|
170
|
-
coverage: { status: 'checked',
|
|
221
|
+
coverage: { status: 'checked', ...coverage, ownerUnits: adjacency.size, cycles, ...counts },
|
|
171
222
|
};
|
|
172
223
|
}
|
|
@@ -33,74 +33,80 @@ const PACKAGE_API_SLOT = 'fe.package.api';
|
|
|
33
33
|
const CLIENT_SLOTS = new Set([APP_CLIENT_SLOT, 'fe.package.api.client']);
|
|
34
34
|
const NO_CLIENT = "the repository has no transport client. Create the one client (packages/<family>-api/src/client.ts, or the only app's modules/api/client.ts) and call it.";
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const violations = [];
|
|
41
|
-
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
42
|
-
const nextApps = resolver.repo.apps.filter(item => item.kind === 'next');
|
|
43
|
-
let readers = 0;
|
|
36
|
+
/** True when `node` names the global fetch: no import, and no declaration outside a lib declaration file. */
|
|
37
|
+
function isGlobalObjectFetch(ts, node) {
|
|
38
|
+
return ts.isPropertyAccessExpression(node) && node.name.text === 'fetch' && ts.isIdentifier(node.expression) && GLOBAL_OBJECTS.has(node.expression.text);
|
|
39
|
+
}
|
|
44
40
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (node.text !== 'fetch' || kit.importBinding(checker, node)) return false;
|
|
49
|
-
const parent = node.parent;
|
|
50
|
-
if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
|
|
51
|
-
// A type position (`typeof fetch`, `typeof globalThis.fetch`) names the type of fetch and sends nothing.
|
|
52
|
-
if (ts.isTypeQueryNode(parent) || ts.isQualifiedName(parent)) return false;
|
|
53
|
-
if ((ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isBindingElement(parent) || ts.isParameter(parent) || ts.isVariableDeclaration(parent)) && parent.name === node) return false;
|
|
54
|
-
if (ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isPropertySignature(parent)) return false;
|
|
55
|
-
const declarations = kit.declarationsOf(checker, node);
|
|
56
|
-
return declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
57
|
-
}
|
|
58
|
-
return ts.isPropertyAccessExpression(node) && node.name.text === 'fetch' && ts.isIdentifier(node.expression) && GLOBAL_OBJECTS.has(node.expression.text);
|
|
59
|
-
};
|
|
41
|
+
function isDeclarationName(ts, parent, node) {
|
|
42
|
+
return (ts.isPropertyAssignment(parent) || ts.isPropertyDeclaration(parent) || ts.isMethodDeclaration(parent) || ts.isBindingElement(parent) || ts.isParameter(parent) || ts.isVariableDeclaration(parent)) && parent.name === node;
|
|
43
|
+
}
|
|
60
44
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const clientRels = new Set(clients.map(file => file.rel));
|
|
65
|
-
const list = items => items.map(item => item.rel).join(', ');
|
|
66
|
-
const owned = clients.length ? `the repository has one transport (${list(clients)}). Call the client and take its Outcome.` : NO_CLIENT;
|
|
45
|
+
function isTypeOrImportPosition(ts, parent) {
|
|
46
|
+
return ts.isTypeQueryNode(parent) || ts.isQualifiedName(parent) || ts.isImportSpecifier(parent) || ts.isExportSpecifier(parent) || ts.isPropertySignature(parent);
|
|
47
|
+
}
|
|
67
48
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
49
|
+
function isGlobalFetch(ts, kit, checker, node) {
|
|
50
|
+
if (!ts.isIdentifier(node)) return isGlobalObjectFetch(ts, node);
|
|
51
|
+
if (node.text !== 'fetch') return false;
|
|
52
|
+
if (kit.importBinding(checker, node)) return false;
|
|
53
|
+
const parent = node.parent;
|
|
54
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
|
|
55
|
+
// A type position (`typeof fetch`, `typeof globalThis.fetch`) names the type of fetch and sends nothing.
|
|
56
|
+
if (isTypeOrImportPosition(ts, parent)) return false;
|
|
57
|
+
if (isDeclarationName(ts, parent, node)) return false;
|
|
58
|
+
const declarations = kit.declarationsOf(checker, node);
|
|
59
|
+
return declarations.every(declaration => declaration.getSourceFile().isDeclarationFile);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function reportMultiplicity(items, noun, nextApps, report, list) {
|
|
63
|
+
for (const file of items) {
|
|
64
|
+
if (items.length > 1) {
|
|
65
|
+
report(file, file.sourceFile, `The repository has ${items.length} ${noun}s (${list(items)}); it has exactly one: the api package's (packages/<family>-api/src/) when the apps share it, or the only app's. Keep one and delete the others.`);
|
|
66
|
+
} else if ((file.slot === APP_CLIENT_SLOT || file.slot === APP_OUTCOME_SLOT) && nextApps.length > 1) {
|
|
67
|
+
report(file, file.sourceFile, `${file.rel} is an app's ${noun} in a repository of ${nextApps.length} apps; a repository with several apps keeps its one ${noun} in the api package (packages/<family>-api/src/).`);
|
|
75
68
|
}
|
|
76
69
|
}
|
|
70
|
+
}
|
|
77
71
|
|
|
72
|
+
function globalFetchUse(ts, kit, checker, node) {
|
|
73
|
+
if (ts.isPropertyAccessExpression(node) && isGlobalFetch(ts, kit, checker, node)) return 'property';
|
|
74
|
+
if (ts.isIdentifier(node) && isGlobalFetch(ts, kit, checker, node)) return 'identifier';
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function moduleSpecifierOf(ts, node) {
|
|
79
|
+
if (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) return node.moduleSpecifier;
|
|
80
|
+
if (ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require'))) return node.arguments[0];
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function inspectTransportNode({ ts, kit, checker, file, node, isClient, clientCalls, clients, owned, report }) {
|
|
85
|
+
const fetchUse = globalFetchUse(ts, kit, checker, node);
|
|
86
|
+
if (fetchUse !== null) {
|
|
87
|
+
if (isClient) clientCalls.value += 1;
|
|
88
|
+
else if (fetchUse === 'property') report(file, node, `${node.getText(file.sourceFile)} reaches the global fetch outside the transport client; ${owned}`);
|
|
89
|
+
else report(file, node, `fetch is used outside the transport client (called, aliased or passed as a value); ${owned}`);
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
const specifier = moduleSpecifierOf(ts, node);
|
|
93
|
+
if (specifier && ts.isStringLiteralLike(specifier) && HTTP_LIBRARIES.has(specifier.text.split('/')[0])) {
|
|
94
|
+
report(file, node, `${specifier.text} is a second HTTP transport; the repository's one transport is built on fetch (${clients.length ? clients.map(item => item.rel).join(', ') : 'no client exists yet'}). Use the client.`, { library: specifier.text });
|
|
95
|
+
}
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function inspectTransportFiles({ files, graph, kit, ts, clientRels, clients, owned, report }) {
|
|
78
100
|
const appsWithFiles = new Set();
|
|
101
|
+
let readers = 0;
|
|
79
102
|
for (const file of files) {
|
|
80
|
-
const bindings = resolver.classifyPath(file.rel).bindings;
|
|
103
|
+
const bindings = kit.resolver.classifyPath(file.rel).bindings;
|
|
81
104
|
if (bindings?.app) appsWithFiles.add(bindings.app);
|
|
82
105
|
const checker = kit.checkerOf(file.sourceFile);
|
|
83
106
|
const isClient = clientRels.has(file.rel);
|
|
84
|
-
|
|
85
|
-
kit.walk(file.sourceFile, node => {
|
|
86
|
-
|
|
87
|
-
if (isClient) clientCalls += 1;
|
|
88
|
-
else report(file, node, `${node.getText(file.sourceFile)} reaches the global fetch outside the transport client; ${owned}`);
|
|
89
|
-
return false;
|
|
90
|
-
}
|
|
91
|
-
if (ts.isIdentifier(node) && isGlobalFetch(checker, node)) {
|
|
92
|
-
if (isClient) clientCalls += 1;
|
|
93
|
-
else report(file, node, `fetch is used outside the transport client (called, aliased or passed as a value); ${owned}`);
|
|
94
|
-
return false;
|
|
95
|
-
}
|
|
96
|
-
const specifier = ts.isImportDeclaration(node) || ts.isExportDeclaration(node) ? node.moduleSpecifier
|
|
97
|
-
: ts.isCallExpression(node) && (node.expression.kind === ts.SyntaxKind.ImportKeyword || (ts.isIdentifier(node.expression) && node.expression.text === 'require')) ? node.arguments[0] : null;
|
|
98
|
-
if (specifier && ts.isStringLiteralLike(specifier) && HTTP_LIBRARIES.has(specifier.text.split('/')[0])) {
|
|
99
|
-
report(file, node, `${specifier.text} is a second HTTP transport; the repository's one transport is built on fetch (${clients.length ? list(clients) : 'no client exists yet'}). Use the client.`, { library: specifier.text });
|
|
100
|
-
}
|
|
101
|
-
return true;
|
|
102
|
-
});
|
|
103
|
-
if (isClient && clientCalls === 0) report(file, file.sourceFile, `${file.rel} never calls the global fetch; the client is the one module that owns fetch, with its timeout and abort signal.`);
|
|
107
|
+
const clientCalls = { value: 0 };
|
|
108
|
+
kit.walk(file.sourceFile, node => inspectTransportNode({ ts, kit, checker, file, node, isClient, clientCalls, clients, owned, report }));
|
|
109
|
+
if (isClient && clientCalls.value === 0) report(file, file.sourceFile, `${file.rel} never calls the global fetch; the client is the one module that owns fetch, with its timeout and abort signal.`);
|
|
104
110
|
if (file.slot === API_SLOT && bindings?.app && path.posix.basename(file.rel).startsWith('read-')) {
|
|
105
111
|
readers += 1;
|
|
106
112
|
const reachesClient = graph.edges.some(edge => edge.from === file.rel && edge.runtime
|
|
@@ -108,5 +114,26 @@ export function checkTransportOwner(input) {
|
|
|
108
114
|
if (!reachesClient) report(file, file.sourceFile, `${file.rel} is a server reader that does not import the transport client; a reader fetches through the repository's one client, never through its own transport.`);
|
|
109
115
|
}
|
|
110
116
|
}
|
|
117
|
+
return { appsWithFiles, readers };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function checkTransportOwner(input) {
|
|
121
|
+
const { graph } = input;
|
|
122
|
+
const kit = machineKit(input);
|
|
123
|
+
const { ts, resolver } = kit;
|
|
124
|
+
const violations = [];
|
|
125
|
+
const report = (file, node, message, extra = {}) => violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node), message, ...extra });
|
|
126
|
+
const nextApps = resolver.repo.apps.filter(item => item.kind === 'next');
|
|
127
|
+
const files = [...graph.files.values()].filter(file => file.slot && file.tier !== 'e2e');
|
|
128
|
+
const clients = files.filter(file => CLIENT_SLOTS.has(file.slot));
|
|
129
|
+
const outcomes = files.filter(file => resolver.slot(file.slot)?.outcomeHome === true);
|
|
130
|
+
const clientRels = new Set(clients.map(file => file.rel));
|
|
131
|
+
const list = items => items.map(item => item.rel).join(', ');
|
|
132
|
+
const owned = clients.length ? `the repository has one transport (${list(clients)}). Call the client and take its Outcome.` : NO_CLIENT;
|
|
133
|
+
|
|
134
|
+
// Exactly one client and one Outcome union per repository.
|
|
135
|
+
for (const [items, noun] of [[clients, 'transport client'], [outcomes, 'Outcome union']]) reportMultiplicity(items, noun, nextApps, report, list);
|
|
136
|
+
|
|
137
|
+
const { appsWithFiles, readers } = inspectTransportFiles({ files, graph, kit, ts, clientRels, clients, owned, report });
|
|
111
138
|
return { violations, coverage: { status: 'checked', apps: appsWithFiles.size, readers, clients: clients.length, outcomes: outcomes.length } };
|
|
112
139
|
}
|