@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
|
@@ -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
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
3
|
+
|
|
4
|
+
const parsed = new Map();
|
|
5
|
+
|
|
6
|
+
/** The parsed YAML catalog at `file`; the megabyte of text is parsed once per file state (path, size, mtime), not once per check. */
|
|
7
|
+
export function readCatalogOnce(file) {
|
|
8
|
+
const { size, mtimeMs } = fs.statSync(file);
|
|
9
|
+
const key = `${file}|${size}|${mtimeMs}`;
|
|
10
|
+
if (!parsed.has(key)) {
|
|
11
|
+
parsed.clear();
|
|
12
|
+
parsed.set(key, parseYaml(fs.readFileSync(file, 'utf8')));
|
|
13
|
+
}
|
|
14
|
+
return parsed.get(key);
|
|
15
|
+
}
|
|
@@ -57,8 +57,8 @@ import path from 'node:path';
|
|
|
57
57
|
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
|
+
import { readCatalogOnce } from './catalog-once.mjs';
|
|
60
61
|
import { APP_SCOPE, HFS_DECLARATION_FILE, appRelativeMessages, HfsSlotsError, SIDES, createSlotResolver, loadRuleCatalog, loadSlotManifest, readRepoDeclaration, resolveRepoDeclaration } from './slots.mjs';
|
|
61
|
-
import { allowsFile } from './allows.mjs';
|
|
62
62
|
import { RUNTIME_KIND } from './manifest-shape.mjs';
|
|
63
63
|
import { lsFiles } from '../api/git/ls-files.mjs';
|
|
64
64
|
import { diff as gitDiff } from '../api/git/diff.mjs';
|
|
@@ -71,14 +71,15 @@ import { appRootFindings } from './rules/app-root.mjs';
|
|
|
71
71
|
import { frontendFindings } from './rules/frontend-tree.mjs';
|
|
72
72
|
import { lintSuppressionFindings } from './rules/lint-suppression.mjs';
|
|
73
73
|
import { repoLocalCheckFindings } from './rules/repo-local-checks.mjs';
|
|
74
|
-
import { readJson } from './rules/read.mjs';
|
|
75
74
|
import { supabaseSecretFindings } from './rules/supabase-secrets.mjs';
|
|
76
75
|
import { pathFindings } from './path-findings.mjs';
|
|
77
76
|
import { onLintSurface } from './architecture/surface.mjs';
|
|
78
77
|
import { checkAppRoot, trackedTreeView } from './architecture/hfs.mjs';
|
|
79
78
|
import { testTopologyFindings } from './rules/test-topology.mjs';
|
|
80
|
-
import { feNoTestsFindings
|
|
79
|
+
import { feNoTestsFindings } from './rules/fe-no-tests.mjs';
|
|
81
80
|
import { editionFindings } from './rules/edition.mjs';
|
|
81
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
82
|
+
import { pinFindings } from './pin-findings.mjs';
|
|
82
83
|
// 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
84
|
export { checkDatabase } from './rules/database.mjs';
|
|
84
85
|
|
|
@@ -106,16 +107,15 @@ export const CHECK_CODES = Object.freeze([
|
|
|
106
107
|
...REFUSAL_CODES,
|
|
107
108
|
]);
|
|
108
109
|
/** 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());
|
|
110
|
+
export const ALL_CHECK_CODES = Object.freeze([...new Set([...CHECK_CODES, ...ARCHITECTURE_RULE_IDS])].sort(byCodeUnit));
|
|
110
111
|
const SOURCE_EXT = /\.(?:[cm]?[jt]sx?)$/;
|
|
111
112
|
const VAR = /<([a-z][a-z0-9-]*)>/g;
|
|
112
|
-
const DEP_SECTIONS = ['dependencies', 'devDependencies'];
|
|
113
113
|
|
|
114
114
|
const refuse = (code, message, details = {}) => { throw new HfsSlotsError(code, message, details); };
|
|
115
115
|
|
|
116
116
|
/** {code: {title, title_vi, meaning_vi, nextStep_vi}} for the codes asked for, read from the failure-code catalog under `root`. */
|
|
117
117
|
export function readWhy(root = skillRoot, codes = CHECK_CODES) {
|
|
118
|
-
const catalog =
|
|
118
|
+
const catalog = readCatalogOnce(path.join(root, FAILURE_CODES_FILE));
|
|
119
119
|
const why = {};
|
|
120
120
|
for (const code of codes) {
|
|
121
121
|
const entry = catalog?.[code];
|
|
@@ -138,30 +138,9 @@ export function trackedFiles(repoRoot) {
|
|
|
138
138
|
|
|
139
139
|
const fill = (text, bindings) => String(text).replace(VAR, (whole, name) => bindings[name] ?? whole);
|
|
140
140
|
|
|
141
|
-
const pinnedSpec = (spec, pin) => (spec === pin.version ? null : `declared ${spec}, pinned ${pin.version}`);
|
|
142
|
-
|
|
143
141
|
/** The pin map of knowledge/hfs/canon-pins.yaml under `root`. */
|
|
144
142
|
const readPins = (root) => parseYaml(fs.readFileSync(path.join(root, CANON_PINS_FILE), 'utf8'))?.pins ?? {};
|
|
145
143
|
|
|
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
144
|
/** Instances (slot, root, bindings) present in the tracked tree, for every slot that names required files or a minimum. */
|
|
166
145
|
function instancesOf(resolver, files) {
|
|
167
146
|
const found = new Map();
|
|
@@ -221,7 +200,7 @@ function treeFindings({ repoRoot, resolver }) {
|
|
|
221
200
|
const findings = [];
|
|
222
201
|
const facts = treeFacts(readTree(repoRoot, { isIgnored: inIgnoredSlot }));
|
|
223
202
|
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
|
|
203
|
+
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
204
|
}
|
|
226
205
|
for (const { path: dir, of, distance } of facts.ghosts) {
|
|
227
206
|
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 +216,90 @@ function treeFindings({ repoRoot, resolver }) {
|
|
|
237
216
|
/** A side-relative path (or null) as an app-relative one. */
|
|
238
217
|
const onSide = (side, p) => (p ? path.posix.normalize(`${side}/${p}`) : p);
|
|
239
218
|
|
|
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();
|
|
219
|
+
function appendRequiredPathFindings(findings, resolver, files, required, present) {
|
|
257
220
|
const missing = new Set();
|
|
258
221
|
const missingFile = (slot, p, via) => {
|
|
259
222
|
const key = `${slot}|${p}`;
|
|
260
223
|
if (missing.has(key) || present(p)) return;
|
|
261
224
|
missing.add(key);
|
|
262
225
|
const app = appOf(p);
|
|
263
|
-
|
|
226
|
+
const appNote = app ? ` (app ${app})` : '';
|
|
227
|
+
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
228
|
};
|
|
265
|
-
// The app's own required paths; each side reports its own (the resolver of the app lists them too, with their side).
|
|
266
229
|
for (const entry of required.paths) if (!entry.side) missingFile(entry.slot, entry.path, entry.via);
|
|
267
230
|
const instances = instancesOf(resolver, files);
|
|
268
231
|
for (const instance of instances) for (const p of requiredOf(resolver.slot(instance.slot), instance)) missingFile(instance.slot, p, 'requires');
|
|
232
|
+
return instances;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function minimumInstanceFindings(resolver, required, instances) {
|
|
236
|
+
const findings = [];
|
|
269
237
|
for (const { slot, min, appKind, side } of required.minimums) {
|
|
270
238
|
if (appKind !== undefined || side) continue; // an app-kind minimum is checked by requiredPaths (one path set per declared app)
|
|
271
|
-
const count = instances.filter((
|
|
239
|
+
const count = instances.filter((instance) => instance.slot === slot).length;
|
|
272
240
|
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
241
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const pins = readPins(root);
|
|
278
|
-
findings.push(...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }));
|
|
242
|
+
return findings;
|
|
243
|
+
}
|
|
279
244
|
|
|
280
|
-
|
|
281
|
-
findings
|
|
282
|
-
...
|
|
283
|
-
...
|
|
245
|
+
function contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, editionDeclaration }) {
|
|
246
|
+
const findings = [
|
|
247
|
+
...pinFindings({ repoRoot, files, profile: repo.profile, pins, only: scoped }),
|
|
248
|
+
...supabaseSecretFindings({ repoRoot, files: files.filter((file) => inScope(file)), resolver, repo }),
|
|
249
|
+
...editionFindings({ repoRoot, files: files.filter((file) => inScope(file)), repo, resolver, withDeclaration: editionDeclaration }),
|
|
284
250
|
...repoLocalCheckFindings({ repoRoot, files }),
|
|
285
251
|
...lintSuppressionFindings({ repoRoot, files }),
|
|
286
|
-
|
|
252
|
+
];
|
|
287
253
|
if (isRoot) {
|
|
288
254
|
findings.push(
|
|
289
255
|
...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
256
|
...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
257
|
);
|
|
293
258
|
} else if (repo.profile === 'be') {
|
|
294
259
|
findings.push(...contractFindings({ repoRoot, files, repo, resolver }), ...testTopologyFindings({ repoRoot, files }));
|
|
295
260
|
} else {
|
|
296
|
-
findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter(inScope) }));
|
|
261
|
+
findings.push(...frontendFindings({ repoRoot, files, repo }), ...feNoTestsFindings({ repoRoot, files: files.filter((file) => inScope(file)) }));
|
|
297
262
|
}
|
|
263
|
+
return findings;
|
|
264
|
+
}
|
|
298
265
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
}
|
|
266
|
+
function sizeSoftFindings({ repoRoot, resolver, files, inScope }) {
|
|
267
|
+
const findings = [];
|
|
268
|
+
const soft = resolver.ruleParams().fileLines.soft;
|
|
269
|
+
for (const file of files) {
|
|
270
|
+
if (!inScope(file) || !SOURCE_EXT.test(file) || resolver.classifyPath(file).status !== 'owned') continue;
|
|
271
|
+
let lines;
|
|
272
|
+
try { lines = fs.readFileSync(path.join(repoRoot, file), 'utf8').split('\n').length; } catch { continue; }
|
|
273
|
+
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
274
|
}
|
|
308
275
|
return findings;
|
|
309
276
|
}
|
|
310
277
|
|
|
278
|
+
/**
|
|
279
|
+
* 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
|
|
280
|
+
* package.json and lockfile, CI, hooks, .starciwork, .starcistacks) or one side (profile be or fe; the side folder is `repoRoot` and every path is
|
|
281
|
+
* relative to it, exactly as the standalone repository root was). `files` are the scope's tracked paths; `all` (root only) every
|
|
282
|
+
* tracked path of the app, for the rules that read across it (dependency skew, proof commands).
|
|
283
|
+
*/
|
|
284
|
+
function scopeFindings({ repoRoot, root, repo, resolver, files, all = files, scoped, manifest, editionDeclaration = true }) {
|
|
285
|
+
const inScope = (file) => !scoped || scoped.has(file);
|
|
286
|
+
// A required directory of the root (be/, fe/) is present through the files below it, which are the sides' own.
|
|
287
|
+
const trackedSet = new Set(all);
|
|
288
|
+
const present = (p) => (p.endsWith('/') ? all.some((f) => f.startsWith(p)) : trackedSet.has(p));
|
|
289
|
+
const isRoot = repo.profile === APP_SCOPE;
|
|
290
|
+
const findings = pathFindings({ files: files.filter(inScope), resolver, profile: repo.profile });
|
|
291
|
+
const required = resolver.requiredPaths();
|
|
292
|
+
const instances = appendRequiredPathFindings(findings, resolver, files, required, present);
|
|
293
|
+
findings.push(...minimumInstanceFindings(resolver, required, instances));
|
|
294
|
+
// The runtime repository (kind runtime) is judged here by its tree law only; its own rules run in scripts/hfs/runtime-check.mjs.
|
|
295
|
+
if (repo.kind === RUNTIME_KIND) return findings;
|
|
296
|
+
|
|
297
|
+
const pins = readPins(root);
|
|
298
|
+
findings.push(...contentRuleFindings({ repoRoot, repo, resolver, files, all, scoped, pins, inScope, isRoot, editionDeclaration }));
|
|
299
|
+
if (!isRoot) findings.push(...sizeSoftFindings({ repoRoot, resolver, files, inScope }));
|
|
300
|
+
return findings;
|
|
301
|
+
}
|
|
302
|
+
|
|
311
303
|
/**
|
|
312
304
|
* The check of one app. `repoRoot` is the app root (a side folder is checked as that side alone). `declaration` overrides hfs.json
|
|
313
305
|
* (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 +378,7 @@ function changedSince(repoRoot, base) {
|
|
|
386
378
|
|
|
387
379
|
/** The machine's violations and errors as findings: each keeps the machine's rule id as its code. */
|
|
388
380
|
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:
|
|
381
|
+
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
382
|
return [...report.errors.map(of), ...report.violations.map(of)];
|
|
391
383
|
}
|
|
392
384
|
|
|
@@ -438,7 +430,7 @@ function machineOver({ scope, manifest, machine, changed }) {
|
|
|
438
430
|
if (changed) {
|
|
439
431
|
const resolver = createSlotResolver(manifest, readRepoDeclaration(manifest, scope.repoRoot));
|
|
440
432
|
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();
|
|
433
|
+
paths = [...new Set(mine.filter((f) => SOURCE_EXT.test(f) || resolver.ownerOf(f)).map((f) => resolver.ownerOf(f)?.root ?? f))].sort(byCodeUnit);
|
|
442
434
|
if (!paths.length) return { info: { side, status: 'skipped', reason: `no changed source file in ${label}` }, findings: [] };
|
|
443
435
|
}
|
|
444
436
|
let report;
|
|
@@ -485,7 +477,7 @@ export function explainPath({ repoRoot, input, root = skillRoot, declaration, ma
|
|
|
485
477
|
tier: tier ?? 'none',
|
|
486
478
|
owner: owner ? { slot: owner.slot, root: owner.root } : null,
|
|
487
479
|
allowedImports: mayImport,
|
|
488
|
-
importRule: mayImport ? manifest.crossOwner : (tier === 'none'
|
|
480
|
+
importRule: mayImport ? manifest.crossOwner : (tier === 'none' && 'the slot takes no part in import checks' || null),
|
|
489
481
|
tests: slot.tests,
|
|
490
482
|
testsMeaning: TEST_KIND[slot.tests],
|
|
491
483
|
requiredFiles: resolver.requiredFiles(c.path),
|