@starci/hfs 4.0.10 → 4.2.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 +5 -1
- package/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +65 -24
- package/package.json +1 -1
- package/report/order.mjs +2 -0
- package/runtime/config.example.yaml +194 -0
- package/runtime/engine/by-code-unit.mjs +12 -0
- package/runtime/engine/config.mjs +222 -239
- 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/release-config.mjs +28 -0
- 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 +35 -31
- package/runtime/engine/sonar-config.mjs +21 -0
- package/runtime/engine/temp-root.mjs +32 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
- package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
- package/runtime/knowledge/hfs/rules.yaml +161 -65
- 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 -41
- package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +126 -265
- package/runtime/modules/models/tiers.yaml +66 -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/make-temp-dir.mjs +11 -0
- package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
- package/runtime/scripts/api/git/lib.mjs +34 -1
- package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
- package/runtime/scripts/api/process/run-program.mjs +2 -1
- package/runtime/scripts/api/sops/lib.mjs +198 -120
- 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 +2 -1
- 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 +164 -104
- 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 +121 -70
- 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/catalog-once.mjs +15 -0
- package/runtime/scripts/hfs/check.mjs +63 -71
- 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 +21 -4
- 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 +59 -48
- 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 +74 -695
- 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/env.mjs +10 -0
- package/runtime/scripts/lib/event-contract.mjs +17 -13
- 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/path-key.mjs +38 -5
- 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 +95 -48
- 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/runtime/scripts/lib/yaml-cached.mjs +14 -0
- 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
- package/runtime/engine/admission.mjs +0 -308
- package/runtime/engine/canonical-json.mjs +0 -10
- package/runtime/engine/db/blob.mjs +0 -315
- package/runtime/engine/db/ledger-paths.mjs +0 -83
- package/runtime/engine/db/ledger.mjs +0 -1118
- package/runtime/engine/db/machine-connection.mjs +0 -135
- package/runtime/engine/db/machine-schema.mjs +0 -84
- package/runtime/engine/db/machine.mjs +0 -1367
- package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
- package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
- package/runtime/engine/db/provider-reservations.mjs +0 -101
- package/runtime/engine/digest.mjs +0 -16
- package/runtime/engine/refuse.mjs +0 -11
- package/runtime/modules/ops/_labels.yaml +0 -53
- package/runtime/scripts/api/node/lib.mjs +0 -14
- package/runtime/scripts/api/node/spawn-node.mjs +0 -6
- package/runtime/scripts/api/process/lib.mjs +0 -111
- package/runtime/scripts/api/process/owned-process.mjs +0 -78
- package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
- package/runtime/scripts/connectors/lib.mjs +0 -488
- package/runtime/scripts/lib/clip.mjs +0 -19
- package/runtime/scripts/lib/display-names.mjs +0 -259
- package/runtime/scripts/lib/example-refs.mjs +0 -158
- package/runtime/scripts/lib/json-schema.mjs +0 -52
- package/runtime/scripts/lib/process-identity.mjs +0 -6
- package/runtime/scripts/lib/read-yaml.mjs +0 -18
- package/runtime/scripts/lib/redact.mjs +0 -161
- package/runtime/scripts/lib/sleep.mjs +0 -7
- package/runtime/scripts/lib/source-phrases.mjs +0 -34
- package/runtime/scripts/lib/sqlite.mjs +0 -21
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { relativePath, UNPROVEN_FRAMEWORK, unwrapExpression } from './typescript.mjs';
|
|
2
|
+
import { constructedDecoratorKind, mutableDecoratorKind, nodeDecorators, tracedFrameworkKinds, violation } from './ast-walks.mjs';
|
|
3
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
4
|
+
function locateSource(h, config, context, sourceFile, roots, state) {
|
|
5
|
+
const { featureRoots, moduleRoots, localFiles } = roots;
|
|
6
|
+
const { canonical, frameworkTargets, locatedRoot, sourceRole, exportedDeclarations, SPECIAL_BASENAMES } = h;
|
|
7
|
+
const fileName = canonical(sourceFile.fileName);
|
|
8
|
+
const featureRoot = locatedRoot(featureRoots, fileName);
|
|
9
|
+
const moduleRoot = locatedRoot(moduleRoots, fileName);
|
|
10
|
+
if (!featureRoot && !moduleRoot) return null;
|
|
11
|
+
state.checkedFiles += 1;
|
|
12
|
+
const checker = context.checkerFor(fileName);
|
|
13
|
+
if (!state.frameworkByChecker.has(checker)) state.frameworkByChecker.set(checker, frameworkTargets(config, context, checker, localFiles));
|
|
14
|
+
const framework = state.frameworkByChecker.get(checker);
|
|
15
|
+
const relativeToOwner = relativePath(featureRoot ?? moduleRoot, fileName);
|
|
16
|
+
const parts = relativeToOwner.split('/');
|
|
17
|
+
const directories = parts.slice(0, -1).map(part => part.toLowerCase());
|
|
18
|
+
const applicationIndex = directories.indexOf('application');
|
|
19
|
+
const transportIndex = directories.indexOf('transport');
|
|
20
|
+
const inApplication = applicationIndex >= 0;
|
|
21
|
+
const inTransport = transportIndex >= 0;
|
|
22
|
+
const role = sourceRole(context.ts, sourceFile);
|
|
23
|
+
const relative = relativePath(config.root, fileName);
|
|
24
|
+
const migrationName = /^\d{10,}-[A-Z][A-Za-z0-9]*$/.test(role.base);
|
|
25
|
+
const persistencePath = !role.spec && (migrationName || directories.includes('migrations'));
|
|
26
|
+
const special = SPECIAL_BASENAMES.has(role.base.toLowerCase()) || migrationName
|
|
27
|
+
|| ['constants', 'enums', 'errors', 'migrations'].some(folder => directories.includes(folder));
|
|
28
|
+
const publicDeclarations = exportedDeclarations(context.ts, checker, sourceFile);
|
|
29
|
+
return { config, context, sourceFile, fileName, featureRoot, moduleRoot, checker, framework, directories, transportIndex,
|
|
30
|
+
inApplication, inTransport, role, relative, migrationName, persistencePath, special, publicDeclarations };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function reportSourceName(h, info, state) {
|
|
34
|
+
const { kebabSourceBase, SOURCE_NAME_RULE_ID } = h;
|
|
35
|
+
const { config, sourceFile, migrationName, role, relative } = info;
|
|
36
|
+
if (!migrationName && !kebabSourceBase(role.base)) state.violations.push(violation(config, sourceFile, sourceFile, SOURCE_NAME_RULE_ID,
|
|
37
|
+
`Source basename ${role.base} must use kebab-case segments plus an explicit role suffix.`));
|
|
38
|
+
if (!role.role && !info.special) state.namingReasons.push(`${relative} has no statically identifiable source role`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function reportFeatureRoleConflicts(h, info, state) {
|
|
42
|
+
const { SOURCE_LAYOUT_RULE_ID, APPLICATION_ROLES, TRANSPORT_ROLES } = h;
|
|
43
|
+
const { config, sourceFile, role, directories, inApplication, inTransport, transportIndex } = info;
|
|
44
|
+
const { violations } = state;
|
|
45
|
+
if (inApplication && inTransport) violations.push(violation(config, sourceFile, sourceFile, SOURCE_LAYOUT_RULE_ID,
|
|
46
|
+
'A feature source cannot belong to both application and transport layers.'));
|
|
47
|
+
if (role.role && APPLICATION_ROLES.has(role.role) && !inApplication) violations.push(violation(config, sourceFile, sourceFile,
|
|
48
|
+
SOURCE_LAYOUT_RULE_ID, `${role.role} source belongs under the feature application/ layer.`));
|
|
49
|
+
if (role.role && TRANSPORT_ROLES.has(role.role) && !inTransport) violations.push(violation(config, sourceFile, sourceFile,
|
|
50
|
+
SOURCE_LAYOUT_RULE_ID, `${role.role} source belongs under the feature transport/<protocol>/ layer.`));
|
|
51
|
+
if (inApplication && role.role && TRANSPORT_ROLES.has(role.role)) violations.push(violation(config, sourceFile, sourceFile,
|
|
52
|
+
SOURCE_LAYOUT_RULE_ID, `Transport role ${role.role} cannot live in application/.`));
|
|
53
|
+
if (inTransport && role.role && APPLICATION_ROLES.has(role.role)) violations.push(violation(config, sourceFile, sourceFile,
|
|
54
|
+
SOURCE_LAYOUT_RULE_ID, `Application role ${role.role} cannot live in transport/.`));
|
|
55
|
+
if (inTransport && transportIndex >= directories.length - 1) violations.push(violation(config, sourceFile, sourceFile,
|
|
56
|
+
SOURCE_LAYOUT_RULE_ID, 'Feature transport source must name a protocol below transport/.'));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A non-index source whose role is neither one of the layer's own roles nor one the other layer or the schema owns. */
|
|
60
|
+
function unselectedLayerRole(role, layerRoles, otherRoles) {
|
|
61
|
+
return role.base !== 'index' && (!role.role || !layerRoles.has(role.role)) && !(role.role && (otherRoles.has(role.role) || role.role === 'entity'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function reportFeatureRoleSelection(h, info, state) {
|
|
65
|
+
const { APPLICATION_ROLES, TRANSPORT_ROLES, APPLICATION_LAYER_ROLES, TRANSPORT_LAYER_ROLES } = h;
|
|
66
|
+
const { role, relative, directories, inApplication, inTransport, persistencePath } = info;
|
|
67
|
+
const topLevelFeatureFile = directories.length === 1 && (role.base === 'index' || role.role === 'module' || role.spec);
|
|
68
|
+
if (inApplication && unselectedLayerRole(role, APPLICATION_LAYER_ROLES, TRANSPORT_ROLES)) {
|
|
69
|
+
state.layoutReasons.push(`${relative} role ${role.role ?? '(unclassified)'} is not a selected application-layer role`);
|
|
70
|
+
}
|
|
71
|
+
if (inTransport && unselectedLayerRole(role, TRANSPORT_LAYER_ROLES, APPLICATION_ROLES)) {
|
|
72
|
+
state.layoutReasons.push(`${relative} role ${role.role ?? '(unclassified)'} is not a selected transport-layer role`);
|
|
73
|
+
}
|
|
74
|
+
if (inApplication || inTransport || topLevelFeatureFile) return;
|
|
75
|
+
if (role.role && role.role !== 'entity' && !APPLICATION_ROLES.has(role.role) && !TRANSPORT_ROLES.has(role.role)) {
|
|
76
|
+
state.layoutReasons.push(`${relative} role ${role.role} has no statically selected feature layer`);
|
|
77
|
+
}
|
|
78
|
+
if (!role.role && !persistencePath) state.layoutReasons.push(`${relative} has no statically selected feature layer`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function reportFeatureSchemaPlacement(h, info, state) {
|
|
82
|
+
const { SOURCE_LAYOUT_RULE_ID } = h;
|
|
83
|
+
const { config, sourceFile, role, persistencePath } = info;
|
|
84
|
+
if (persistencePath) state.violations.push(violation(config, sourceFile, sourceFile, SOURCE_LAYOUT_RULE_ID,
|
|
85
|
+
'Migration source cannot be owned by a feature; place it under the declared persistence module.'));
|
|
86
|
+
if (role.role === 'entity') state.violations.push(violation(config, sourceFile, sourceFile, SOURCE_LAYOUT_RULE_ID,
|
|
87
|
+
'Entity source cannot be owned by a feature; place schema under the declared persistence module.'));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function reportFeatureLayout(h, info, state) {
|
|
91
|
+
if (!info.featureRoot) return;
|
|
92
|
+
reportFeatureRoleConflicts(h, info, state);
|
|
93
|
+
reportFeatureRoleSelection(h, info, state);
|
|
94
|
+
reportFeatureSchemaPlacement(h, info, state);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function inspectClassPlacement(h, info, declaration, kinds, state) {
|
|
98
|
+
const { implementsFramework, SOURCE_LAYOUT_RULE_ID } = h;
|
|
99
|
+
const { config, context, sourceFile, featureRoot, inTransport, directories, transportIndex, checker, framework } = info;
|
|
100
|
+
const { ts } = context;
|
|
101
|
+
const graphQlDto = kinds.some(kind => kind === 'ArgsType' || kind === 'InputType' || kind === 'ObjectType');
|
|
102
|
+
if (graphQlDto && (!featureRoot || !inTransport || directories[transportIndex + 1] !== 'graphql')) {
|
|
103
|
+
state.violations.push(violation(config, sourceFile, declaration.name ?? declaration, SOURCE_LAYOUT_RULE_ID,
|
|
104
|
+
'GraphQL DTO classes belong under a feature transport/graphql/ boundary.'));
|
|
105
|
+
}
|
|
106
|
+
const persistenceSchema = kinds.includes('Entity') || kinds.includes('ViewEntity')
|
|
107
|
+
|| implementsFramework(ts, checker, declaration, framework.bySymbol, 'MigrationInterface');
|
|
108
|
+
if (persistenceSchema && featureRoot) state.violations.push(violation(config, sourceFile, declaration.name ?? declaration, SOURCE_LAYOUT_RULE_ID,
|
|
109
|
+
'TypeORM entities and migrations cannot be owned by a feature; place schema under its declared persistence module.'));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function inspectClassName(h, info, declaration, state) {
|
|
113
|
+
const { implementedPortNames, SOURCE_NAME_RULE_ID, CLASS_ROLE_SUFFIX, NON_CLASS_ROLES } = h;
|
|
114
|
+
const { config, sourceFile, role, relative, checker } = info;
|
|
115
|
+
const { ts } = info.context;
|
|
116
|
+
const { publicDeclarations } = info;
|
|
117
|
+
const classSuffix = role.role && !NON_CLASS_ROLES.has(role.role) ? CLASS_ROLE_SUFFIX.get(role.role) : null;
|
|
118
|
+
if (publicDeclarations.has(declaration) && !declaration.name) state.namingReasons.push(`${relative} exports an anonymous class whose role name cannot be proved`);
|
|
119
|
+
else if (publicDeclarations.has(declaration) && declaration.name && classSuffix && !declaration.name.text.endsWith(classSuffix)
|
|
120
|
+
&& !implementedPortNames(ts, checker, declaration).some(port => declaration.name.text.endsWith(port))) {
|
|
121
|
+
state.violations.push(violation(config, sourceFile, declaration.name, SOURCE_NAME_RULE_ID,
|
|
122
|
+
`Exported class ${declaration.name.text} must end in ${classSuffix} for a ${role.role} source file, or in the name of the port it implements.`));
|
|
123
|
+
} else if (publicDeclarations.has(declaration) && declaration.name && role.role && !NON_CLASS_ROLES.has(role.role) && !classSuffix) {
|
|
124
|
+
state.namingReasons.push(`${relative} declares exported class ${declaration.name.text} with unclassified file role ${role.role}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function inspectClass(h, info, declaration, state) {
|
|
129
|
+
const { decoratorKind } = h;
|
|
130
|
+
const { ts } = info.context;
|
|
131
|
+
const { checker, framework } = info;
|
|
132
|
+
const declarationDecorators = nodeDecorators(ts, declaration);
|
|
133
|
+
const kinds = declarationDecorators.map(decorator => decoratorKind(ts, checker, decorator, framework.bySymbol)).filter(Boolean);
|
|
134
|
+
inspectClassPlacement(h, info, declaration, kinds, state);
|
|
135
|
+
inspectClassName(h, info, declaration, state);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function inspectClasses(h, info, state) {
|
|
139
|
+
const declarations = info.sourceFile.statements.filter(statement => info.context.ts.isClassDeclaration(statement));
|
|
140
|
+
for (const declaration of declarations) inspectClass(h, info, declaration, state);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function inspectClassValue(h, info, declaration, state) {
|
|
144
|
+
const { classValueStatus, SOURCE_NAME_RULE_ID, CLASS_ROLE_SUFFIX, NON_CLASS_ROLES } = h;
|
|
145
|
+
const { config, sourceFile, relative, role, checker, publicDeclarations } = info;
|
|
146
|
+
const { ts } = info.context;
|
|
147
|
+
if (!publicDeclarations.has(declaration) || !declaration.initializer) return;
|
|
148
|
+
const classValue = classValueStatus(ts, checker, declaration.initializer);
|
|
149
|
+
if (classValue.status === 'unavailable') {
|
|
150
|
+
state.namingReasons.push(`${relative} exports a class-valued declaration whose role identity is not statically proved`);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (classValue.status !== 'class') return;
|
|
154
|
+
const classSuffix = role.role && !NON_CLASS_ROLES.has(role.role) ? CLASS_ROLE_SUFFIX.get(role.role) : null;
|
|
155
|
+
if (!classSuffix || !ts.isIdentifier(declaration.name)) {
|
|
156
|
+
state.namingReasons.push(`${relative} exports a class-valued declaration without a statically selected class role`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const names = [declaration.name.text, ...classValue.names];
|
|
160
|
+
const invalid = names.find(name => !name.endsWith(classSuffix));
|
|
161
|
+
if (invalid) state.violations.push(violation(config, sourceFile, declaration.name, SOURCE_NAME_RULE_ID,
|
|
162
|
+
`Exported class value ${invalid} must end in ${classSuffix} for a ${role.role} source file.`));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function inspectClassValues(h, info, state) {
|
|
166
|
+
const { ts } = info.context;
|
|
167
|
+
const { sourceFile } = info;
|
|
168
|
+
for (const statement of sourceFile.statements) if (ts.isVariableStatement(statement)) {
|
|
169
|
+
for (const declaration of statement.declarationList.declarations) inspectClassValue(h, info, declaration, state);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function inspectEnumMembers(h, info, statement, state) {
|
|
174
|
+
const { SOURCE_NAME_RULE_ID } = h;
|
|
175
|
+
const { config, sourceFile } = info;
|
|
176
|
+
const { ts } = info.context;
|
|
177
|
+
for (const member of statement.members) {
|
|
178
|
+
const name = ts.isIdentifier(member.name) || ts.isStringLiteralLike(member.name) ? member.name.text : null;
|
|
179
|
+
const initializer = member.initializer && unwrapExpression(ts, member.initializer);
|
|
180
|
+
if (!name || !/^[A-Z][A-Za-z0-9]*$/.test(name) || !initializer || !ts.isStringLiteralLike(initializer)) {
|
|
181
|
+
state.violations.push(violation(config, sourceFile, member.name, SOURCE_NAME_RULE_ID,
|
|
182
|
+
'Enum members must use PascalCase names with explicit static string values.'));
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function inspectObjectContract(h, info, statement, state) {
|
|
188
|
+
const { objectContractStatus, companionInterfaceAllowed, SOURCE_NAME_RULE_ID, TRANSPORT_OBJECT_SUFFIX } = h;
|
|
189
|
+
const { config, sourceFile, role, checker, publicDeclarations } = info;
|
|
190
|
+
const { ts } = info.context;
|
|
191
|
+
const contractStatus = objectContractStatus(ts, checker, statement);
|
|
192
|
+
const transportSuffix = TRANSPORT_OBJECT_SUFFIX.get(role.role);
|
|
193
|
+
if (contractStatus === 'object' && publicDeclarations.has(statement) && transportSuffix && !role.spec
|
|
194
|
+
&& !statement.name.text.endsWith(transportSuffix) && !companionInterfaceAllowed(ts, sourceFile, statement.name.text)) {
|
|
195
|
+
state.violations.push(violation(config, sourceFile, statement.name, SOURCE_NAME_RULE_ID,
|
|
196
|
+
`Public object contract ${statement.name.text} in a ${role.role} source file must end in ${transportSuffix}.`));
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function inspectEnumAndContract(h, info, statement, state) {
|
|
201
|
+
const { hasModifier, SOURCE_NAME_RULE_ID } = h;
|
|
202
|
+
const { config, sourceFile } = info;
|
|
203
|
+
const { ts } = info.context;
|
|
204
|
+
if (ts.isEnumDeclaration(statement)) {
|
|
205
|
+
if (hasModifier(ts, statement, ts.SyntaxKind.ConstKeyword) || !/^[A-Z][A-Za-z0-9]*$/.test(statement.name.text)) {
|
|
206
|
+
state.violations.push(violation(config, sourceFile, statement.name, SOURCE_NAME_RULE_ID,
|
|
207
|
+
'Enums must be non-const declarations with PascalCase names.'));
|
|
208
|
+
}
|
|
209
|
+
inspectEnumMembers(h, info, statement, state);
|
|
210
|
+
}
|
|
211
|
+
inspectObjectContract(h, info, statement, state);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function inspectEnumsAndContracts(h, info, state) {
|
|
215
|
+
for (const statement of info.sourceFile.statements) inspectEnumAndContract(h, info, statement, state);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function inspectDynamicDecorator(info, dynamic, state) {
|
|
219
|
+
const detail = `${info.relative} uses a mutable or constructed ${dynamic} decorator identity`;
|
|
220
|
+
if (['Entity', 'ViewEntity', 'ArgsType', 'InputType', 'ObjectType', 'multiple framework', 'unproven framework'].includes(dynamic)) state.layoutReasons.push(detail);
|
|
221
|
+
if (!['Entity', 'ViewEntity'].includes(dynamic)) state.namingReasons.push(detail);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function inspectDecorator(h, info, node, decorator, state) {
|
|
225
|
+
const { graphqlField, graphqlArgument, decoratorKind, TRACE_NEW } = h;
|
|
226
|
+
const { ts } = info.context;
|
|
227
|
+
const { checker, framework } = info;
|
|
228
|
+
const kind = decoratorKind(ts, checker, decorator, framework.bySymbol);
|
|
229
|
+
if (!kind) {
|
|
230
|
+
const dynamic = mutableDecoratorKind(ts, checker, decorator, framework.bySymbol)
|
|
231
|
+
?? constructedDecoratorKind(ts, checker, decorator, framework.bySymbol, TRACE_NEW);
|
|
232
|
+
if (dynamic) inspectDynamicDecorator(info, dynamic, state);
|
|
233
|
+
}
|
|
234
|
+
if (kind === 'Mutation' || kind === 'Query') graphqlField({ config: info.config, context: info.context, sourceFile: info.sourceFile,
|
|
235
|
+
node, decorator, kind, namingReasons: state.namingReasons, violations: state.violations });
|
|
236
|
+
if (kind === 'Args') graphqlArgument(info.config, info.context, info.sourceFile, node, decorator, state.namingReasons, state.violations);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function inspectNodeDecorators(h, info, node, state) {
|
|
240
|
+
for (const decorator of nodeDecorators(info.context.ts, node)) inspectDecorator(h, info, node, decorator, state);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function inspectEnumCalls(h, info, node, state) {
|
|
244
|
+
const { graphqlEnumRegistration, selectedCallKind, callsNamedHelper } = h;
|
|
245
|
+
const { ts } = info.context;
|
|
246
|
+
const { checker, framework } = info;
|
|
247
|
+
if (!ts.isCallExpression(node)) return;
|
|
248
|
+
if (selectedCallKind(ts, checker, node, framework.bySymbol) === 'registerEnumType') {
|
|
249
|
+
graphqlEnumRegistration(info.config, info.context, info.sourceFile, node, state.namingReasons, state.violations);
|
|
250
|
+
} else if (callsNamedHelper(ts, checker, node, 'createEnumType')) {
|
|
251
|
+
state.namingReasons.push(`${info.relative} calls a project GraphQL enum adapter whose emitted type name is not statically proved`);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function inspectConstructedSource(h, info, node, state) {
|
|
256
|
+
const { SOURCE_LAYOUT_RULE_ID, TRACE_NEW } = h;
|
|
257
|
+
const { ts } = info.context;
|
|
258
|
+
const { checker, framework, publicDeclarations, featureRoot } = info;
|
|
259
|
+
const exportedFactoryCall = ts.isCallExpression(node) && ts.isVariableDeclaration(node.parent) && publicDeclarations.has(node.parent);
|
|
260
|
+
if ((ts.isNewExpression(node) || exportedFactoryCall) && featureRoot) {
|
|
261
|
+
const constructed = tracedFrameworkKinds(ts, checker, node, framework.bySymbol, TRACE_NEW);
|
|
262
|
+
if (constructed.has('EntitySchema')) state.violations.push(violation(info.config, info.sourceFile, node.expression, SOURCE_LAYOUT_RULE_ID,
|
|
263
|
+
'TypeORM EntitySchema cannot be owned by a feature; place schema under its declared persistence module.'));
|
|
264
|
+
if (constructed.has(UNPROVEN_FRAMEWORK)) state.layoutReasons.push(`${info.relative} has a constructed provider identity beyond the bounded static trace`);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function visitSourceNode(h, info, node, state) {
|
|
269
|
+
inspectNodeDecorators(h, info, node, state);
|
|
270
|
+
inspectEnumCalls(h, info, node, state);
|
|
271
|
+
inspectConstructedSource(h, info, node, state);
|
|
272
|
+
info.context.ts.forEachChild(node, child => visitSourceNode(h, info, child, state));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function inspectSourceFile(h, config, context, sourceFile, roots, state) {
|
|
276
|
+
const info = locateSource(h, config, context, sourceFile, roots, state);
|
|
277
|
+
if (!info) return;
|
|
278
|
+
reportSourceName(h, info, state);
|
|
279
|
+
reportFeatureLayout(h, info, state);
|
|
280
|
+
inspectClasses(h, info, state);
|
|
281
|
+
inspectClassValues(h, info, state);
|
|
282
|
+
inspectEnumsAndContracts(h, info, state);
|
|
283
|
+
visitSourceNode(h, info, sourceFile, state);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function coverageFor(state) {
|
|
287
|
+
for (const framework of state.frameworkByChecker.values()) {
|
|
288
|
+
state.layoutReasons.push(...framework.reasons);
|
|
289
|
+
state.namingReasons.push(...framework.reasons.filter(reason => reason.includes('@nestjs/graphql')));
|
|
290
|
+
}
|
|
291
|
+
if (state.checkedFiles === 0) {
|
|
292
|
+
state.layoutReasons.push('the checked TypeScript program contains no configured backend feature or module source');
|
|
293
|
+
state.namingReasons.push('the checked TypeScript program contains no configured backend feature or module source');
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
files: state.checkedFiles,
|
|
297
|
+
layout: state.layoutReasons.length
|
|
298
|
+
? { status: 'unavailable', reason: 'one or more backend source placement relations are not statically proved', details: [...new Set(state.layoutReasons)].sort(byCodeUnit) }
|
|
299
|
+
: { status: 'checked' },
|
|
300
|
+
naming: state.namingReasons.length
|
|
301
|
+
? { status: 'unavailable', reason: 'one or more backend source naming relations are not statically proved', details: [...new Set(state.namingReasons)].sort(byCodeUnit) }
|
|
302
|
+
: { status: 'checked' },
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** Check the adopted domain-first backend source layout without inferring semantic ownership from folder names alone. */
|
|
307
|
+
export function createBackendSourceShapeChecker(helpers) {
|
|
308
|
+
const { absoluteRoots, canonical } = helpers;
|
|
309
|
+
return function checkBackendSourceShape(config, context) {
|
|
310
|
+
const featureRoots = absoluteRoots(config.root, config.backend.features);
|
|
311
|
+
const moduleRoots = absoluteRoots(config.root, config.backend.modules);
|
|
312
|
+
const localFiles = new Set(context.files.map(file => canonical(file.fileName)));
|
|
313
|
+
const state = { frameworkByChecker: new Map(), violations: [], layoutReasons: [], namingReasons: [], checkedFiles: 0 };
|
|
314
|
+
const roots = { featureRoots, moduleRoots, localFiles };
|
|
315
|
+
for (const sourceFile of context.files) inspectSourceFile(helpers, config, context, sourceFile, roots, state);
|
|
316
|
+
return { violations: state.violations, coverage: coverageFor(state) };
|
|
317
|
+
};
|
|
318
|
+
}
|