@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
|
@@ -14,7 +14,7 @@ import { buildHfsGraph } from './hfs-graph.mjs';
|
|
|
14
14
|
import { checkTiers, TIER_RULE_IDS } from './tiers.mjs';
|
|
15
15
|
import { checkReachability, REACHABILITY_RULE_IDS } from './reachability.mjs';
|
|
16
16
|
import { checkDeadExports, DEAD_EXPORT_RULE_IDS } from './dead-exports.mjs';
|
|
17
|
-
import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS } from './required-files.mjs';
|
|
17
|
+
import { checkRequiredFiles, REQUIRED_FILE_RULE_IDS, withTreeCache } from './required-files.mjs';
|
|
18
18
|
import { checkClones, CLONE_RULE_IDS } from './clones.mjs';
|
|
19
19
|
import { checkSymbols, SYMBOL_RULE_IDS } from './symbols.mjs';
|
|
20
20
|
import { checkConnectionMap, CONNECTION_RULE_IDS } from './connection-map.mjs';
|
|
@@ -49,6 +49,7 @@ import { checkI18nKeys, I18N_KEYS_RULE_IDS } from './i18n-keys.mjs';
|
|
|
49
49
|
import { checkDocLanguage, DOC_LANGUAGE_RULE_IDS } from './doc-language.mjs';
|
|
50
50
|
import { checkBackendSupabase, checkFrontendSupabase, BACKEND_SUPABASE_RULE_IDS, FRONTEND_SUPABASE_RULE_IDS } from './supabase.mjs';
|
|
51
51
|
import { LINT_CODES, onLintSurface } from './surface.mjs';
|
|
52
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
52
53
|
|
|
53
54
|
export { REGISTRATION_RULE_IDS, SWR_DATA_RULE_IDS };
|
|
54
55
|
|
|
@@ -162,7 +163,7 @@ const machineIds = () => [
|
|
|
162
163
|
export const HFS_MACHINE_RULE_IDS = Object.freeze((() => {
|
|
163
164
|
const ordinary = new Set([...COMMON_RULE_IDS, ...BACKEND_RULE_IDS, ...FRONTEND_RULE_IDS, ...OWNER_RULE_IDS, ...GRAMMAR_RULE_IDS, ...REGISTRATION_RULE_IDS,
|
|
164
165
|
...SWR_DATA_RULE_IDS, SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID, PUBLIC_CONTRACT_RULE_ID, READONLY_BOUNDARY_RULE_ID]);
|
|
165
|
-
return [...new Set(machineIds().filter((id) => !ordinary.has(id)))].sort();
|
|
166
|
+
return [...new Set(machineIds().filter((id) => !ordinary.has(id)))].sort(byCodeUnit);
|
|
166
167
|
})());
|
|
167
168
|
|
|
168
169
|
/** Every code the machine can emit, derived from the rule id lists of its checks (`starci app check` ships exactly these why entries). */
|
|
@@ -171,40 +172,176 @@ export const ARCHITECTURE_RULE_IDS = Object.freeze([...new Set([
|
|
|
171
172
|
...DEAD_EXPORT_RULE_IDS, ...DOC_LANGUAGE_RULE_IDS, ...REQUIRED_FILE_RULE_IDS, ...CLONE_RULE_IDS, ...OWNER_RULE_IDS, ...GRAMMAR_RULE_IDS,
|
|
172
173
|
...REGISTRATION_RULE_IDS, ...SWR_DATA_RULE_IDS, ...SYMBOL_RULE_IDS, SOURCE_LAYOUT_RULE_ID, SOURCE_NAME_RULE_ID, PUBLIC_CONTRACT_RULE_ID,
|
|
173
174
|
READONLY_BOUNDARY_RULE_ID, ...ERROR_RULE_IDS, ...CONFIG_UNREAD_RULE_IDS, ...Object.values(BACKEND_MACHINE).flatMap(([, ids]) => ids), ...Object.values(FRONTEND_MACHINE).flatMap(([, ids]) => ids),
|
|
174
|
-
])].sort());
|
|
175
|
+
])].sort(byCodeUnit));
|
|
175
176
|
|
|
176
177
|
function stable(items) {
|
|
177
178
|
return items.sort((a, b) => `${a.path ?? ''}:${a.line ?? 0}:${a.column ?? 0}:${a.ruleId}`.localeCompare(`${b.path ?? ''}:${b.line ?? 0}:${b.column ?? 0}:${b.ruleId}`));
|
|
178
179
|
}
|
|
179
180
|
|
|
181
|
+
const ownerPublicApiOf = (owners, missingEntries) => {
|
|
182
|
+
if (!owners.length) return { status: 'unavailable', reason: 'no slot owner instance with an entry file exists in the repository' };
|
|
183
|
+
if (missingEntries.length) return { status: 'unavailable', reason: 'one or more declared owner entries are outside the checked production TypeScript or JavaScript program',
|
|
184
|
+
missingEntries: missingEntries.map(owner => owner.entry).sort(byCodeUnit) };
|
|
185
|
+
return { status: 'checked', declarations: owners.length };
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const grammarContractOf = config => {
|
|
189
|
+
if (!config.kinds.includes('frontend')) return { status: 'not-applicable' };
|
|
190
|
+
if (config.frontend.grammar) return { status: 'checked', package: config.frontend.grammar.package };
|
|
191
|
+
return { status: 'unavailable', reason: 'no app has a src/app/globals.css to judge against the Grammar style entry' };
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
function invalidConfigurationResult(repositoryRoot, error) {
|
|
195
|
+
const hfs = checkHfsWithoutConfig(repositoryRoot);
|
|
196
|
+
return { schema: 'starci/architecture-check@1', ok: false, repository: String(repositoryRoot ?? ''), kinds: [], files: 0,
|
|
197
|
+
compiler: null, violations: stable(hfs.violations), coverage: { hfs: hfs.coverage },
|
|
198
|
+
errors: [{ ruleId: /^(HFS_[A-Z_]+):/.exec(String(error.message))?.[1] ?? 'ARCH_CONFIG_INVALID', message: String(error.message ?? error).replace(/^HFS_[A-Z_]+:\s*/, '') }], limitations: LIMITATIONS };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function compilerFailureResult(config, hfs, error) {
|
|
202
|
+
const message = String(error.message ?? error);
|
|
203
|
+
const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
|
|
204
|
+
return { schema: 'starci/architecture-check@1', ok: false, repository: config.root, kinds: config.kinds, files: 0,
|
|
205
|
+
compiler: null, violations: hfs.violations, coverage: { hfs: hfs.coverage },
|
|
206
|
+
errors: [{ ruleId: match?.[1] ?? 'ARCH_COMPILER_FAILURE', message: message.replace(/^(ARCH_[A-Z_]+):\s*/, '') }], limitations: LIMITATIONS };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function runBackendChecks(config, context, paths, injectedTypeScript, violations) {
|
|
210
|
+
violations.push(...checkBackend(config, context));
|
|
211
|
+
// A slice may omit every app root. Registration ownership needs the complete
|
|
212
|
+
// composition graph even when the caller asks for findings in only one path.
|
|
213
|
+
const registrationContext = paths.length && config.backend.moduleRegistration
|
|
214
|
+
? buildTypeScriptContext(config, injectedTypeScript) : context;
|
|
215
|
+
const registration = checkModuleRegistration(config, registrationContext);
|
|
216
|
+
violations.push(...registration.violations);
|
|
217
|
+
const sourceShape = checkBackendSourceShape(config, context);
|
|
218
|
+
violations.push(...sourceShape.violations);
|
|
219
|
+
const contracts = checkBackendContracts(config, context);
|
|
220
|
+
violations.push(...contracts.violations);
|
|
221
|
+
return { moduleRegistration: registration.coverage, backendSourceShape: sourceShape.coverage, backendContractTypeForm: contracts.coverage };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function runFrontendChecks(config, context, violations) {
|
|
225
|
+
let frontendChecked = true;
|
|
226
|
+
try {
|
|
227
|
+
violations.push(...checkFrontend(config, context));
|
|
228
|
+
} catch (error) {
|
|
229
|
+
const message = String(error.message ?? error);
|
|
230
|
+
const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
|
|
231
|
+
if (!match) throw error;
|
|
232
|
+
context.errors.push({ ruleId: match[1], message: message.slice(match[0].length) });
|
|
233
|
+
frontendChecked = false;
|
|
234
|
+
}
|
|
235
|
+
const dataLifecycle = checkFrontendDataLifecycle(config, context);
|
|
236
|
+
violations.push(...dataLifecycle.violations);
|
|
237
|
+
return { frontendChecked, frontendDataLifecycle: dataLifecycle.coverage };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function runHfsMachine(config, context, paths, injectedTypeScript, base, fast, violations) {
|
|
241
|
+
// HFS machine: the slot-driven graph checks read the whole program, also when the caller asked for one path.
|
|
242
|
+
const hfsContext = paths.length ? buildTypeScriptContext(config, injectedTypeScript) : context;
|
|
243
|
+
const hfsChecks = { tiers: null, reachability: null, deadExports: null, requiredFiles: null, clones: null, symbols: null, docLanguage: null };
|
|
244
|
+
if (!hfsContext.program) return hfsChecks;
|
|
245
|
+
const graph = buildHfsGraph(config, hfsContext);
|
|
246
|
+
const input = { config, context: hfsContext, graph, base };
|
|
247
|
+
const runs = { tiers: () => checkTiers(graph), reachability: () => checkReachability(input), deadExports: () => checkDeadExports(input),
|
|
248
|
+
requiredFiles: () => checkRequiredFiles(input), clones: () => checkClones(input), symbols: () => checkSymbols(input), docLanguage: () => checkDocLanguage(input) };
|
|
249
|
+
if (graph.profile === 'be') for (const [name, [check]] of Object.entries(BACKEND_MACHINE)) runs[name] = () => check(input);
|
|
250
|
+
if (graph.profile === 'fe') for (const [name, [check]] of Object.entries(FRONTEND_MACHINE)) runs[name] = () => check(input);
|
|
251
|
+
if (fast) { delete runs.deadExports; delete runs.clones; delete runs.symbols; }
|
|
252
|
+
for (const [name, run] of Object.entries(runs)) {
|
|
253
|
+
const result = run();
|
|
254
|
+
violations.push(...result.violations.map(violation => ({ ...violation, check: name })));
|
|
255
|
+
hfsChecks[name] = result.coverage;
|
|
256
|
+
}
|
|
257
|
+
return hfsChecks;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function hfsRuleIds(hfs) {
|
|
261
|
+
return hfs.coverage.status === 'checked' ? HFS_RULE_IDS : [];
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function hfsCheckRuleIds(hfsChecks) {
|
|
265
|
+
return [
|
|
266
|
+
...(hfsChecks.tiers ? TIER_RULE_IDS : []),
|
|
267
|
+
...(hfsChecks.reachability?.status === 'checked' ? REACHABILITY_RULE_IDS : []),
|
|
268
|
+
...(hfsChecks.deadExports?.status === 'checked' ? DEAD_EXPORT_RULE_IDS : []),
|
|
269
|
+
...(hfsChecks.requiredFiles?.status === 'checked' ? REQUIRED_FILE_RULE_IDS : []),
|
|
270
|
+
...(hfsChecks.clones?.status === 'checked' ? CLONE_RULE_IDS : []),
|
|
271
|
+
...(hfsChecks.symbols?.status === 'checked' ? SYMBOL_RULE_IDS : []),
|
|
272
|
+
...(hfsChecks.docLanguage?.status === 'checked' ? DOC_LANGUAGE_RULE_IDS : []),
|
|
273
|
+
];
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function derivedCoverageRuleIds(coverage) {
|
|
277
|
+
return [
|
|
278
|
+
...(coverage.ownerPublicApi.status === 'checked' ? OWNER_RULE_IDS : []),
|
|
279
|
+
...(coverage.grammarContract.status === 'checked' ? GRAMMAR_RULE_IDS : []),
|
|
280
|
+
...(coverage.moduleRegistration.status === 'checked' ? REGISTRATION_RULE_IDS : []),
|
|
281
|
+
...(coverage.backendSourceShape.layout?.status === 'checked' ? [SOURCE_LAYOUT_RULE_ID] : []),
|
|
282
|
+
...(coverage.backendSourceShape.naming?.status === 'checked' ? [SOURCE_NAME_RULE_ID] : []),
|
|
283
|
+
...(coverage.backendContractTypeForm.publicContracts?.status === 'checked' ? [PUBLIC_CONTRACT_RULE_ID] : []),
|
|
284
|
+
...(coverage.backendContractTypeForm.readonlyBoundaries?.status === 'checked' ? [READONLY_BOUNDARY_RULE_ID] : []),
|
|
285
|
+
];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function checkedRuleIdsFor(config, hfs, hfsChecks, frontendChecked, coverage) {
|
|
289
|
+
const machineRuleIds = Object.entries({ ...BACKEND_MACHINE, ...FRONTEND_MACHINE })
|
|
290
|
+
.flatMap(([name, [, ids]]) => (hfsChecks[name]?.status === 'checked' ? ids : []));
|
|
291
|
+
const checked = [...new Set([
|
|
292
|
+
...COMMON_RULE_IDS,
|
|
293
|
+
...hfsRuleIds(hfs),
|
|
294
|
+
...(config.kinds.includes('backend') ? BACKEND_RULE_IDS : []),
|
|
295
|
+
...hfsCheckRuleIds(hfsChecks),
|
|
296
|
+
...CONFIG_UNREAD_RULE_IDS,
|
|
297
|
+
...machineRuleIds,
|
|
298
|
+
...(frontendChecked ? FRONTEND_RULE_IDS : []),
|
|
299
|
+
...derivedCoverageRuleIds(coverage),
|
|
300
|
+
...(config.kinds.includes('frontend') && ['checked', 'not-applicable'].includes(coverage.frontendDataLifecycle.status) ? SWR_DATA_RULE_IDS : []),
|
|
301
|
+
])];
|
|
302
|
+
return checked.filter(id => !(id.startsWith('FE_') && !config.kinds.includes('frontend')) && !(id.startsWith('BE_') && !config.kinds.includes('backend')))
|
|
303
|
+
.sort(byCodeUnit);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** The errors and the violations a surface reports: findings under the requested paths, on the requested surface, in stable order. */
|
|
307
|
+
function scopeFindings({ config, context, violations, paths, surface }) {
|
|
308
|
+
const inScope = item => !paths.length || (item.path && paths.some(prefix => sameOrUnder(item.path, prefix.replace(/\/$/, ''))));
|
|
309
|
+
// Two findings travel as context errors (package boundary edges); they are obligations of the lint surface like any other finding.
|
|
310
|
+
const asViolation = item => LINT_CODES.has(item.ruleId);
|
|
311
|
+
const allErrors = context.errors.filter(item => item.ruleId.startsWith('ARCH_TSCONFIG_') || !item.path || inScope(item));
|
|
312
|
+
const obligations = surface === 'all' ? violations : [...violations, ...context.errors.filter(asViolation).filter(inScope)];
|
|
313
|
+
// The lint surface is what an editor can show on a line of a TypeScript file; `starci app check` keeps every other finding of the machine.
|
|
314
|
+
const onSurface = item => surface === 'all' || onLintSurface(config.root, item) === (surface === 'lint');
|
|
315
|
+
const errors = stable(surface === 'all' ? allErrors : allErrors.filter(item => !asViolation(item)));
|
|
316
|
+
const scopedViolations = stable(obligations.filter(inScope).filter(onSurface));
|
|
317
|
+
return { errors, scopedViolations };
|
|
318
|
+
}
|
|
319
|
+
|
|
180
320
|
/**
|
|
181
321
|
* Check a target repository. injectedTypeScript exists only for hermetic rule fixtures. `fast` leaves out the checks
|
|
182
322
|
* that read the whole repository to answer (clones, dead exports, repository-wide symbols); the pre-push check of the changed owners uses it.
|
|
183
323
|
*/
|
|
184
|
-
export function checkArchitecture(
|
|
324
|
+
export function checkArchitecture(options = {}) {
|
|
325
|
+
return withTreeCache(() => checkArchitectureOnce(options));
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function checkArchitectureOnce({ repositoryRoot, injectedTypeScript, paths = [], base, fast = false, hfs: openedHfs, surface = 'all' }) {
|
|
185
329
|
if (!['all', 'lint', 'check'].includes(surface)) throw new Error(`unknown surface ${surface}`);
|
|
186
330
|
let config;
|
|
187
331
|
try {
|
|
188
332
|
config = loadArchitectureConfig(repositoryRoot, { hfs: openedHfs });
|
|
189
333
|
} catch (error) {
|
|
190
|
-
|
|
191
|
-
return { schema: 'starci/architecture-check@1', ok: false, repository: String(repositoryRoot ?? ''), kinds: [], files: 0,
|
|
192
|
-
compiler: null, violations: stable(hfs.violations), coverage: { hfs: hfs.coverage },
|
|
193
|
-
errors: [{ ruleId: /^(HFS_[A-Z_]+):/.exec(String(error.message))?.[1] ?? 'ARCH_CONFIG_INVALID', message: String(error.message ?? error).replace(/^HFS_[A-Z_]+:\s*/, '') }], limitations: LIMITATIONS };
|
|
334
|
+
return invalidConfigurationResult(repositoryRoot, error);
|
|
194
335
|
}
|
|
195
336
|
const hfs = checkHfs(config);
|
|
196
337
|
let context;
|
|
197
338
|
try {
|
|
198
339
|
context = buildTypeScriptContext(config, injectedTypeScript, paths);
|
|
199
340
|
} catch (error) {
|
|
200
|
-
|
|
201
|
-
const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
|
|
202
|
-
return { schema: 'starci/architecture-check@1', ok: false, repository: config.root, kinds: config.kinds, files: 0,
|
|
203
|
-
compiler: null, violations: hfs.violations, coverage: { hfs: hfs.coverage },
|
|
204
|
-
errors: [{ ruleId: match?.[1] ?? 'ARCH_COMPILER_FAILURE', message: message.replace(/^(ARCH_[A-Z_]+):\s*/, '') }], limitations: LIMITATIONS };
|
|
341
|
+
return compilerFailureResult(config, hfs, error);
|
|
205
342
|
}
|
|
206
343
|
const violations = [...hfs.violations];
|
|
207
|
-
const configUnread = checkConfigUnread({
|
|
344
|
+
const configUnread = checkConfigUnread({ context });
|
|
208
345
|
violations.push(...configUnread.violations);
|
|
209
346
|
let moduleRegistration = { status: 'not-applicable' };
|
|
210
347
|
let backendSourceShape = { status: 'not-applicable' };
|
|
@@ -212,68 +349,25 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
|
|
|
212
349
|
let frontendDataLifecycle = { status: 'not-applicable' };
|
|
213
350
|
if (context.program) violations.push(...checkOwners(config, context));
|
|
214
351
|
if (context.program && config.kinds.includes('backend')) {
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
? buildTypeScriptContext(config, injectedTypeScript) : context;
|
|
220
|
-
const registration = checkModuleRegistration(config, registrationContext);
|
|
221
|
-
violations.push(...registration.violations);
|
|
222
|
-
moduleRegistration = registration.coverage;
|
|
223
|
-
const sourceShape = checkBackendSourceShape(config, context);
|
|
224
|
-
violations.push(...sourceShape.violations);
|
|
225
|
-
backendSourceShape = sourceShape.coverage;
|
|
226
|
-
const contracts = checkBackendContracts(config, context);
|
|
227
|
-
violations.push(...contracts.violations);
|
|
228
|
-
backendContractTypeForm = contracts.coverage;
|
|
352
|
+
const backend = runBackendChecks(config, context, paths, injectedTypeScript, violations);
|
|
353
|
+
moduleRegistration = backend.moduleRegistration;
|
|
354
|
+
backendSourceShape = backend.backendSourceShape;
|
|
355
|
+
backendContractTypeForm = backend.backendContractTypeForm;
|
|
229
356
|
}
|
|
230
357
|
let frontendChecked = config.kinds.includes('frontend');
|
|
231
358
|
if (context.program && config.kinds.includes('frontend')) {
|
|
232
359
|
// A broken authored input (the framework-pinned root list in knowledge) is an error, never a
|
|
233
360
|
// silently different contract: the frontend rules are then not reported as checked.
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
const message = String(error.message ?? error);
|
|
238
|
-
const match = /^(ARCH_[A-Z_]+):\s*/.exec(message);
|
|
239
|
-
if (!match) throw error;
|
|
240
|
-
context.errors.push({ ruleId: match[1], message: message.slice(match[0].length) });
|
|
241
|
-
frontendChecked = false;
|
|
242
|
-
}
|
|
243
|
-
const dataLifecycle = checkFrontendDataLifecycle(config, context);
|
|
244
|
-
violations.push(...dataLifecycle.violations);
|
|
245
|
-
frontendDataLifecycle = dataLifecycle.coverage;
|
|
246
|
-
}
|
|
247
|
-
// HFS machine: the slot-driven graph checks read the whole program, also when the caller asked for one path.
|
|
248
|
-
const hfsContext = paths.length ? buildTypeScriptContext(config, injectedTypeScript) : context;
|
|
249
|
-
const hfsChecks = { tiers: null, reachability: null, deadExports: null, requiredFiles: null, clones: null, symbols: null, docLanguage: null };
|
|
250
|
-
if (hfsContext.program) {
|
|
251
|
-
const graph = buildHfsGraph(config, hfsContext);
|
|
252
|
-
const input = { config, context: hfsContext, graph, base };
|
|
253
|
-
const runs = { tiers: () => checkTiers(graph), reachability: () => checkReachability(input), deadExports: () => checkDeadExports(input),
|
|
254
|
-
requiredFiles: () => checkRequiredFiles(input), clones: () => checkClones(input), symbols: () => checkSymbols(input), docLanguage: () => checkDocLanguage(input) };
|
|
255
|
-
if (graph.profile === 'be') for (const [name, [check]] of Object.entries(BACKEND_MACHINE)) runs[name] = () => check(input);
|
|
256
|
-
if (graph.profile === 'fe') for (const [name, [check]] of Object.entries(FRONTEND_MACHINE)) runs[name] = () => check(input);
|
|
257
|
-
if (fast) { delete runs.deadExports; delete runs.clones; delete runs.symbols; }
|
|
258
|
-
for (const [name, run] of Object.entries(runs)) {
|
|
259
|
-
const result = run();
|
|
260
|
-
violations.push(...result.violations.map(violation => ({ ...violation, check: name })));
|
|
261
|
-
hfsChecks[name] = result.coverage;
|
|
262
|
-
}
|
|
361
|
+
const frontend = runFrontendChecks(config, context, violations);
|
|
362
|
+
frontendChecked = frontend.frontendChecked;
|
|
363
|
+
frontendDataLifecycle = frontend.frontendDataLifecycle;
|
|
263
364
|
}
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
const asViolation = item => LINT_CODES.has(item.ruleId);
|
|
267
|
-
const allErrors = context.errors.filter(item => item.ruleId.startsWith('ARCH_TSCONFIG_') || !item.path || inScope(item));
|
|
268
|
-
const obligations = surface === 'all' ? violations : [...violations, ...context.errors.filter(asViolation).filter(inScope)];
|
|
269
|
-
// The lint surface is what an editor can show on a line of a TypeScript file; `starci app check` keeps every other finding of the machine.
|
|
270
|
-
const onSurface = item => surface === 'all' || onLintSurface(config.root, item) === (surface === 'lint');
|
|
271
|
-
const errors = stable(surface === 'all' ? allErrors : allErrors.filter(item => !asViolation(item)));
|
|
272
|
-
const scopedViolations = stable(obligations.filter(inScope).filter(onSurface));
|
|
365
|
+
const hfsChecks = runHfsMachine(config, context, paths, injectedTypeScript, base, fast, violations);
|
|
366
|
+
const { errors, scopedViolations } = scopeFindings({ config, context, violations, paths, surface });
|
|
273
367
|
const sourceFiles = new Set(context.files.map(file => canonical(file.fileName)));
|
|
274
368
|
const missingOwnerEntries = config.owners?.filter(owner => !sourceFiles.has(canonical(path.resolve(config.root, ...owner.entry.split('/'))))) ?? [];
|
|
275
369
|
const coverage = {
|
|
276
|
-
sourceFiles: context.files.map(file => relativePath(config.root, canonical(file.fileName))).sort(),
|
|
370
|
+
sourceFiles: context.files.map(file => relativePath(config.root, canonical(file.fileName))).sort(byCodeUnit),
|
|
277
371
|
hfs: hfs.coverage,
|
|
278
372
|
configUnread: configUnread.coverage,
|
|
279
373
|
backendContractTypeForm,
|
|
@@ -281,44 +375,10 @@ export function checkArchitecture({ repositoryRoot, injectedTypeScript, paths =
|
|
|
281
375
|
frontendDataLifecycle,
|
|
282
376
|
moduleRegistration,
|
|
283
377
|
hfsMachine: hfsChecks,
|
|
284
|
-
ownerPublicApi:
|
|
285
|
-
|
|
286
|
-
: missingOwnerEntries.length
|
|
287
|
-
? { status: 'unavailable', reason: 'one or more declared owner entries are outside the checked production TypeScript or JavaScript program',
|
|
288
|
-
missingEntries: missingOwnerEntries.map(owner => owner.entry).sort() }
|
|
289
|
-
: { status: 'checked', declarations: config.owners.length },
|
|
290
|
-
grammarContract: !config.kinds.includes('frontend')
|
|
291
|
-
? { status: 'not-applicable' }
|
|
292
|
-
: config.frontend.grammar
|
|
293
|
-
? { status: 'checked', package: config.frontend.grammar.package }
|
|
294
|
-
: { status: 'unavailable', reason: 'no app has a src/app/globals.css to judge against the Grammar style entry' },
|
|
378
|
+
ownerPublicApi: ownerPublicApiOf(config.owners, missingOwnerEntries),
|
|
379
|
+
grammarContract: grammarContractOf(config),
|
|
295
380
|
};
|
|
296
|
-
coverage.checkedRuleIds =
|
|
297
|
-
...COMMON_RULE_IDS,
|
|
298
|
-
...(hfs.coverage.status === 'checked' ? HFS_RULE_IDS : []),
|
|
299
|
-
...(config.kinds.includes('backend') ? BACKEND_RULE_IDS : []),
|
|
300
|
-
...(hfsChecks.tiers ? TIER_RULE_IDS : []),
|
|
301
|
-
...(hfsChecks.reachability?.status === 'checked' ? REACHABILITY_RULE_IDS : []),
|
|
302
|
-
...(hfsChecks.deadExports?.status === 'checked' ? DEAD_EXPORT_RULE_IDS : []),
|
|
303
|
-
...(hfsChecks.requiredFiles?.status === 'checked' ? REQUIRED_FILE_RULE_IDS : []),
|
|
304
|
-
...(hfsChecks.clones?.status === 'checked' ? CLONE_RULE_IDS : []),
|
|
305
|
-
...(hfsChecks.symbols?.status === 'checked' ? SYMBOL_RULE_IDS : []),
|
|
306
|
-
...(hfsChecks.docLanguage?.status === 'checked' ? DOC_LANGUAGE_RULE_IDS : []),
|
|
307
|
-
...CONFIG_UNREAD_RULE_IDS,
|
|
308
|
-
...Object.entries({ ...BACKEND_MACHINE, ...FRONTEND_MACHINE }).flatMap(([name, [, ids]]) => (hfsChecks[name]?.status === 'checked' ? ids : [])),
|
|
309
|
-
...(frontendChecked ? FRONTEND_RULE_IDS : []),
|
|
310
|
-
...(coverage.ownerPublicApi.status === 'checked' ? OWNER_RULE_IDS : []),
|
|
311
|
-
...(coverage.grammarContract.status === 'checked' ? GRAMMAR_RULE_IDS : []),
|
|
312
|
-
...(coverage.moduleRegistration.status === 'checked' ? REGISTRATION_RULE_IDS : []),
|
|
313
|
-
...(coverage.backendSourceShape.layout?.status === 'checked' ? [SOURCE_LAYOUT_RULE_ID] : []),
|
|
314
|
-
...(coverage.backendSourceShape.naming?.status === 'checked' ? [SOURCE_NAME_RULE_ID] : []),
|
|
315
|
-
...(coverage.backendContractTypeForm.publicContracts?.status === 'checked' ? [PUBLIC_CONTRACT_RULE_ID] : []),
|
|
316
|
-
...(coverage.backendContractTypeForm.readonlyBoundaries?.status === 'checked' ? [READONLY_BOUNDARY_RULE_ID] : []),
|
|
317
|
-
...(config.kinds.includes('frontend') && ['checked', 'not-applicable'].includes(coverage.frontendDataLifecycle.status) ? SWR_DATA_RULE_IDS : []),
|
|
318
|
-
])]
|
|
319
|
-
// A back-end repository checked no FE_ rule and a front-end one no BE_ rule, whatever list a shared check (tiers, required files) carries.
|
|
320
|
-
.filter(id => !(id.startsWith('FE_') && !config.kinds.includes('frontend')) && !(id.startsWith('BE_') && !config.kinds.includes('backend')))
|
|
321
|
-
.sort();
|
|
381
|
+
coverage.checkedRuleIds = checkedRuleIdsFor(config, hfs, hfsChecks, frontendChecked, coverage);
|
|
322
382
|
return {
|
|
323
383
|
schema: 'starci/architecture-check@1',
|
|
324
384
|
ok: errors.length === 0 && scopedViolations.length === 0,
|
|
@@ -15,6 +15,37 @@ export const INJECTION_TOKEN_EXPORTED_RULE_IDS = ['BE_RAW_INJECT'];
|
|
|
15
15
|
|
|
16
16
|
const RULE = 'BE_RAW_INJECT';
|
|
17
17
|
|
|
18
|
+
const exportedVariableHasName = (ts, kit, statement, name) => {
|
|
19
|
+
if (!ts.isVariableStatement(statement) || !kit.isExported(statement)) return false;
|
|
20
|
+
return statement.declarationList.declarations.some(declaration =>
|
|
21
|
+
(ts.isIdentifier(declaration.name) && declaration.name.text === name)
|
|
22
|
+
|| (ts.isObjectBindingPattern(declaration.name) && declaration.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === name)));
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const exportedDeclarationHasName = (ts, statement, name) => {
|
|
26
|
+
if (!ts.isExportDeclaration(statement)) return false;
|
|
27
|
+
const clause = statement.exportClause;
|
|
28
|
+
return clause !== undefined && ts.isNamedExports(clause) && clause.elements.some(element => element.name.text === name);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const exportsName = (ts, kit, file, name) => file?.sourceFile.statements.some(statement =>
|
|
32
|
+
exportedVariableHasName(ts, kit, statement, name) || exportedDeclarationHasName(ts, statement, name)) === true;
|
|
33
|
+
|
|
34
|
+
const indexOf = (graph, file) => (file?.owner ? graph.files.get(`${file.owner.root}/index.ts`) : null);
|
|
35
|
+
|
|
36
|
+
const injectionViolationFor = (graph, kit, ts, dependency) => {
|
|
37
|
+
const decoratorFile = graph.files.get(kit.graphPath(dependency.decoratorDeclaration));
|
|
38
|
+
if (dependency.kind === 'unresolved') {
|
|
39
|
+
return decoratorFile ? { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator,
|
|
40
|
+
message: `${dependency.decorator}() cannot be followed to its token (${dependency.reason}); name an exported \`unique symbol\` const so a unit spec can provide it.` } : null;
|
|
41
|
+
}
|
|
42
|
+
const tokenFile = dependency.declaration ? graph.files.get(kit.graphPath(dependency.declaration)) : null;
|
|
43
|
+
const homes = [decoratorFile, tokenFile && path.posix.basename(tokenFile.rel).endsWith('.decorators.ts') ? tokenFile : null, indexOf(graph, decoratorFile), indexOf(graph, tokenFile)];
|
|
44
|
+
if (!decoratorFile || homes.some(home => exportsName(ts, kit, home, dependency.name))) return null;
|
|
45
|
+
return { ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator, token: dependency.name,
|
|
46
|
+
message: `${dependency.decorator}() injects ${dependency.name}, which is not exported from ${path.posix.basename(decoratorFile.rel)} nor from the capability index.ts; export the token so a unit spec can provide it (\`{ provide: ${dependency.name}, useValue: double }\`).` };
|
|
47
|
+
};
|
|
48
|
+
|
|
18
49
|
export function checkInjectionTokenExported(input) {
|
|
19
50
|
const { graph } = input;
|
|
20
51
|
const kit = machineKit(input);
|
|
@@ -23,39 +54,13 @@ export function checkInjectionTokenExported(input) {
|
|
|
23
54
|
const seen = new Set();
|
|
24
55
|
let decorators = 0;
|
|
25
56
|
|
|
26
|
-
const exportsName = (file, name) => {
|
|
27
|
-
if (!file) return false;
|
|
28
|
-
for (const statement of file.sourceFile.statements) {
|
|
29
|
-
if (ts.isVariableStatement(statement) && kit.isExported(statement)) {
|
|
30
|
-
for (const declaration of statement.declarationList.declarations) {
|
|
31
|
-
if (ts.isIdentifier(declaration.name) && declaration.name.text === name) return true;
|
|
32
|
-
if (ts.isObjectBindingPattern(declaration.name) && declaration.name.elements.some(element => ts.isIdentifier(element.name) && element.name.text === name)) return true;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
if (ts.isExportDeclaration(statement) && statement.exportClause && ts.isNamedExports(statement.exportClause)
|
|
36
|
-
&& statement.exportClause.elements.some(element => element.name.text === name)) return true;
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
};
|
|
40
|
-
const indexOf = file => (file?.owner ? graph.files.get(`${file.owner.root}/index.ts`) : null);
|
|
41
|
-
|
|
42
57
|
for (const { file, declaration } of serviceClasses(kit, graph)) {
|
|
43
58
|
for (const dependency of constructorDependencies(kit, file, declaration)) {
|
|
44
59
|
if (dependency.external || !dependency.decoratorDeclaration || !dependency.injectorCall || seen.has(dependency.injectorCall)) continue;
|
|
45
60
|
seen.add(dependency.injectorCall);
|
|
46
61
|
decorators += 1;
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
49
|
-
if (decoratorFile) violations.push({ ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator,
|
|
50
|
-
message: `${dependency.decorator}() cannot be followed to its token (${dependency.reason}); name an exported \`unique symbol\` const so a unit spec can provide it.` });
|
|
51
|
-
continue;
|
|
52
|
-
}
|
|
53
|
-
const tokenFile = dependency.declaration ? graph.files.get(kit.graphPath(dependency.declaration)) : null;
|
|
54
|
-
const homes = [decoratorFile, tokenFile && path.posix.basename(tokenFile.rel).endsWith('.decorators.ts') ? tokenFile : null, indexOf(decoratorFile), indexOf(tokenFile)];
|
|
55
|
-
if (homes.some(home => exportsName(home, dependency.name))) continue;
|
|
56
|
-
if (!decoratorFile) continue;
|
|
57
|
-
violations.push({ ruleId: RULE, path: decoratorFile.rel, ...kit.at(decoratorFile.rel, decoratorFile.sourceFile, dependency.injectorCall), decorator: dependency.decorator, token: dependency.name,
|
|
58
|
-
message: `${dependency.decorator}() injects ${dependency.name}, which is not exported from ${path.posix.basename(decoratorFile.rel)} nor from the capability index.ts; export the token so a unit spec can provide it (\`{ provide: ${dependency.name}, useValue: double }\`).` });
|
|
62
|
+
const violation = injectionViolationFor(graph, kit, ts, dependency);
|
|
63
|
+
if (violation) violations.push(violation);
|
|
59
64
|
}
|
|
60
65
|
}
|
|
61
66
|
return { violations, coverage: { status: 'checked', decorators, files: graph.files.size } };
|