@starci/hfs 4.0.10 → 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/emit/operations.mjs +1 -1
- package/emit/type-schema.mjs +2 -1
- package/lint/run.mjs +3 -2
- package/package.json +1 -1
- 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 +221 -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/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 +98 -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 +168 -0
- package/runtime/modules/models/registry.yaml +25 -359
- package/runtime/modules/models/runtimes.yaml +92 -256
- 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/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 +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 +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 +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/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/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
|
@@ -3,6 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { machineKit } from './machine-ast.mjs';
|
|
4
4
|
import { constructorDependencies } from './constructor-deps.mjs';
|
|
5
5
|
import { treeOf } from './required-files.mjs';
|
|
6
|
+
import { byCodeUnit } from '../../lib/list.mjs';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* R48 `unit-spec-providers` (BE_SPEC_QUALITY), the unit test standard's rule 3: the unit spec of a service builds its
|
|
@@ -22,7 +23,6 @@ import { treeOf } from './required-files.mjs';
|
|
|
22
23
|
export const UNIT_SPEC_PROVIDERS_RULE_IDS = ['BE_SPEC_QUALITY'];
|
|
23
24
|
|
|
24
25
|
const RULE = 'BE_SPEC_QUALITY';
|
|
25
|
-
const SERVICE_SPEC = /\.service\.spec\.ts$/u;
|
|
26
26
|
const SPEC_ROOT = /^(?:src|apps)\//u;
|
|
27
27
|
|
|
28
28
|
/** local name -> {name, module} for every named import of a source file. */
|
|
@@ -49,6 +49,87 @@ function testingModules(ts, sourceFile) {
|
|
|
49
49
|
|
|
50
50
|
const literalKey = (ts, property) => (ts.isPropertyAssignment(property) || ts.isShorthandPropertyAssignment(property)) && (ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)) ? property.name.text : null;
|
|
51
51
|
|
|
52
|
+
function providerList(ts, call, rel, report) {
|
|
53
|
+
const options = call.arguments[0];
|
|
54
|
+
const property = options && ts.isObjectLiteralExpression(options) ? options.properties.find(item => literalKey(ts, item) === 'providers') : null;
|
|
55
|
+
const list = property && ts.isPropertyAssignment(property) && ts.isArrayLiteralExpression(property.initializer) ? property.initializer : null;
|
|
56
|
+
if (!list) report(call, `${rel}: Test.createTestingModule has no literal \`providers\` array; list the service and each constructor dependency there.`);
|
|
57
|
+
return list;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function readProviders(ts, list, rel, nameOf, report) {
|
|
61
|
+
const provided = [];
|
|
62
|
+
let readable = true;
|
|
63
|
+
for (const element of list.elements) {
|
|
64
|
+
if (ts.isIdentifier(element)) { provided.push({ name: nameOf(element), node: element, isClass: true }); continue; }
|
|
65
|
+
if (ts.isObjectLiteralExpression(element)) {
|
|
66
|
+
const keys = element.properties.map(item => literalKey(ts, item));
|
|
67
|
+
const provide = element.properties.find(item => literalKey(ts, item) === 'provide');
|
|
68
|
+
const value = provide && ts.isPropertyAssignment(provide) ? provide.initializer : null;
|
|
69
|
+
if (keys.length === 2 && keys.includes('useValue') && value && ts.isIdentifier(value)) { provided.push({ name: nameOf(value), node: element, isClass: false }); continue; }
|
|
70
|
+
}
|
|
71
|
+
readable = false;
|
|
72
|
+
report(element, `${rel}: a provider is a class or \`{ provide: TOKEN, useValue: double }\` and nothing else (no useClass, useFactory, useExisting, spread or computed token).`);
|
|
73
|
+
}
|
|
74
|
+
return { provided, readable };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report) {
|
|
78
|
+
const first = list.elements[0];
|
|
79
|
+
const serviceName = first && ts.isIdentifier(first) ? imports.get(first.text) : null;
|
|
80
|
+
const target = serviceName ? path.posix.normalize(path.posix.join(path.posix.dirname(rel), serviceName.module)).replace(/\.[cm]?[jt]s$/u, '') : null;
|
|
81
|
+
const serviceClass = serviceName && target === serviceRel.replace(/\.ts$/u, '')
|
|
82
|
+
? serviceFile.sourceFile.statements.find(statement => ts.isClassDeclaration(statement) && statement.name?.text === serviceName.name) : null;
|
|
83
|
+
if (!serviceClass) report(first ?? list, `${rel}: the first provider is the service under test, imported from ./${path.posix.basename(serviceRel, '.ts')}.`);
|
|
84
|
+
return serviceClass ? { serviceClass, serviceName } : null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function checkDependencies(kit, rel, service, serviceFile, list, provided, report) {
|
|
88
|
+
const { serviceName, serviceClass } = service;
|
|
89
|
+
const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
|
|
90
|
+
const expected = [];
|
|
91
|
+
for (const dependency of dependencies) {
|
|
92
|
+
if (dependency.kind === 'unresolved') {
|
|
93
|
+
report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
expected.push(dependency.name);
|
|
97
|
+
}
|
|
98
|
+
const remaining = [...expected];
|
|
99
|
+
for (const item of provided.slice(1)) {
|
|
100
|
+
const index = remaining.indexOf(item.name);
|
|
101
|
+
if (index === -1) report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
|
|
102
|
+
else remaining.splice(index, 1);
|
|
103
|
+
}
|
|
104
|
+
for (const name of remaining) report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report }) {
|
|
108
|
+
const list = providerList(ts, call, rel, report);
|
|
109
|
+
if (!list) return;
|
|
110
|
+
const nameOf = identifier => imports.get(identifier.text)?.name ?? identifier.text;
|
|
111
|
+
const { provided, readable } = readProviders(ts, list, rel, nameOf, report);
|
|
112
|
+
if (!readable) return;
|
|
113
|
+
const service = serviceClassFor(ts, rel, serviceRel, serviceFile, imports, list, report);
|
|
114
|
+
if (!service) return;
|
|
115
|
+
checkDependencies(kit, rel, service, serviceFile, list, provided, report);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function checkServiceSpec({ config, graph, kit, ts, rel, violations }) {
|
|
119
|
+
const abs = path.join(config.root, ...rel.split('/'));
|
|
120
|
+
if (!fs.existsSync(abs)) return false;
|
|
121
|
+
const sourceFile = ts.createSourceFile(abs, fs.readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
122
|
+
const report = (node, message) => violations.push({ ruleId: RULE, path: rel, ...kit.at(rel, sourceFile, node), message });
|
|
123
|
+
const imports = importsOf(ts, sourceFile);
|
|
124
|
+
const serviceRel = rel.replace(/\.spec\.ts$/u, '.ts');
|
|
125
|
+
const serviceFile = graph.files.get(serviceRel);
|
|
126
|
+
if (!serviceFile) { report(sourceFile, `${rel} has no ${path.posix.basename(serviceRel)} beside it; a service spec sits next to the service it tests.`); return true; }
|
|
127
|
+
const modules = testingModules(ts, sourceFile);
|
|
128
|
+
if (!modules.length) { report(sourceFile, `${rel} never calls Test.createTestingModule({ providers: [...] }); build the service from a testing module holding exactly its constructor dependencies.`); return true; }
|
|
129
|
+
for (const call of modules) checkTestingModule({ ts, kit, rel, serviceRel, serviceFile, imports, call, report });
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
|
|
52
133
|
export function checkUnitSpecProviders(input) {
|
|
53
134
|
const { config, graph } = input;
|
|
54
135
|
const kit = machineKit(input);
|
|
@@ -56,67 +137,8 @@ export function checkUnitSpecProviders(input) {
|
|
|
56
137
|
const violations = [];
|
|
57
138
|
let specs = 0;
|
|
58
139
|
const tree = treeOf(config.root);
|
|
59
|
-
for (const rel of [...tree.files].filter(file => SPEC_ROOT.test(file) &&
|
|
60
|
-
|
|
61
|
-
if (!fs.existsSync(abs)) continue;
|
|
62
|
-
specs += 1;
|
|
63
|
-
const sourceFile = ts.createSourceFile(abs, fs.readFileSync(abs, 'utf8'), ts.ScriptTarget.Latest, true);
|
|
64
|
-
const report = (node, message) => violations.push({ ruleId: RULE, path: rel, ...kit.at(rel, sourceFile, node), message });
|
|
65
|
-
const imports = importsOf(ts, sourceFile);
|
|
66
|
-
const nameOf = identifier => imports.get(identifier.text)?.name ?? identifier.text;
|
|
67
|
-
|
|
68
|
-
const serviceRel = rel.replace(/\.spec\.ts$/u, '.ts');
|
|
69
|
-
const serviceFile = graph.files.get(serviceRel);
|
|
70
|
-
if (!serviceFile) { report(sourceFile, `${rel} has no ${path.posix.basename(serviceRel)} beside it; a service spec sits next to the service it tests.`); continue; }
|
|
71
|
-
|
|
72
|
-
const modules = testingModules(ts, sourceFile);
|
|
73
|
-
if (!modules.length) { report(sourceFile, `${rel} never calls Test.createTestingModule({ providers: [...] }); build the service from a testing module holding exactly its constructor dependencies.`); continue; }
|
|
74
|
-
|
|
75
|
-
for (const call of modules) {
|
|
76
|
-
const options = call.arguments[0];
|
|
77
|
-
const property = options && ts.isObjectLiteralExpression(options) ? options.properties.find(item => literalKey(ts, item) === 'providers') : null;
|
|
78
|
-
const list = property && ts.isPropertyAssignment(property) && ts.isArrayLiteralExpression(property.initializer) ? property.initializer : null;
|
|
79
|
-
if (!list) { report(call, `${rel}: Test.createTestingModule has no literal \`providers\` array; list the service and each constructor dependency there.`); continue; }
|
|
80
|
-
|
|
81
|
-
const provided = []; // [{name, node}]
|
|
82
|
-
let readable = true;
|
|
83
|
-
for (const element of list.elements) {
|
|
84
|
-
if (ts.isIdentifier(element)) { provided.push({ name: nameOf(element), node: element, isClass: true }); continue; }
|
|
85
|
-
if (ts.isObjectLiteralExpression(element)) {
|
|
86
|
-
const keys = element.properties.map(item => literalKey(ts, item));
|
|
87
|
-
const provide = element.properties.find(item => literalKey(ts, item) === 'provide');
|
|
88
|
-
const value = provide && ts.isPropertyAssignment(provide) ? provide.initializer : null;
|
|
89
|
-
if (keys.length === 2 && keys.includes('useValue') && value && ts.isIdentifier(value)) { provided.push({ name: nameOf(value), node: element, isClass: false }); continue; }
|
|
90
|
-
}
|
|
91
|
-
readable = false;
|
|
92
|
-
report(element, `${rel}: a provider is a class or \`{ provide: TOKEN, useValue: double }\` and nothing else (no useClass, useFactory, useExisting, spread or computed token).`);
|
|
93
|
-
}
|
|
94
|
-
if (!readable) continue;
|
|
95
|
-
|
|
96
|
-
const first = list.elements[0];
|
|
97
|
-
const serviceName = first && ts.isIdentifier(first) ? imports.get(first.text) : null;
|
|
98
|
-
const target = serviceName ? path.posix.normalize(path.posix.join(path.posix.dirname(rel), serviceName.module)).replace(/\.[cm]?[jt]s$/u, '') : null;
|
|
99
|
-
const serviceClass = serviceName && target === serviceRel.replace(/\.ts$/u, '')
|
|
100
|
-
? serviceFile.sourceFile.statements.find(statement => ts.isClassDeclaration(statement) && statement.name?.text === serviceName.name) : null;
|
|
101
|
-
if (!serviceClass) { report(first ?? list, `${rel}: the first provider is the service under test, imported from ./${path.posix.basename(serviceRel, '.ts')}.`); continue; }
|
|
102
|
-
|
|
103
|
-
const dependencies = constructorDependencies(kit, serviceFile, serviceClass);
|
|
104
|
-
const expected = [];
|
|
105
|
-
for (const dependency of dependencies) {
|
|
106
|
-
if (dependency.kind === 'unresolved') {
|
|
107
|
-
report(list, `${rel}: a constructor dependency of ${serviceName.name} cannot be resolved to a token (${dependency.reason}), so its provider cannot be checked; inject it through an exported Inject<Thing>() decorator or a class type.`);
|
|
108
|
-
continue;
|
|
109
|
-
}
|
|
110
|
-
expected.push(dependency.name);
|
|
111
|
-
}
|
|
112
|
-
const remaining = [...expected];
|
|
113
|
-
for (const item of provided.slice(1)) {
|
|
114
|
-
const index = remaining.indexOf(item.name);
|
|
115
|
-
if (index === -1) report(item.node, `${rel}: ${item.name} is provided but is not a constructor dependency of ${serviceName.name}; a unit spec provides exactly what the constructor needs.`);
|
|
116
|
-
else remaining.splice(index, 1);
|
|
117
|
-
}
|
|
118
|
-
for (const name of remaining) report(list, `${rel}: ${name} is a constructor dependency of ${serviceName.name} but is not provided; add \`{ provide: ${name}, useValue: double }\` (or the class itself) to the providers.`);
|
|
119
|
-
}
|
|
140
|
+
for (const rel of [...tree.files].filter(file => SPEC_ROOT.test(file) && file.endsWith('.service.spec.ts')).sort(byCodeUnit)) {
|
|
141
|
+
if (checkServiceSpec({ config, graph, kit, ts, rel, violations })) specs += 1;
|
|
120
142
|
}
|
|
121
143
|
return { violations, coverage: { status: 'checked', specs, files: graph.files.size } };
|
|
122
144
|
}
|
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
// Prints one starci/architecture-check@1 record. Exit 0: ok. Exit 1: violations or errors (a check that
|
|
6
6
|
// cannot run is an error, never a pass). Exit 2: bad arguments.
|
|
7
7
|
import path from 'node:path';
|
|
8
|
-
import {fileURLToPath} from 'node:url';
|
|
9
8
|
import {checkArchitecture} from './architecture/index.mjs';
|
|
10
9
|
import { isMain } from '../lib/is-main.mjs';
|
|
11
10
|
|
|
@@ -19,11 +18,11 @@ function parseArchitectureArgs(argv) {
|
|
|
19
18
|
const value = argv[index];
|
|
20
19
|
if (value === '--base') {
|
|
21
20
|
base = argv[++index];
|
|
22
|
-
if (!base) throw Error(`--base needs a commit; ${USAGE}`);
|
|
23
|
-
} else if (value.startsWith('-') || root !== null) throw Error(`unexpected argument ${value}; ${USAGE}`);
|
|
21
|
+
if (!base) throw new Error(`--base needs a commit; ${USAGE}`);
|
|
22
|
+
} else if (value.startsWith('-') || root !== null) throw new Error(`unexpected argument ${value}; ${USAGE}`);
|
|
24
23
|
else root = value;
|
|
25
24
|
}
|
|
26
|
-
if (root === null) throw Error(USAGE);
|
|
25
|
+
if (root === null) throw new Error(USAGE);
|
|
27
26
|
return {root, base};
|
|
28
27
|
}
|
|
29
28
|
|
|
@@ -58,7 +58,6 @@ import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
|
58
58
|
import { ARCHITECTURE_RULE_IDS, checkArchitecture } from './architecture/index.mjs';
|
|
59
59
|
import { parseYaml } from '../../engine/yaml.mjs';
|
|
60
60
|
import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadRuleCatalog, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
|
|
61
|
-
import { allowsFile } from './allows.mjs';
|
|
62
61
|
import { RUNTIME_KIND } from './manifest-shape.mjs';
|
|
63
62
|
import { lsFiles } from '../api/git/ls-files.mjs';
|
|
64
63
|
import { diff as gitDiff } from '../api/git/diff.mjs';
|
|
@@ -71,14 +70,15 @@ import { appRootFindings } from './rules/app-root.mjs';
|
|
|
71
70
|
import { frontendFindings } from './rules/frontend-tree.mjs';
|
|
72
71
|
import { lintSuppressionFindings } from './rules/lint-suppression.mjs';
|
|
73
72
|
import { repoLocalCheckFindings } from './rules/repo-local-checks.mjs';
|
|
74
|
-
import { readJson } from './rules/read.mjs';
|
|
75
73
|
import { supabaseSecretFindings } from './rules/supabase-secrets.mjs';
|
|
76
74
|
import { pathFindings } from './path-findings.mjs';
|
|
77
75
|
import { onLintSurface } from './architecture/surface.mjs';
|
|
78
76
|
import { checkAppRoot, trackedTreeView } from './architecture/hfs.mjs';
|
|
79
77
|
import { testTopologyFindings } from './rules/test-topology.mjs';
|
|
80
|
-
import { feNoTestsFindings
|
|
78
|
+
import { feNoTestsFindings } from './rules/fe-no-tests.mjs';
|
|
81
79
|
import { editionFindings } from './rules/edition.mjs';
|
|
80
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
81
|
+
import { pinFindings } from './pin-findings.mjs';
|
|
82
82
|
// The database rules (R213-R216) read SQL through a WASM parser, so they are async: `starci app check` calls them beside the other emitters and passes the findings in as `extraFindings`.
|
|
83
83
|
export { checkDatabase } from './rules/database.mjs';
|
|
84
84
|
|
|
@@ -106,10 +106,9 @@ export const CHECK_CODES = Object.freeze([
|
|
|
106
106
|
...REFUSAL_CODES,
|
|
107
107
|
]);
|
|
108
108
|
/** Every code `starci app check` can report: its own and every code the architecture machine can emit (derived from the machine's rule id lists). */
|
|
109
|
-
export const ALL_CHECK_CODES = Object.freeze([...new Set([...CHECK_CODES, ...ARCHITECTURE_RULE_IDS])].sort());
|
|
109
|
+
export const ALL_CHECK_CODES = Object.freeze([...new Set([...CHECK_CODES, ...ARCHITECTURE_RULE_IDS])].sort(byCodeUnit));
|
|
110
110
|
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
|
|
111
111
|
const VAR = /<([a-z][a-z0-9-]*)>/g;
|
|
112
|
-
const DEP_SECTIONS = ['dependencies', 'devDependencies'];
|
|
113
112
|
|
|
114
113
|
const refuse = (code, message, details = {}) => { throw new HfsSlotsError(code, message, details); };
|
|
115
114
|
|
|
@@ -138,30 +137,9 @@ export function trackedFiles(repoRoot) {
|
|
|
138
137
|
|
|
139
138
|
const fill = (text, bindings) => String(text).replace(VAR, (whole, name) => bindings[name] ?? whole);
|
|
140
139
|
|
|
141
|
-
const pinnedSpec = (spec, pin) => (spec === pin.version ? null : `declared ${spec}, pinned ${pin.version}`);
|
|
142
|
-
|
|
143
140
|
/** The pin map of knowledge/hfs/canon-pins.yaml under `root`. */
|
|
144
141
|
const readPins = (root) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
|
|
145
142
|
|
|
146
|
-
function pinFindings({ repoRoot, files, profile, pins, only }) {
|
|
147
|
-
const findings = [];
|
|
148
|
-
for (const file of files.filter((f) => (f === 'package.json' || f.endsWith('/package.json')) && (!only || only.has(f)))) {
|
|
149
|
-
let pkg;
|
|
150
|
-
try { pkg = JSON.parse(fs.readFileSync(path.join(repoRoot, file), 'utf8')); } catch { continue; }
|
|
151
|
-
for (const [name, pin] of Object.entries(pins)) {
|
|
152
|
-
// The app root's one package.json carries the pins of both sides.
|
|
153
|
-
if (profile !== APP_SCOPE && pin.side !== 'both' && pin.side !== profile) continue;
|
|
154
|
-
for (const section of DEP_SECTIONS) {
|
|
155
|
-
const spec = pkg[section]?.[name];
|
|
156
|
-
if (spec === undefined) continue;
|
|
157
|
-
const drift = pinnedSpec(spec, pin);
|
|
158
|
-
if (drift) findings.push({ code: 'HFS_CANON_PIN_DRIFT', level: 'error', path: file, dependency: name, section, pinned: pin.version, declared: spec, message: `${name} in ${file} ${section}: ${drift}` });
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
return findings;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
143
|
/** Instances (slot, root, bindings) present in the tracked tree, for every slot that names required files or a minimum. */
|
|
166
144
|
function instancesOf(resolver, files) {
|
|
167
145
|
const found = new Map();
|
|
@@ -221,7 +199,7 @@ function treeFindings({ repoRoot, resolver }) {
|
|
|
221
199
|
const findings = [];
|
|
222
200
|
const facts = treeFacts(readTree(repoRoot, { isIgnored: inIgnoredSlot }));
|
|
223
201
|
for (const { path: dir, below } of facts.empty) {
|
|
224
|
-
findings.push({ code: 'HFS_EMPTY_DIR', level: 'error', path: dir, below, message: `${dir} has no file below it
|
|
202
|
+
findings.push({ code: 'HFS_EMPTY_DIR', level: 'error', path: dir, below, message: `${dir} has no file below it` + (below ? ` (nor in its ${below} sub-director${below === 1 && 'y' || 'ies'})` : '') + '; git tracks no empty directory, so it is a leftover' });
|
|
225
203
|
}
|
|
226
204
|
for (const { path: dir, of, distance } of facts.ghosts) {
|
|
227
205
|
findings.push({ code: 'HFS_GHOST_TREE', level: 'error', path: dir, of, distance, message: `${dir} is empty and ${distance} edit${distance === 1 ? '' : 's'} from its sibling ${of}: a renamed or misspelt structure that was never removed` });
|
|
@@ -237,77 +215,90 @@ function treeFindings({ repoRoot, resolver }) {
|
|
|
237
215
|
/** A side-relative path (or null) as an app-relative one. */
|
|
238
216
|
const onSide = (side, p) => (p ? path.posix.normalize(`${side}/${p}`) : p);
|
|
239
217
|
|
|
240
|
-
|
|
241
|
-
* The findings of one scope: the app root (profile app; its own files, and the rules of the files only the root holds: the one
|
|
242
|
-
* package.json and lockfile, CI, hooks, .starciwork, .starcistacks) or one side (profile be or fe; the side folder is `repoRoot` and every path is
|
|
243
|
-
* relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
|
|
244
|
-
* tracked path of the app, for the rules that read across it (dependency skew, proof commands).
|
|
245
|
-
*/
|
|
246
|
-
function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, editionDeclaration = true }) {
|
|
247
|
-
const inScope = (file) => !scoped || scoped.has(file);
|
|
248
|
-
// A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
|
|
249
|
-
const trackedSet = new Set(all);
|
|
250
|
-
const present = (p) => (p.endsWith('/') ? all.some((f) => f.startsWith(p)) : trackedSet.has(p));
|
|
251
|
-
const findings = [];
|
|
252
|
-
const isRoot = repo.profile === APP_SCOPE;
|
|
253
|
-
|
|
254
|
-
findings.push(...pathFindings({ files: files.filter(inScope), resolver, profile: repo.profile }));
|
|
255
|
-
|
|
256
|
-
const required = resolver.requiredPaths();
|
|
218
|
+
function appendRequiredPathFindings(findings, resolver, files, required, present) {
|
|
257
219
|
const missing = new Set();
|
|
258
220
|
const missingFile = (slot, p, via) => {
|
|
259
221
|
const key = `${slot}|${p}`;
|
|
260
222
|
if (missing.has(key) || present(p)) return;
|
|
261
223
|
missing.add(key);
|
|
262
224
|
const app = appOf(p);
|
|
263
|
-
|
|
225
|
+
const appNote = app ? ` (app ${app})` : '';
|
|
226
|
+
findings.push({ code: 'HFS_SLOT_REQUIRED_MISSING', level: 'error', path: p, slot, via, ...(app ? { app } : {}), message: `${slot} requires ${p}${appNote}, which is not tracked` });
|
|
264
227
|
};
|
|
265
|
-
// The app's own required paths; each side reports its own (the resolver of the app lists them too, with their side).
|
|
266
228
|
for (const entry of required.paths) if (!entry.side) missingFile(entry.slot, entry.path, entry.via);
|
|
267
229
|
const instances = instancesOf(resolver, files);
|
|
268
230
|
for (const instance of instances) for (const p of requiredOf(resolver.slot(instance.slot), instance)) missingFile(instance.slot, p, 'requires');
|
|
231
|
+
return instances;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function minimumInstanceFindings(resolver, required, instances) {
|
|
235
|
+
const findings = [];
|
|
269
236
|
for (const { slot, min, appKind, side } of required.minimums) {
|
|
270
237
|
if (appKind !== undefined || side) continue; // an app-kind minimum is checked by requiredPaths (one path set per declared app)
|
|
271
|
-
const count = instances.filter((
|
|
238
|
+
const count = instances.filter((instance) => instance.slot === slot).length;
|
|
272
239
|
if (count < min) findings.push({ code: 'HFS_MIN_INSTANCES', level: 'error', path: resolver.slot(slot).path, slot, min, count, message: `${slot} needs at least ${min} instance${min === 1 ? '' : 's'} (${resolver.slot(slot).path}), found ${count}` });
|
|
273
240
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const pins = readPins(root);
|
|
278
|
-
findings.push(...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }));
|
|
241
|
+
return findings;
|
|
242
|
+
}
|
|
279
243
|
|
|
280
|
-
|
|
281
|
-
findings
|
|
282
|
-
...
|
|
283
|
-
...
|
|
244
|
+
function contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, editionDeclaration }) {
|
|
245
|
+
const findings = [
|
|
246
|
+
...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }),
|
|
247
|
+
...supabaseSecretFindings({ repoRoot, files: files.filter((file) => inScope(file)), resolver, repo }),
|
|
248
|
+
...editionFindings({ repoRoot, files: files.filter((file) => inScope(file)), repo, resolver, withDeclaration: editionDeclaration }),
|
|
284
249
|
...repoLocalCheckFindings({ repoRoot, files }),
|
|
285
250
|
...lintSuppressionFindings({ repoRoot, files }),
|
|
286
|
-
|
|
251
|
+
];
|
|
287
252
|
if (isRoot) {
|
|
288
253
|
findings.push(
|
|
289
254
|
...appRootFindings({ repoRoot, files, all, repo, resolver, pins }),
|
|
290
|
-
// The tree check of the app root the machine runs per side for a side folder: README, root entries, automatic gates, hooks path.
|
|
291
255
|
...checkAppRoot({ root: repoRoot, resolver, tree: trackedTreeView(all) }).violations.map((item) => ({ code: item.ruleId, level: 'error', path: item.path, line: item.line, column: item.column, source: 'machine', message: `${item.path}: ${item.message}` })),
|
|
292
256
|
);
|
|
293
257
|
} else if (repo.profile === 'be') {
|
|
294
258
|
findings.push(...contractFindings({ repoRoot, files, repo, resolver }), ...testTopologyFindings({ repoRoot, files }));
|
|
295
259
|
} else {
|
|
296
|
-
findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter(inScope) }));
|
|
260
|
+
findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter((file) => inScope(file)) }));
|
|
297
261
|
}
|
|
262
|
+
return findings;
|
|
263
|
+
}
|
|
298
264
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
265
|
+
function sizeSoftFindings({ repoRoot, resolver, files, inScope }) {
|
|
266
|
+
const findings = [];
|
|
267
|
+
const soft = resolver.ruleParams().fileLines.soft;
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
if (!inScope(file) || !SOURCE_EXT.test(file) || resolver.classifyPath(file).status !== 'owned') continue;
|
|
270
|
+
let lines;
|
|
271
|
+
try { lines = fs.readFileSync(path.join(repoRoot, file), 'utf8').split('\n').length; } catch { continue; }
|
|
272
|
+
if (lines > soft) findings.push({ code: 'HFS_SIZE_SOFT_BACKLOG', level: 'info', path: file, lines, soft, message: `${file} has ${lines} lines, above the soft size ${soft}; report only` });
|
|
307
273
|
}
|
|
308
274
|
return findings;
|
|
309
275
|
}
|
|
310
276
|
|
|
277
|
+
/**
|
|
278
|
+
* The findings of one scope: the app root (profile app; its own files, and the rules of the files only the root holds: the one
|
|
279
|
+
* package.json and lockfile, CI, hooks, .starciwork, .starcistacks) or one side (profile be or fe; the side folder is `repoRoot` and every path is
|
|
280
|
+
* relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
|
|
281
|
+
* tracked path of the app, for the rules that read across it (dependency skew, proof commands).
|
|
282
|
+
*/
|
|
283
|
+
function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, editionDeclaration = true }) {
|
|
284
|
+
const inScope = (file) => !scoped || scoped.has(file);
|
|
285
|
+
// A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
|
|
286
|
+
const trackedSet = new Set(all);
|
|
287
|
+
const present = (p) => (p.endsWith('/') ? all.some((f) => f.startsWith(p)) : trackedSet.has(p));
|
|
288
|
+
const isRoot = repo.profile === APP_SCOPE;
|
|
289
|
+
const findings = pathFindings({ files: files.filter(inScope), resolver, profile: repo.profile });
|
|
290
|
+
const required = resolver.requiredPaths();
|
|
291
|
+
const instances = appendRequiredPathFindings(findings, resolver, files, required, present);
|
|
292
|
+
findings.push(...minimumInstanceFindings(resolver, required, instances));
|
|
293
|
+
// The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
|
|
294
|
+
if (repo.kind === RUNTIME_KIND) return findings;
|
|
295
|
+
|
|
296
|
+
const pins = readPins(root);
|
|
297
|
+
findings.push(...contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, editionDeclaration }));
|
|
298
|
+
if (!isRoot) findings.push(...sizeSoftFindings({ repoRoot, resolver, files, inScope }));
|
|
299
|
+
return findings;
|
|
300
|
+
}
|
|
301
|
+
|
|
311
302
|
/**
|
|
312
303
|
* The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `declaration` overrides hfs.json
|
|
313
304
|
* (a dry run over an app that has none); `files` overrides git ls-files (specs), app-relative. `only` (a list of app-relative paths)
|
|
@@ -386,7 +377,7 @@ function changedSince(repoRoot, base) {
|
|
|
386
377
|
|
|
387
378
|
/** The machine's violations and errors as findings: each keeps the machine's rule id as its code. */
|
|
388
379
|
function machineFindings(report) {
|
|
389
|
-
const of = (item) => ({ code: item.ruleId, level: 'error', ...(item.path ? { path: item.path } : {}), ...(item.line ? { line: item.line, column: item.column } : {}), source: 'machine', message:
|
|
380
|
+
const of = (item) => ({ code: item.ruleId, level: 'error', ...(item.path ? { path: item.path } : {}), ...(item.line ? { line: item.line, column: item.column } : {}), source: 'machine', message: ((item.path && item.path + (item.line && ':' + item.line || '') + ': ') || '') + item.message });
|
|
390
381
|
return [...report.errors.map(of), ...report.violations.map(of)];
|
|
391
382
|
}
|
|
392
383
|
|
|
@@ -438,7 +429,7 @@ function machineOver({ scope, manifest, machine, changed }) {
|
|
|
438
429
|
if (changed) {
|
|
439
430
|
const resolver = createSlotResolver(manifest, readRepoDeclaration(manifest, scope.repoRoot));
|
|
440
431
|
const mine = changed.files.filter((f) => f.startsWith(prefix)).map((f) => f.slice(prefix.length));
|
|
441
|
-
paths = [...new Set(mine.filter((f) => SOURCE_EXT.test(f) || resolver.ownerOf(f)).map((f) => resolver.ownerOf(f)?.root ?? f))].sort();
|
|
432
|
+
paths = [...new Set(mine.filter((f) => SOURCE_EXT.test(f) || resolver.ownerOf(f)).map((f) => resolver.ownerOf(f)?.root ?? f))].sort(byCodeUnit);
|
|
442
433
|
if (!paths.length) return { info: { side, status: 'skipped', reason: `no changed source file in ${label}` }, findings: [] };
|
|
443
434
|
}
|
|
444
435
|
let report;
|
|
@@ -485,7 +476,7 @@ export function explainPath({ repoRoot, input, root = skillRoot, declaration, ma
|
|
|
485
476
|
tier: tier ?? 'none',
|
|
486
477
|
owner: owner ? { slot: owner.slot, root: owner.root } : null,
|
|
487
478
|
allowedImports: mayImport,
|
|
488
|
-
importRule: mayImport ? manifest.crossOwner : (tier === 'none'
|
|
479
|
+
importRule: mayImport ? manifest.crossOwner : (tier === 'none' && 'the slot takes no part in import checks' || null),
|
|
489
480
|
tests: slot.tests,
|
|
490
481
|
testsMeaning: TEST_KIND[slot.tests],
|
|
491
482
|
requiredFiles: resolver.requiredFiles(c.path),
|
|
@@ -13,18 +13,38 @@
|
|
|
13
13
|
// and `starci app check` compares the files with this derivation. Nothing here lists a path of a product: it all comes from slots.yaml.
|
|
14
14
|
import { braceVariants, globExpression } from '../lib/glob.mjs';
|
|
15
15
|
import { logicRolesOf } from './manifest-shape.mjs';
|
|
16
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
17
|
+
import { trimTrailingSlashes } from './trailing-slashes.mjs';
|
|
16
18
|
|
|
17
19
|
/** The component that holds the shared infrastructure: the platform tier and every capability more than one service app composes. */
|
|
18
20
|
export const PLATFORM_COMPONENT = 'platform';
|
|
19
21
|
const SERVICE_APP_KINDS = new Set(['api', 'worker']);
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
/** `text` with every `<placeholder>` (a `<`, at least one character, the next `>`) replaced by `*`; a `<` with no closing `>` stays. */
|
|
23
|
+
export const star = (text) => {
|
|
24
|
+
const source = String(text);
|
|
25
|
+
let result = '';
|
|
26
|
+
let copied = 0;
|
|
27
|
+
let open = source.indexOf('<');
|
|
28
|
+
while (open !== -1) {
|
|
29
|
+
const close = source.indexOf('>', open + 1);
|
|
30
|
+
if (close === -1) break;
|
|
31
|
+
if (close === open + 1) {
|
|
32
|
+
open = source.indexOf('<', close);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
result += `${source.slice(copied, open)}*`;
|
|
36
|
+
copied = close + 1;
|
|
37
|
+
open = source.indexOf('<', copied);
|
|
38
|
+
}
|
|
39
|
+
return result + source.slice(copied);
|
|
40
|
+
};
|
|
41
|
+
export const segments = (dir, trimSlashes = false) => (trimSlashes ? trimTrailingSlashes(dir) : dir).split('/').filter(Boolean);
|
|
22
42
|
const isDir = (path) => path.endsWith('/');
|
|
23
43
|
|
|
24
44
|
/** True when directory pattern `inner` lies inside `outer` (an outer `*` segment matches any one inner segment) or is the same. */
|
|
25
|
-
function nested(inner, outer) {
|
|
26
|
-
const a = segments(inner);
|
|
27
|
-
const b = segments(outer);
|
|
45
|
+
export function nested(inner, outer) {
|
|
46
|
+
const a = segments(inner, true);
|
|
47
|
+
const b = segments(outer, true);
|
|
28
48
|
return a.length >= b.length && b.every((segment, index) => segment === '*' ? true : segment === a[index]);
|
|
29
49
|
}
|
|
30
50
|
|
|
@@ -37,6 +57,24 @@ const variantsOf = (slot) => braceVariants(slot.path).map(star);
|
|
|
37
57
|
*/
|
|
38
58
|
const beSlots = (manifest, providers) => manifest.slots.filter((slot) => slot.profiles.includes('be') && slot.tracked === 'tracked' && (slot.provider === undefined || providers.includes(slot.provider)));
|
|
39
59
|
|
|
60
|
+
function addRequiredPatterns(sonar, required, suffixes) {
|
|
61
|
+
const escapedSuffixes = suffixes.map((suffix) => suffix.replaceAll('-', String.raw`\-`)).join('|');
|
|
62
|
+
const isRole = new RegExp(String.raw`\.(?:${escapedSuffixes})\.ts$`);
|
|
63
|
+
for (const slot of required) {
|
|
64
|
+
for (const entry of [...(slot.requires ?? []), ...(slot.allows ?? [])]) {
|
|
65
|
+
addEntryPatterns(sonar, slot, entry, isRole);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function addEntryPatterns(sonar, slot, entry, isRole) {
|
|
71
|
+
if (!entry.endsWith('.ts') || entry.includes('<role>')) return;
|
|
72
|
+
for (const name of braceVariants(entry)) {
|
|
73
|
+
if (isRole.test(star(name).replaceAll('*', 'x'))) continue;
|
|
74
|
+
for (const dir of variantsOf(slot)) sonar.add(`be/${dir}${star(name)}`.replace(/\/\/+/g, '/'));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
40
78
|
/** The directory a path pattern lives in (itself when it is one). */
|
|
41
79
|
const dirOf = (path) => (isDir(path) ? path : path.slice(0, path.lastIndexOf('/') + 1));
|
|
42
80
|
|
|
@@ -64,31 +102,26 @@ export function coverageScope(manifest, providers = []) {
|
|
|
64
102
|
if (!isDir(dir)) throw new Error(`slot ${slot.id} is coverage: required but its path ${slot.path} is not a directory`);
|
|
65
103
|
return dir;
|
|
66
104
|
}));
|
|
67
|
-
const roots = minimal(requiredDirs).sort();
|
|
105
|
+
const roots = minimal(requiredDirs).sort(byCodeUnit);
|
|
68
106
|
const nonePaths = none.flatMap((slot) => variantsOf(slot));
|
|
69
107
|
for (const dir of nonePaths.filter(isDir)) if (requiredDirs.some((other) => nested(other, dir))) throw new Error(`a coverage: none directory ${dir} contains a coverage: required slot`);
|
|
70
108
|
const noneUnique = [...new Set(nonePaths)];
|
|
71
109
|
const noneMinimal = noneUnique.filter((path) => !noneUnique.some((other) => other !== path && isDir(other) && nested(dirOf(path), other)));
|
|
72
|
-
const excludes = noneMinimal.filter((path) => isDir(path) && roots.some((root) => nested(path, root))).sort();
|
|
110
|
+
const excludes = noneMinimal.filter((path) => isDir(path) && roots.some((root) => nested(path, root))).sort(byCodeUnit);
|
|
73
111
|
const suffixes = manifest.ruleParams.be.suffixes;
|
|
74
|
-
const isRole = new RegExp(`\\.(?:${suffixes.map((s) => s.replace(/-/g, '\\-')).join('|')})\\.ts$`);
|
|
75
112
|
const sonar = new Set(noneMinimal.map((path) => `be/${path}${isDir(path) ? '**' : ''}`));
|
|
76
113
|
for (const role of suffixes.filter((suffix) => !roles.includes(suffix))) sonar.add(`be/**/*.${role}.ts`);
|
|
77
|
-
|
|
78
|
-
for (const entry of [...(slot.requires ?? []), ...(slot.allows ?? [])]) {
|
|
79
|
-
if (!/\.ts$/.test(entry) || entry.includes('<role>')) continue;
|
|
80
|
-
for (const name of braceVariants(entry)) {
|
|
81
|
-
if (isRole.test(star(name).replace(/\*/g, 'x'))) continue;
|
|
82
|
-
for (const dir of variantsOf(slot)) sonar.add(`be/${dir}${star(name)}`.replace(/\/\/+/g, '/'));
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
114
|
+
addRequiredPatterns(sonar, required, suffixes);
|
|
86
115
|
sonar.add('fe/**');
|
|
87
|
-
|
|
116
|
+
noneMinimal.sort(byCodeUnit);
|
|
117
|
+
return { roles, roots, excludes, none: noneMinimal, sonar: [...sonar].sort(byCodeUnit), codecovPaths: roots.map((root) => `be/${root}**`) };
|
|
88
118
|
}
|
|
89
119
|
|
|
90
120
|
/** The glob a root measures: every file of a logic role below it (`src/modules/domain/*\/**\/*.{service,policy}.ts`). */
|
|
91
|
-
export const rootGlob = (root, roles) =>
|
|
121
|
+
export const rootGlob = (root, roles) => {
|
|
122
|
+
const roleGlob = roles.length === 1 ? roles[0] : `{${roles.join(',')}}`;
|
|
123
|
+
return `${root}**/*.${roleGlob}.ts`;
|
|
124
|
+
};
|
|
92
125
|
|
|
93
126
|
/** The jest coverage options of the rendered be/jest.config.js: { roots, roles, excludes }. */
|
|
94
127
|
export function jestCoverage(manifest, providers = []) {
|
|
@@ -107,13 +140,17 @@ export const codecovPaths = (manifest, providers = []) => coverageScope(manifest
|
|
|
107
140
|
* (`src/modules/domain/*\/` and `src/modules/domain/order/order.service.ts` give `order`).
|
|
108
141
|
*/
|
|
109
142
|
function capabilityOf(root, file) {
|
|
110
|
-
const parts = segments(root);
|
|
143
|
+
const parts = segments(root, true);
|
|
111
144
|
const at = parts.length - 1;
|
|
112
145
|
if (parts[at] !== '*') return null;
|
|
113
146
|
const own = file.split('/');
|
|
114
147
|
return parts.slice(0, at).every((part, index) => part === own[index]) && own.length > at + 1 ? own[at] : null;
|
|
115
148
|
}
|
|
116
149
|
|
|
150
|
+
function escapeImportPart(part) {
|
|
151
|
+
return part.replace(/[/.]/g, String.raw`\$&`);
|
|
152
|
+
}
|
|
153
|
+
|
|
117
154
|
/**
|
|
118
155
|
* The Codecov components of an app, [{ id, name, paths }] (paths repository-relative from the app root): one per service app (api or worker)
|
|
119
156
|
* that composes the capabilities nothing else composes, then `platform`. A capability of a root whose last segment is a placeholder
|
|
@@ -130,28 +167,37 @@ export function coverageComponents(manifest, { files, read, apps, providers = []
|
|
|
130
167
|
const imports = new Map(services.map((app) => [app.name, files.filter((file) => file.startsWith(`apps/${app.name}/`) && /\.[cm]?ts$/.test(file)).map((file) => read(file)).join('\n')]));
|
|
131
168
|
const platformPaths = [];
|
|
132
169
|
const ownedBy = new Map(services.map((app) => [app.name, []]));
|
|
133
|
-
for (const root of roots) {
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
}
|
|
141
|
-
for (const capability of capabilities) {
|
|
142
|
-
const spec = new RegExp(`from\\s+["']@modules/${module.replace(/[/.]/g, '\\$&')}/${capability.replace(/[/.]/g, '\\$&')}["']`);
|
|
143
|
-
const importers = services.filter((app) => spec.test(imports.get(app.name)));
|
|
144
|
-
const owners = importers.length === 1 ? importers : importers.filter((app) => app.name === capability);
|
|
145
|
-
if (owners.length === 1) ownedBy.get(owners[0].name).push(`be/${base}${capability}/**`);
|
|
146
|
-
else platformPaths.push(`be/${base}${capability}/**`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
170
|
+
for (const root of roots) assignRoot({ root, files, services, imports, platformPaths, ownedBy });
|
|
171
|
+
const components = services.filter((app) => ownedBy.get(app.name).length).map((app) => {
|
|
172
|
+
const paths = ownedBy.get(app.name);
|
|
173
|
+
paths.sort(byCodeUnit);
|
|
174
|
+
return { id: app.name, name: app.name, paths };
|
|
175
|
+
});
|
|
176
|
+
if (platformPaths.length) platformPaths.sort(byCodeUnit);
|
|
149
177
|
return [
|
|
150
|
-
...
|
|
151
|
-
...(platformPaths.length ? [{ id: PLATFORM_COMPONENT, name: PLATFORM_COMPONENT, paths: platformPaths
|
|
178
|
+
...components,
|
|
179
|
+
...(platformPaths.length ? [{ id: PLATFORM_COMPONENT, name: PLATFORM_COMPONENT, paths: platformPaths }] : []),
|
|
152
180
|
];
|
|
153
181
|
}
|
|
154
182
|
|
|
183
|
+
/** Puts the measured root, or each capability below it, into the platform paths or the paths of the one service app that composes it. */
|
|
184
|
+
function assignRoot({ root, files, services, imports, platformPaths, ownedBy }) {
|
|
185
|
+
const module = /^src\/modules\/(.+)\/\*\/$/.exec(root)?.[1] ?? null;
|
|
186
|
+
const capabilities = [...new Set(files.map((file) => capabilityOf(root, file)).filter(Boolean))].sort(byCodeUnit);
|
|
187
|
+
const base = root.slice(0, -2);
|
|
188
|
+
if (module === null || module === 'platform' || !capabilities.length) {
|
|
189
|
+
platformPaths.push(`be/${root}**`);
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
for (const capability of capabilities) {
|
|
193
|
+
const spec = new RegExp(String.raw`from\s+["']@modules/${escapeImportPart(module)}/${escapeImportPart(capability)}["']`);
|
|
194
|
+
const importers = services.filter((app) => spec.test(imports.get(app.name)));
|
|
195
|
+
const owners = importers.length === 1 ? importers : importers.filter((app) => app.name === capability);
|
|
196
|
+
if (owners.length === 1) ownedBy.get(owners[0].name).push(`be/${base}${capability}/**`);
|
|
197
|
+
else platformPaths.push(`be/${base}${capability}/**`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
155
201
|
/** True when the be-relative `file` is measured: it is `<name>.<role>.ts` of a logic role below a measured root and inside no none directory of it. */
|
|
156
202
|
export function isMeasured(manifest, file) {
|
|
157
203
|
const { roots, roles, excludes } = coverageScope(manifest);
|