@starci/hfs 1.0.1 → 2.0.1
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 +51 -0
- package/README.md +110 -12
- package/bin/hfs.mjs +119 -15
- package/emit/compiler.mjs +35 -0
- package/emit/contracts.mjs +97 -0
- package/emit/operations-worker.mjs +24 -0
- package/emit/operations.mjs +126 -0
- package/emit/schema-worker.mjs +117 -0
- package/emit/static-graph.mjs +670 -0
- package/emit/type-schema.mjs +145 -0
- package/package.json +4 -1
- package/report/sonar.mjs +180 -0
- package/runtime/engine/admission.mjs +284 -0
- package/runtime/engine/digest.mjs +10 -0
- package/runtime/engine/ledger-db.mjs +1245 -0
- package/runtime/engine/machine-db.mjs +1484 -0
- package/runtime/engine/migrations/machine/0001-init.sql +887 -0
- package/runtime/engine/migrations/runtime/0001-init.sql +1072 -0
- package/runtime/engine/migrations/runtime/0003-usage-unavailable.sql +13 -0
- package/runtime/engine/migrations/runtime/0004-attempt-why.sql +43 -0
- package/runtime/engine/plain-object.mjs +5 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +16 -58
- package/runtime/knowledge/hfs/slots.yaml +405 -137
- package/runtime/knowledge/patterns/fe/folder.yaml +309 -0
- package/runtime/knowledge/sonar-gate.yaml +85 -0
- package/runtime/modules/kernel/failure-codes.yaml +1480 -16
- package/runtime/scripts/checks/architecture/backend.mjs +350 -0
- package/runtime/scripts/checks/architecture/background-unowned.mjs +107 -0
- package/runtime/scripts/checks/architecture/client-reaches-server.mjs +94 -0
- package/runtime/scripts/checks/architecture/clones.mjs +200 -0
- package/runtime/scripts/checks/architecture/config-unread.mjs +35 -0
- package/runtime/scripts/checks/architecture/config.mjs +310 -0
- package/runtime/scripts/checks/architecture/connection-map.mjs +208 -0
- package/runtime/scripts/checks/architecture/constructor-deps.mjs +100 -0
- package/runtime/scripts/checks/architecture/contract-fixture-guard.mjs +128 -0
- package/runtime/scripts/checks/architecture/contracts.mjs +792 -0
- package/runtime/scripts/checks/architecture/cross-app-duplicate.mjs +73 -0
- package/runtime/scripts/checks/architecture/dead-exports.mjs +265 -0
- package/runtime/scripts/checks/architecture/default-deny.mjs +129 -0
- package/runtime/scripts/checks/architecture/doc-language.mjs +39 -0
- package/runtime/scripts/checks/architecture/entrypoint.mjs +57 -0
- package/runtime/scripts/checks/architecture/error-codes.mjs +45 -0
- package/runtime/scripts/checks/architecture/error-masked.mjs +60 -0
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +38 -0
- package/runtime/scripts/checks/architecture/feature-shape.mjs +49 -0
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +83 -0
- package/runtime/scripts/checks/architecture/frontend.mjs +995 -0
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +61 -0
- package/runtime/scripts/checks/architecture/hfs.mjs +521 -0
- package/runtime/scripts/checks/architecture/hooks-are-hooks.mjs +88 -0
- package/runtime/scripts/checks/architecture/i18n-keys.mjs +146 -0
- package/runtime/scripts/checks/architecture/index.mjs +316 -0
- package/runtime/scripts/checks/architecture/injection-token-exported.mjs +62 -0
- package/runtime/scripts/checks/architecture/machine-ast.mjs +138 -0
- package/runtime/scripts/checks/architecture/module-per-transport.mjs +130 -0
- package/runtime/scripts/checks/architecture/next-data.mjs +775 -0
- package/runtime/scripts/checks/architecture/owners.mjs +89 -0
- package/runtime/scripts/checks/architecture/package-shape.mjs +63 -0
- package/runtime/scripts/checks/architecture/reachability.mjs +233 -0
- package/runtime/scripts/checks/architecture/register-once.mjs +141 -0
- package/runtime/scripts/checks/architecture/registration.mjs +319 -0
- package/runtime/scripts/checks/architecture/required-files.mjs +172 -0
- package/runtime/scripts/checks/architecture/route-files-thin.mjs +97 -0
- package/runtime/scripts/checks/architecture/schema-owner.mjs +261 -0
- package/runtime/scripts/checks/architecture/size-growth.mjs +73 -0
- package/runtime/scripts/checks/architecture/source-names.mjs +607 -0
- package/runtime/scripts/checks/architecture/sql-owner.mjs +142 -0
- package/runtime/scripts/checks/architecture/sql-tokens.mjs +327 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +193 -0
- package/runtime/scripts/checks/architecture/test-world-files.mjs +163 -0
- package/runtime/scripts/checks/architecture/tiers.mjs +130 -0
- package/runtime/scripts/checks/architecture/transport-owner.mjs +112 -0
- package/runtime/scripts/checks/architecture/typescript.mjs +500 -0
- package/runtime/scripts/checks/architecture/unit-spec-providers.mjs +122 -0
- package/runtime/scripts/checks/architecture.mjs +41 -0
- package/runtime/scripts/checks/common.mjs +37 -0
- package/runtime/scripts/checks/typescript-programs.mjs +82 -0
- package/runtime/scripts/lib/artifact-hold.mjs +89 -0
- package/runtime/scripts/lib/artifact-store.mjs +103 -0
- package/runtime/scripts/lib/fs-kind.mjs +10 -0
- package/runtime/scripts/lib/git.mjs +53 -0
- package/runtime/scripts/lib/hfs-allows.mjs +57 -0
- package/runtime/scripts/lib/hfs-check.mjs +254 -28
- package/runtime/scripts/lib/hfs-rules/contract.mjs +126 -0
- package/runtime/scripts/lib/hfs-rules/deps.mjs +63 -0
- package/runtime/scripts/lib/hfs-rules/fe-no-tests.mjs +47 -0
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +124 -0
- package/runtime/scripts/lib/hfs-rules/lint-suppression.mjs +34 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +51 -0
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +64 -0
- package/runtime/scripts/lib/hfs-rules/read.mjs +28 -0
- package/runtime/scripts/lib/hfs-rules/repo-local-checks.mjs +32 -0
- package/runtime/scripts/lib/hfs-rules/secrets.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +31 -0
- package/runtime/scripts/lib/hfs-rules/stacks.mjs +54 -0
- package/runtime/scripts/lib/hfs-rules/test-topology.mjs +31 -0
- package/runtime/scripts/lib/hfs-slots.mjs +88 -40
- package/runtime/scripts/lib/hfs-tree.mjs +80 -0
- package/runtime/scripts/lib/hfs-view.mjs +68 -0
- package/runtime/scripts/lib/json.mjs +22 -0
- package/runtime/scripts/lib/language.mjs +107 -0
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/redact.mjs +148 -0
- package/runtime/scripts/lib/repo-identity.mjs +50 -0
- package/runtime/scripts/lib/safe-remove.mjs +179 -0
- package/runtime/scripts/lib/secret-patterns.mjs +44 -0
- package/runtime/scripts/lib/sleep-sync.mjs +17 -0
- package/runtime/scripts/lib/stack-declaration.mjs +52 -0
- package/runtime/scripts/lib/stack-services.mjs +217 -0
- package/runtime/scripts/lib/test-secrets.mjs +120 -0
- package/scaffold/service.mjs +333 -0
- package/sync/format.mjs +46 -0
- package/sync/hygiene.mjs +56 -24
- package/sync/index.mjs +133 -41
- package/sync/managed.mjs +170 -0
- package/sync/skeleton.mjs +32 -10
- package/sync/sonar-key.mjs +13 -0
- package/sync/ts-strict.mjs +48 -0
- package/templates/{common → be/ci-workflows/github/workflows}/ci.yml +13 -13
- package/templates/be/{e2e.yml → ci-workflows/github/workflows/e2e.yml} +1 -0
- package/templates/be/hooks/husky/pre-commit +13 -0
- package/templates/be/hooks/husky/pre-push +7 -0
- package/templates/be/package-scripts/package.json +22 -0
- package/templates/be/{sonar-project.properties → quality-config/sonar-project.properties} +2 -3
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +14 -4
- package/templates/be/skeleton/apps/__app__/src/main.ts +9 -6
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.contracts.ts +2 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.handler.ts +22 -0
- package/templates/be/skeleton/src/features/system-health/application/check-liveness.query.ts +11 -0
- package/templates/be/skeleton/src/features/system-health/index.ts +1 -1
- package/templates/be/skeleton/src/features/system-health/system-health.module.ts +3 -3
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +8 -9
- package/templates/be/skeleton/src/features/system-health/transport/http/system-health-http.module.ts +3 -6
- package/templates/be/skeleton/src/modules/domain/liveness/index.ts +3 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.contracts.ts +9 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.spec.ts +40 -0
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.service.ts +26 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/clock/clock.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/clock/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.spec.ts +20 -0
- package/templates/be/skeleton/src/modules/platform/clock/system-clock.service.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/config/index.ts +2 -3
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.options.ts +0 -3
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.handler.ts +27 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +8 -12
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +4 -4
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +99 -0
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +38 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +23 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +15 -0
- package/templates/be/tool-config/eslint.config.mjs +3 -0
- package/templates/be/tool-config/jest.config.js +1 -0
- package/templates/be/tool-config/prettierignore +8 -0
- package/templates/be/tool-config/prettierrc +1 -0
- package/templates/be/tool-config/src/tests/tsconfig.json +5 -0
- package/templates/be/tool-config/tsconfig.build.json +5 -0
- package/templates/be/tool-config/tsconfig.json +11 -0
- package/templates/common/gitignore.base +1 -1
- package/templates/fe/ci-workflows/github/workflows/ci.yml +54 -0
- package/templates/fe/hooks/husky/pre-commit +16 -0
- package/templates/fe/hooks/husky/pre-push +6 -0
- package/templates/fe/package-scripts/package.json +17 -0
- package/templates/fe/parts/api-client.ts +44 -0
- package/templates/fe/parts/api-outcome.ts +7 -0
- package/templates/fe/quality-config/sonar-project.properties +8 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +1 -1
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +1 -1
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +3 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +1 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +4 -0
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +12 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +2 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +9 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +5 -0
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +3 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +1 -0
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +5 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +18 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +19 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +2 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +12 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +15 -0
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +5 -0
- package/templates/fe/tool-config/eslint.config.mjs +3 -0
- package/templates/fe/tool-config/prettierignore +10 -0
- package/templates/fe/tool-config/prettierrc +1 -0
- package/templates/fe/tool-config/stylelint.config.mjs +3 -0
- package/templates/fe/tool-config/tsconfig.json +4 -0
- package/templates/be/pre-commit +0 -8
- package/templates/be/skeleton/apps/__app__/src/__app__.composition.spec.ts +0 -31
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.spec.ts +0 -13
- package/templates/be/skeleton/src/modules/platform/config/env-source.spec.ts +0 -36
- package/templates/be/skeleton/src/modules/platform/config/server.config.spec.ts +0 -19
- package/templates/be/skeleton/src/modules/platform/errors/domain-error.spec.ts +0 -15
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.spec.ts +0 -38
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.spec.ts +0 -33
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.ts +0 -35
- package/templates/be/skeleton/src/modules/platform/logging/log-id.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/logging/logger.port.ts +0 -19
- package/templates/common/codecov.yml +0 -13
- package/templates/common/pre-push +0 -5
- package/templates/fe/e2e.yml +0 -22
- package/templates/fe/pre-commit +0 -7
- package/templates/fe/skeleton/apps/__app__/src/app/health/live/route.spec.ts +0 -10
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages.spec.ts +0 -27
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/routing.spec.ts +0 -10
- package/templates/fe/sonar-project.properties +0 -11
- /package/templates/be/skeleton/src/modules/platform/config/{env-source.ts → env-source.config.ts} +0 -0
- /package/templates/be/skeleton/src/modules/platform/errors/{domain-error.ts → domain.error.ts} +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/navigation.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/request.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/modules/i18n/routing.ts +0 -0
- /package/templates/fe/{skeleton → skeleton-app}/apps/__app__/src/proxy.ts +0 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { machineKit } from './machine-ast.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* R39 `error-masked` (BE_ERROR_MASKED). Every api app root provides exactly one `APP_FILTER`, its class declared in
|
|
5
|
+
* platform/errors (the one ExceptionFilter that masks everything that is not a DomainError), and every GraphQL module
|
|
6
|
+
* registration of the program passes the `formatError` platform/errors exports. Filters and format functions defined
|
|
7
|
+
* anywhere else, a second filter, or a GraphQL registration without `formatError` leak internal errors to the client.
|
|
8
|
+
*/
|
|
9
|
+
export const ERROR_MASKED_RULE_IDS = ['BE_ERROR_MASKED'];
|
|
10
|
+
|
|
11
|
+
const RULE = 'BE_ERROR_MASKED';
|
|
12
|
+
|
|
13
|
+
export function checkErrorMasked(input) {
|
|
14
|
+
const { config, graph } = input;
|
|
15
|
+
const kit = machineKit(input);
|
|
16
|
+
const { ts } = kit;
|
|
17
|
+
const violations = [];
|
|
18
|
+
const inErrors = (checker, node) => kit.declarationsOf(checker, node).map(kit.ownerOfDeclaration).some(owner => owner && owner.tier === 'platform' && owner.name === 'errors');
|
|
19
|
+
let apps = 0;
|
|
20
|
+
for (const app of config.apps.filter(item => item.kind === 'api')) {
|
|
21
|
+
const root = kit.appRoot(app.name);
|
|
22
|
+
if (!root) continue;
|
|
23
|
+
apps += 1;
|
|
24
|
+
const filters = kit.providersOf(root, 'APP_FILTER', '@nestjs/core');
|
|
25
|
+
const anchor = filters[0]?.node ?? root.sourceFile.statements[0] ?? root.sourceFile;
|
|
26
|
+
if (filters.length !== 1) {
|
|
27
|
+
violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, anchor), app: app.name,
|
|
28
|
+
message: `App ${app.name} provides ${filters.length} APP_FILTER entries; an api app provides exactly one, the ExceptionFilter of platform/errors.` });
|
|
29
|
+
}
|
|
30
|
+
for (const filter of filters) {
|
|
31
|
+
if (!filter.useClass || !inErrors(filter.checker, filter.useClass)) {
|
|
32
|
+
violations.push({ ruleId: RULE, path: root.rel, ...kit.at(root.rel, root.sourceFile, filter.node), app: app.name,
|
|
33
|
+
message: 'The APP_FILTER must be `useClass` of the filter declared in platform/errors; any other filter can let an undeclared error reach the client unmasked.' });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
let graphql = 0;
|
|
38
|
+
for (const file of graph.files.values()) {
|
|
39
|
+
const checker = kit.checkerOf(file.sourceFile);
|
|
40
|
+
kit.walk(file.sourceFile, node => {
|
|
41
|
+
if (!(ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && ['forRoot', 'forRootAsync'].includes(node.expression.name.text)
|
|
42
|
+
&& kit.isImportOf(checker, node.expression.expression, 'GraphQLModule', '@nestjs/graphql'))) return true;
|
|
43
|
+
graphql += 1;
|
|
44
|
+
let masked = false;
|
|
45
|
+
for (const argument of node.arguments) {
|
|
46
|
+
kit.walk(argument, inner => {
|
|
47
|
+
if ((ts.isPropertyAssignment(inner) || ts.isShorthandPropertyAssignment(inner)) && kit.propertyNameText(inner.name) === 'formatError'
|
|
48
|
+
&& inErrors(checker, kit.valueOfProperty(inner))) masked = true;
|
|
49
|
+
return true;
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
if (!masked) {
|
|
53
|
+
violations.push({ ruleId: RULE, path: file.rel, ...kit.at(file.rel, file.sourceFile, node),
|
|
54
|
+
message: `GraphQLModule.${node.expression.name.text} must pass the \`formatError\` exported by platform/errors; without it a resolver error reaches the client with its internal message.` });
|
|
55
|
+
}
|
|
56
|
+
return true;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
return { violations, coverage: { status: 'checked', apps, graphqlRegistrations: graphql } };
|
|
60
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { treeOf } from './required-files.mjs';
|
|
2
|
+
import { allowsFile } from '../../lib/hfs-allows.mjs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* R94 `fe-slot-allows` (FE_SLOT_FILE_ROLE). A front-end slot that owns a whole directory (`fe.route`, `fe.feature`,
|
|
6
|
+
* `fe.components`, `fe.hooks`, `fe.modules.api`) also says what its instances hold (`allows` in knowledge/hfs/slots.yaml).
|
|
7
|
+
* Every tracked file of such a slot is matched against the `requires` and `allows` entries of its slot; a file no entry names is a
|
|
8
|
+
* finding - `page.tsx` in a hooks domain, a `[lang]` segment under `app/`, a `.test.tsx` beside a component, a stray file at
|
|
9
|
+
* a feature root. A file that sits where the slot expects a folder (`hooks/useX.ts`, so the "domain" is a file name) is one too.
|
|
10
|
+
* Files no slot owns are HFS_PATH_NO_SLOT's (`hfs check`), so together every front-end file is placed by exactly one slot and
|
|
11
|
+
* named by it.
|
|
12
|
+
*/
|
|
13
|
+
export const FE_SLOT_ALLOWS_RULE_IDS = ['FE_SLOT_FILE_ROLE'];
|
|
14
|
+
|
|
15
|
+
const RULE = 'FE_SLOT_FILE_ROLE';
|
|
16
|
+
|
|
17
|
+
export function checkFeSlotAllows({ config, graph }) {
|
|
18
|
+
const resolver = graph.resolver;
|
|
19
|
+
const tree = treeOf(config.root);
|
|
20
|
+
const violations = [];
|
|
21
|
+
let files = 0;
|
|
22
|
+
const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
|
|
23
|
+
for (const file of [...tree.files].sort()) {
|
|
24
|
+
const classified = resolver.classifyPath(file);
|
|
25
|
+
if (classified.status !== 'owned') continue;
|
|
26
|
+
const slot = resolver.slot(classified.slot);
|
|
27
|
+
if (!slot?.allows?.length) continue;
|
|
28
|
+
files += 1;
|
|
29
|
+
if (slot.owner === true && classified.root === classified.path) {
|
|
30
|
+
report(file, `${file} sits where slot ${slot.id} expects a folder (${slot.path}); it holds ${slot.allows.join(', ')} inside a folder of its own. Move it into one.`, { slot: slot.id });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
const verdict = allowsFile(resolver, file);
|
|
34
|
+
if (!verdict || verdict.allowed) continue;
|
|
35
|
+
report(file, `${file} is not allowed in ${verdict.slot} (${verdict.root}); that slot holds only ${verdict.allows.join(', ')}. Rename it to an allowed role or move it to the slot it belongs to.`, { slot: verdict.slot });
|
|
36
|
+
}
|
|
37
|
+
return { violations, coverage: { status: 'checked', files } };
|
|
38
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import { treeOf } from './required-files.mjs';
|
|
3
|
+
import { allowsFile } from '../../lib/hfs-allows.mjs';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* R29 `feature-shape` (BE_FEATURE_SHAPE). A feature root holds `index.ts`, `<feature>.module.ts`, `application/`,
|
|
7
|
+
* `transport/<protocol>/` and `messages/` only, and each of those folders holds only what its slot `allows` (knowledge/hfs/
|
|
8
|
+
* slots.yaml: be.feature, be.feature.application, be.feature.application.support, be.transport.*, be.feature.transport.cli,
|
|
9
|
+
* be.feature.messages). Every tracked file below a feature root is classified by the slot resolver and matched against
|
|
10
|
+
* the `requires` and `allows` entries of the slot that owns it; a directory that no slot owns (an unknown protocol under
|
|
11
|
+
* `transport/`, a `graphql/` folder under `application/`) is refused because its files fall back to the feature root slot.
|
|
12
|
+
*/
|
|
13
|
+
export const FEATURE_SHAPE_RULE_IDS = ['BE_FEATURE_SHAPE'];
|
|
14
|
+
|
|
15
|
+
const RULE = 'BE_FEATURE_SHAPE';
|
|
16
|
+
|
|
17
|
+
export function checkFeatureShape({ config, graph }) {
|
|
18
|
+
const resolver = graph.resolver;
|
|
19
|
+
const tree = treeOf(config.root);
|
|
20
|
+
const violations = [];
|
|
21
|
+
const features = new Set();
|
|
22
|
+
let files = 0;
|
|
23
|
+
const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
|
|
24
|
+
for (const file of [...tree.files].sort()) {
|
|
25
|
+
const owner = resolver.ownerOf(file);
|
|
26
|
+
if (!owner || resolver.slot(owner.slot)?.tier !== 'feature') continue;
|
|
27
|
+
features.add(owner.root);
|
|
28
|
+
files += 1;
|
|
29
|
+
const verdict = allowsFile(resolver, file);
|
|
30
|
+
if (!verdict) continue;
|
|
31
|
+
const feature = path.posix.basename(owner.root);
|
|
32
|
+
const where = `${verdict.slot} (${verdict.root})`;
|
|
33
|
+
if (verdict.slot === owner.slot) {
|
|
34
|
+
// The feature root slot owns only the files at the root; anything deeper sits in a directory no slot claims.
|
|
35
|
+
if (verdict.relative.includes('/')) {
|
|
36
|
+
const folder = verdict.relative.split('/')[0];
|
|
37
|
+
report(file, `${file} sits in ${owner.root}/${folder}/, which no slot owns; a feature root holds only ${verdict.allows.join(', ')}, and transport/ holds one folder per protocol a slot names. Move it to application/ or a transport/<protocol>/ folder, or delete it.`,
|
|
38
|
+
{ feature, folder });
|
|
39
|
+
} else if (!verdict.allowed) {
|
|
40
|
+
report(file, `${file} is not allowed at the root of feature ${feature}; the root holds only ${verdict.allows.join(', ')}. Move it under application/ or transport/<protocol>/.`, { feature });
|
|
41
|
+
}
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (verdict.allowed) continue;
|
|
45
|
+
const forbidden = verdict.forbiddenBy ? ` ${verdict.forbiddenBy} is forbidden there;` : '';
|
|
46
|
+
report(file, `${file} is not allowed in ${where};${forbidden} that folder holds only ${verdict.allows.join(', ')}. Rename it to an allowed role or move it to the folder its role belongs to.`, { feature, slot: verdict.slot });
|
|
47
|
+
}
|
|
48
|
+
return { violations, coverage: { status: 'checked', features: features.size, files } };
|
|
49
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// framework-pinned.mjs - the Next.js files that only load from a source root (the directory holding
|
|
2
|
+
// app/), and the export names the framework mandates in them. Authored once in
|
|
3
|
+
// knowledge/patterns/fe/folder.yaml FE-FOLDER-1 (frameworkPinnedRootFiles, frameworkPinnedRootExports),
|
|
4
|
+
// never hard-coded here (supervisor rulings, nivo wf-nivo-fe-debt-mug06w7h inc-2e42a24b74e4 and
|
|
5
|
+
// inc-846867b9a34e). Read by the architecture check (FE_SOURCE_LAYOUT_INVALID) and the Next name-shape
|
|
6
|
+
// check (FE_SOURCE_NAME_SHAPE).
|
|
7
|
+
//
|
|
8
|
+
// An unreadable or malformed list is a broken install, not "no pinned files": both readers throw
|
|
9
|
+
// ARCH_KNOWLEDGE_UNAVAILABLE so the caller reports an error instead of judging with a different contract.
|
|
10
|
+
import fs from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
14
|
+
|
|
15
|
+
export const FRAMEWORK_PINNED_KNOWLEDGE = fileURLToPath(new URL('../../../knowledge/patterns/fe/folder.yaml', import.meta.url));
|
|
16
|
+
|
|
17
|
+
const cache = new Map(); // only the installed knowledge file is cached; an explicit file is re-read
|
|
18
|
+
const EXACT_NAME = /^[^\\/*?[\]{}\s]+$/;
|
|
19
|
+
|
|
20
|
+
function unavailable(file, detail) {
|
|
21
|
+
return Error(`ARCH_KNOWLEDGE_UNAVAILABLE: ${file} ${detail}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The file stem Next.js keys a pinned file by: `middleware.ts` -> `middleware`, `next-env.d.ts` -> `next-env`. */
|
|
25
|
+
export function pinnedStem(fileName) {
|
|
26
|
+
return path.basename(fileName).replace(/(?:\.d)?\.[cm]?[jt]sx?$/i, '');
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function load(file) {
|
|
30
|
+
if (cache.has(file)) return cache.get(file);
|
|
31
|
+
let rule;
|
|
32
|
+
try {
|
|
33
|
+
rule = parseYaml(fs.readFileSync(file, 'utf8'))?.rules?.find(item => item?.id === 'FE-FOLDER-1');
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw unavailable(file, `cannot be read (${error.message ?? error}).`);
|
|
36
|
+
}
|
|
37
|
+
const list = rule?.frameworkPinnedRootFiles;
|
|
38
|
+
if (!Array.isArray(list) || !list.length || list.some(name => typeof name !== 'string' || !EXACT_NAME.test(name))) {
|
|
39
|
+
throw unavailable(file, 'FE-FOLDER-1 frameworkPinnedRootFiles must be a nonempty list of exact file names.');
|
|
40
|
+
}
|
|
41
|
+
const files = new Set(list);
|
|
42
|
+
const stems = new Set(list.map(pinnedStem));
|
|
43
|
+
const declared = rule.frameworkPinnedRootExports;
|
|
44
|
+
if (!declared || typeof declared !== 'object' || Array.isArray(declared)) {
|
|
45
|
+
throw unavailable(file, 'FE-FOLDER-1 frameworkPinnedRootExports must map a pinned file stem to its framework-mandated export names.');
|
|
46
|
+
}
|
|
47
|
+
const exports = new Map();
|
|
48
|
+
for (const [stem, names] of Object.entries(declared)) {
|
|
49
|
+
if (!stems.has(stem)) throw unavailable(file, `FE-FOLDER-1 frameworkPinnedRootExports names ${stem}, which no frameworkPinnedRootFiles entry pins.`);
|
|
50
|
+
if (!Array.isArray(names) || !names.length || names.some(name => typeof name !== 'string' || !/^[A-Za-z_$][\w$]*$/.test(name))) {
|
|
51
|
+
throw unavailable(file, `FE-FOLDER-1 frameworkPinnedRootExports.${stem} must be a nonempty list of export identifiers.`);
|
|
52
|
+
}
|
|
53
|
+
exports.set(stem, new Set(names));
|
|
54
|
+
}
|
|
55
|
+
const loaded = { files, exports };
|
|
56
|
+
if (file === FRAMEWORK_PINNED_KNOWLEDGE) cache.set(file, loaded);
|
|
57
|
+
return loaded;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Exact basenames Next.js loads only from the source root. */
|
|
61
|
+
export function frameworkPinnedRootFiles(file = FRAMEWORK_PINNED_KNOWLEDGE) {
|
|
62
|
+
return load(file).files;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Map of pinned file stem to the export names the framework mandates there. */
|
|
66
|
+
export function frameworkPinnedRootExports(file = FRAMEWORK_PINNED_KNOWLEDGE) {
|
|
67
|
+
return load(file).exports;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The framework-mandated export names for an absolute source file when it is a pinned file sitting
|
|
72
|
+
* directly in a Next source root (its directory holds an app/ directory), else null.
|
|
73
|
+
*/
|
|
74
|
+
export function frameworkMandatedExports(absoluteFile, file = FRAMEWORK_PINNED_KNOWLEDGE) {
|
|
75
|
+
const { files, exports } = load(file);
|
|
76
|
+
if (!files.has(path.basename(absoluteFile))) return null;
|
|
77
|
+
try {
|
|
78
|
+
if (!fs.statSync(path.join(path.dirname(absoluteFile), 'app')).isDirectory()) return null;
|
|
79
|
+
} catch {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return exports.get(pinnedStem(absoluteFile)) ?? new Set();
|
|
83
|
+
}
|