@starci/hfs 2.0.2 → 4.0.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 +25 -0
- package/README.md +33 -33
- package/bin/hfs.mjs +94 -68
- package/lint/run.mjs +161 -0
- package/package.json +3 -2
- package/report/sonar.mjs +14 -29
- package/runtime/engine/admission.mjs +3 -3
- package/runtime/engine/ledger-db.mjs +2 -2
- package/runtime/engine/machine-db.mjs +90 -9
- package/runtime/engine/migrations/machine/0002-worktrees-no-workflow-kind.sql +13 -0
- package/runtime/engine/migrations/runtime/0005-ended-workflow-views.sql +93 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +32 -13
- package/runtime/knowledge/hfs/peer-integrations.yaml +18 -0
- package/runtime/knowledge/hfs/slots.yaml +193 -128
- package/runtime/knowledge/patterns/fe/folder.yaml +36 -36
- package/runtime/knowledge/sonar-gate.yaml +8 -7
- package/runtime/modules/kernel/failure-codes.yaml +31 -52
- package/runtime/scripts/checks/architecture/backend.mjs +1 -1
- package/runtime/scripts/checks/architecture/config.mjs +31 -11
- package/runtime/scripts/checks/architecture/contracts.mjs +4 -4
- package/runtime/scripts/checks/architecture/fe-slot-allows.mjs +7 -3
- package/runtime/scripts/checks/architecture/framework-pinned.mjs +5 -47
- package/runtime/scripts/checks/architecture/frontend.mjs +6 -4
- package/runtime/scripts/checks/architecture/hfs-graph.mjs +1 -1
- package/runtime/scripts/checks/architecture/hfs.mjs +105 -67
- package/runtime/scripts/checks/architecture/index.mjs +18 -13
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/owners.mjs +12 -6
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- package/runtime/scripts/checks/architecture/surface.mjs +91 -0
- package/runtime/scripts/checks/architecture/symbols.mjs +13 -2
- package/runtime/scripts/checks/architecture/test-world-files.mjs +83 -45
- package/runtime/scripts/checks/architecture/typescript.mjs +127 -29
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-check.mjs +160 -209
- package/runtime/scripts/lib/hfs-path-findings.mjs +95 -0
- package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
- package/runtime/scripts/lib/hfs-rules/frontend.mjs +37 -36
- package/runtime/scripts/lib/hfs-rules/peer-integrations.mjs +44 -0
- package/runtime/scripts/lib/hfs-rules/pipeline.mjs +12 -13
- package/runtime/scripts/lib/hfs-rules/proof-commands.mjs +11 -8
- package/runtime/scripts/lib/hfs-rules/spec-placement.mjs +1 -3
- package/runtime/scripts/lib/hfs-slots.mjs +244 -61
- package/runtime/scripts/lib/hfs-view.mjs +9 -7
- package/runtime/scripts/lib/language.mjs +11 -1
- package/runtime/scripts/lib/safe-remove.mjs +95 -10
- package/scaffold/app.mjs +179 -0
- package/scaffold/service.mjs +26 -16
- package/sync/cli.mjs +1 -1
- package/sync/hygiene.mjs +11 -8
- package/sync/index.mjs +109 -111
- package/sync/managed.mjs +9 -8
- package/sync/sonar-key.mjs +20 -22
- package/templates/{be → app}/ci-workflows/github/workflows/ci.yml +5 -11
- package/templates/app/gitignore +6 -0
- package/templates/app/hooks/husky/pre-commit +25 -0
- package/templates/app/hooks/husky/pre-push +7 -0
- package/templates/app/package-scripts/package.json +22 -0
- package/templates/{be → app}/quality-config/sonar-project.properties +4 -3
- package/templates/app/skeleton/.editorconfig +15 -0
- package/templates/app/skeleton/.gitattributes +2 -0
- package/templates/app/skeleton/.nvmrc +1 -0
- package/templates/app/skeleton/.starciwork/features/index.yaml +7 -0
- package/templates/app/skeleton/.starciwork/workspace.yaml +9 -0
- package/templates/app/skeleton/README.md +36 -0
- package/templates/app/skeleton/scripts/codegen.mjs +4 -0
- package/templates/{fe → app}/tool-config/prettierignore +4 -1
- package/templates/be/skeleton/.sops.yaml +2 -0
- package/templates/be/skeleton/.starcistacks/application-stacks.yaml +10 -0
- package/templates/be/skeleton/apps/__app__/src/__app__.options.ts +3 -0
- package/templates/be/skeleton/apps/__app__/src/app.module.ts +27 -6
- package/templates/be/skeleton/apps/__app__/src/main.ts +4 -1
- package/templates/be/skeleton/src/features/system-health/transport/http/live.controller.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/admission.policy.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/auth.guard.ts +25 -0
- package/templates/be/skeleton/src/modules/domain/identity/errors/identity.error.ts +16 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.contracts.ts +11 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.decorators.ts +8 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.module.ts +14 -0
- package/templates/be/skeleton/src/modules/domain/identity/identity.options.ts +2 -0
- package/templates/be/skeleton/src/modules/domain/identity/index.ts +6 -0
- package/templates/be/skeleton/src/modules/domain/identity/messages/identity.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/composition/composition.decorators.ts +8 -1
- package/templates/be/skeleton/src/modules/platform/composition/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/env-source.config.ts +71 -23
- package/templates/be/skeleton/src/modules/platform/config/errors/config.error.ts +14 -16
- package/templates/be/skeleton/src/modules/platform/config/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/config/server.config.ts +2 -12
- package/templates/be/skeleton/src/modules/platform/errors/domain.error.ts +16 -7
- package/templates/be/skeleton/src/modules/platform/errors/errors/errors.error.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.contracts.ts +33 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.decorators.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.filter.ts +32 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.log-events.ts +2 -2
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.spec.ts +94 -0
- package/templates/be/skeleton/src/modules/platform/errors/errors.service.ts +47 -0
- package/templates/be/skeleton/src/modules/platform/errors/http-status.policy.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/errors/index.ts +4 -1
- package/templates/be/skeleton/src/modules/platform/errors/messages/errors.messages.ts +11 -0
- package/templates/be/skeleton/src/modules/platform/http-security/errors/http-security.error.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http-security/execution-request.mapper.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.config.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.decorators.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.options.ts +17 -0
- package/templates/be/skeleton/src/modules/platform/http-security/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http-security/messages/http-security.messages.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/http-security/origin.guard.ts +31 -0
- package/templates/be/skeleton/src/modules/platform/http-security/rate-limit.guard.ts +68 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.spec.ts +66 -0
- package/templates/be/skeleton/src/modules/platform/i18n/bundle-message-catalog.service.ts +30 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.contracts.ts +18 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.decorators.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module.ts +24 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.options.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.port.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/i18n/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.spec.ts +45 -0
- package/templates/be/skeleton/src/modules/platform/i18n/request-locale.service.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +85 -68
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +14 -10
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +13 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/logging/logging.port.ts +0 -1
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.contracts.ts +25 -0
- package/templates/be/skeleton/src/modules/platform/primitives/outcome.mapper.ts +24 -0
- package/templates/fe/skeleton/apps/__app__/postcss.config.mjs +7 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/error.tsx +5 -17
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/layout.tsx +11 -22
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/loading.tsx +6 -0
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/not-found.tsx +3 -12
- package/templates/fe/skeleton/apps/__app__/src/app/[locale]/page.tsx +6 -24
- package/templates/fe/skeleton/apps/__app__/src/app/global-error.tsx +4 -18
- package/templates/fe/skeleton/apps/__app__/src/app/globals.css +5 -0
- package/templates/fe/skeleton/apps/__app__/src/components/composites/FailureScreen/index.tsx +28 -0
- package/templates/fe/skeleton/apps/__app__/src/features/layouts/LocaleShell/index.tsx +35 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/component.tsx +21 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/ErrorPage/index.tsx +17 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/component.tsx +26 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/GlobalErrorPage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/component.tsx +25 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/HomePage/index.tsx +15 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/component.tsx +27 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/LoadingPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/component.tsx +32 -0
- package/templates/fe/skeleton/apps/__app__/src/features/pages/NotFoundPage/index.tsx +8 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/config/index.ts +12 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/index.ts +2 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/i18n/messages/vi.json +6 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/request.ts +1 -0
- package/templates/fe/skeleton/apps/__app__/src/modules/routes/index.ts +4 -0
- package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/proxy.ts +1 -1
- package/runtime/scripts/checks/architecture/size-growth.mjs +0 -73
- package/sync/skeleton.mjs +0 -76
- package/templates/be/gitignore +0 -2
- package/templates/be/hooks/husky/pre-commit +0 -13
- package/templates/be/hooks/husky/pre-push +0 -7
- package/templates/be/package-scripts/package.json +0 -22
- package/templates/be/skeleton/scripts/.gitkeep +0 -0
- package/templates/be/skeleton/src/modules/platform/config/config.decorators.ts +0 -9
- package/templates/be/skeleton/src/modules/platform/errors/error.filter.ts +0 -20
- package/templates/be/tool-config/prettierignore +0 -8
- package/templates/fe/ci-workflows/github/workflows/ci.yml +0 -54
- package/templates/fe/gitignore +0 -3
- package/templates/fe/hooks/husky/pre-commit +0 -16
- package/templates/fe/hooks/husky/pre-push +0 -6
- package/templates/fe/package-scripts/package.json +0 -17
- package/templates/fe/parts/api-client.ts +0 -44
- package/templates/fe/parts/api-outcome.ts +0 -7
- package/templates/fe/quality-config/sonar-project.properties +0 -8
- package/templates/fe/skeleton/scripts/.gitkeep +0 -0
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/client.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/index.ts +0 -3
- package/templates/fe/skeleton-app/apps/__app__/src/modules/api/outcome.ts +0 -1
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/index.ts +0 -4
- package/templates/fe/skeleton-app/apps/__app__/src/modules/i18n/navigation.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/next.config.ts +0 -12
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/api/index.ts +0 -2
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/index.ts +0 -9
- package/templates/fe/skeleton-shared/apps/__app__/src/modules/i18n/request.ts +0 -5
- package/templates/fe/skeleton-shared/apps/__app__/src/proxy.ts +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-api/package.json +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-api/src/client.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/src/index.ts +0 -3
- package/templates/fe/skeleton-shared/packages/__family__-api/src/outcome.ts +0 -1
- package/templates/fe/skeleton-shared/packages/__family__-api/tsconfig.json +0 -5
- package/templates/fe/skeleton-shared/packages/__family__-i18n/package.json +0 -18
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/app.ts +0 -19
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/index.ts +0 -2
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/proxy.ts +0 -12
- package/templates/fe/skeleton-shared/packages/__family__-i18n/src/request.ts +0 -15
- package/templates/fe/skeleton-shared/packages/__family__-i18n/tsconfig.json +0 -5
- package/templates/fe/tool-config/prettierrc +0 -1
- /package/templates/{be → app}/ci-workflows/github/workflows/e2e.yml +0 -0
- /package/templates/{be → app}/starciwork.gitignore +0 -0
- /package/templates/{be → app}/tool-config/prettierrc +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/next.config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/config.ts +0 -0
- /package/templates/fe/{skeleton-app → skeleton}/apps/__app__/src/modules/i18n/routing.ts +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
// contract.mjs - HFS_CONTRACT_SNAPSHOT_DRIFT (R23): the
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `contracts/<be-app>/schema.graphql` (openapi.json for .json); hfs.json `stacks` names the sibling
|
|
6
|
-
// repository, and a sibling that is not checked out is reported (info), not compared
|
|
1
|
+
// contract.mjs - HFS_CONTRACT_SNAPSHOT_DRIFT (R23): the back end commits its contract.
|
|
2
|
+
// a be side with a GraphQL transport commits `contracts/<app>/schema.graphql` for each api app, and an api app with an operation table
|
|
3
|
+
// commits `contracts/<app>/openapi.json`. The fe side of the same app reads those snapshots where they are (`be/contracts/`, the one
|
|
4
|
+
// cross-side read of hfs.json sides.fe.reads) for its codegen: there is no copy to keep equal.
|
|
7
5
|
// The snapshots are generated by `npm run contract:emit` (`hfs emit-contracts`, packages/hfs/emit/contracts.mjs): printSchema of the
|
|
8
6
|
// resolvers the app root composes, and the OpenAPI 3.1 document of the typed operation table `apps/<app>/src/operations.ts`.
|
|
9
7
|
// That the committed snapshot equals what the app emits now is contractEmitFindings (full pass only, never --fast): it emits every
|
|
@@ -17,7 +15,6 @@ import { safeRemoveTree } from '../safe-remove.mjs';
|
|
|
17
15
|
|
|
18
16
|
export const CONTRACT_SNAPSHOT_DRIFT = 'HFS_CONTRACT_SNAPSHOT_DRIFT';
|
|
19
17
|
export const GRAPHQL_TRANSPORT_SLOT = 'be.transport.graphql';
|
|
20
|
-
const COPY = /^apps\/([^/]+)\/src\/modules\/api\/contract\/([^/]+)\.(graphql|json)$/;
|
|
21
18
|
const SNAPSHOT_OF = { graphql: 'schema.graphql', json: 'openapi.json' };
|
|
22
19
|
|
|
23
20
|
/** sha256 of a file's text with line endings folded, or null when it cannot be read. */
|
|
@@ -25,42 +22,18 @@ export function contractHash(file) {
|
|
|
25
22
|
try { return createHash('sha256').update(fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n')).digest('hex'); } catch { return null; }
|
|
26
23
|
}
|
|
27
24
|
|
|
28
|
-
/** The findings of R23 for the
|
|
29
|
-
export function contractFindings({
|
|
25
|
+
/** The findings of R23 for the be side at `repoRoot` (a side of another profile has none). */
|
|
26
|
+
export function contractFindings({ files, repo, resolver }) {
|
|
30
27
|
const findings = [];
|
|
31
|
-
if (repo.profile
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return findings;
|
|
42
|
-
}
|
|
43
|
-
for (const file of files) {
|
|
44
|
-
const match = COPY.exec(file);
|
|
45
|
-
if (!match) continue;
|
|
46
|
-
const [, app, beApp, ext] = match;
|
|
47
|
-
if (!stacks) {
|
|
48
|
-
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, file, `${file} is a contract copy but hfs.json names no sibling back end (stacks), so it cannot be compared with the back end's snapshot`, { app }));
|
|
49
|
-
continue;
|
|
50
|
-
}
|
|
51
|
-
const sibling = path.resolve(repoRoot, stacks);
|
|
52
|
-
if (!fs.existsSync(sibling)) {
|
|
53
|
-
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, file, `${file} was not compared: the sibling back end ${stacks} is not checked out here`, { app, level: 'info' }));
|
|
54
|
-
continue;
|
|
55
|
-
}
|
|
56
|
-
const snapshot = `contracts/${beApp}/${SNAPSHOT_OF[ext]}`;
|
|
57
|
-
const theirs = contractHash(path.join(sibling, snapshot));
|
|
58
|
-
if (theirs === null) {
|
|
59
|
-
findings.push(found(CONTRACT_SNAPSHOT_DRIFT, file, `${stacks}/${snapshot} does not exist, so ${file} copies nothing; the back end commits its contract first`, { app, snapshot }));
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
const ours = contractHash(path.join(repoRoot, file));
|
|
63
|
-
if (ours !== theirs) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, file, `${file} (${String(ours).slice(0, 12)}) differs from ${stacks}/${snapshot} (${theirs.slice(0, 12)}); run \`npm run contract:pull\``, { app, snapshot }));
|
|
28
|
+
if (repo.profile !== 'be') return findings;
|
|
29
|
+
const serves = files.some((file) => resolver.classifyPath(file).slot === GRAPHQL_TRANSPORT_SLOT);
|
|
30
|
+
const tracked = new Set(files);
|
|
31
|
+
for (const app of repo.apps.filter((a) => a.kind === 'api')) {
|
|
32
|
+
const operations = `contracts/${app.name}/${SNAPSHOT_OF.json}`;
|
|
33
|
+
if (tracked.has(`apps/${app.name}/src/operations.ts`) && !tracked.has(operations)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, operations, `${app.name} declares an operation table (apps/${app.name}/src/operations.ts) but ${operations} is not committed; declare be.contract.openapi in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
|
|
34
|
+
if (!serves) continue;
|
|
35
|
+
const snapshot = `contracts/${app.name}/${SNAPSHOT_OF.graphql}`;
|
|
36
|
+
if (!tracked.has(snapshot)) findings.push(found(CONTRACT_SNAPSHOT_DRIFT, snapshot, `${app.name} serves GraphQL but ${snapshot} is not committed; declare be.contract.graphql in hfs.json sides.be.optionalSlots, run \`npm run contract:emit\` and commit the snapshot`, { app: app.name }));
|
|
64
37
|
}
|
|
65
38
|
return findings;
|
|
66
39
|
}
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
// frontend.mjs - the front-end tree checks of hfs check
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// FE_I18N_PLACEMENT (R59) next-intl with the `[locale]` segment: `next-intl` is a dependency (of the app, the root or the shared
|
|
6
|
-
// i18n package under `packages/`), every route file sits under
|
|
7
|
-
// `src/app/[locale]/` (the root redirect page, global-error and health probes excepted), locale
|
|
8
|
-
// routing is `src/proxy.ts` and never `middleware.ts`, and the default locale's catalog `vi.json` exists
|
|
1
|
+
// frontend.mjs - the front-end tree checks of hfs check. Per app of the fe side (`apps/<app>`, kind next, side-relative paths):
|
|
2
|
+
// FE_I18N_PLACEMENT (R59) next-intl with the `[locale]` segment: every route file sits under `src/app/[locale]/` (the root
|
|
3
|
+
// redirect page, global-error and health probes excepted), locale routing is `src/proxy.ts` and never
|
|
4
|
+
// `middleware.ts`, and the default locale's catalog `vi.json` exists
|
|
9
5
|
// FE_I18N_CATALOG (R60) every `modules/i18n/messages/<locale>.json` has the same key set
|
|
6
|
+
// Over the whole app (the root scope, app-relative paths: the one package.json and the be contract snapshots sit outside the fe side):
|
|
7
|
+
// FE_WIRE_GENERATED (R52) wire types come from the be contract snapshots the fe side reads in place (its declared reads,
|
|
8
|
+
// `be/contracts/`): the root package.json declares the `codegen` script, and each fe app's generated
|
|
9
|
+
// types on disk (`modules/api/__generated__/`, never tracked) are not older than any snapshot
|
|
10
|
+
// FE_I18N_PLACEMENT (R59) `next-intl` is a dependency of the root package.json or of a shared i18n package under fe/packages/
|
|
10
11
|
// The files each app must hold (routing.ts, navigation.ts, request.ts, messages/, the [locale] layout) are the slot manifest's requires
|
|
11
12
|
// (HFS_SLOT_REQUIRED_MISSING); which source may hold display text and how copy resolves is eslint-fe's.
|
|
12
13
|
import fs from 'node:fs';
|
|
@@ -46,42 +47,35 @@ function newestBelow(dir) {
|
|
|
46
47
|
|
|
47
48
|
const scriptsOf = (repoRoot, file) => readJson(repoRoot, file)?.scripts ?? {};
|
|
48
49
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
50
|
+
/** R52 over the app: the codegen script of the root and the age of each fe app's generated types against the be snapshots. */
|
|
51
|
+
function wireFindings({ repoRoot, files, apps, reads }) {
|
|
52
|
+
const snapshots = files.filter((file) => reads.some((read) => file.startsWith(read)));
|
|
53
|
+
if (!snapshots.length) return [];
|
|
53
54
|
const findings = [];
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const wired = (name) => scripts[name] ?? (delegates(rootScripts[name]) ? undefined : rootScripts[name]);
|
|
61
|
-
if (typeof wired('codegen') !== 'string') findings.push(found(WIRE_GENERATED, manifest, `${app} keeps a contract copy but declares no \`codegen\` script; wire types are generated from the copy, never typed by hand`, { app }));
|
|
62
|
-
else for (const hook of ['prebuild', 'pretypecheck']) {
|
|
63
|
-
if (!/\bcodegen\b/.test(String(wired(hook) ?? ''))) findings.push(found(WIRE_GENERATED, manifest, `${app} does not run codegen in \`${hook}\`; generate the wire types before build and typecheck`, { app, script: hook }));
|
|
64
|
-
}
|
|
65
|
-
const generated = `${base}/src/modules/api/__generated__`;
|
|
66
|
-
const built = newestBelow(path.join(repoRoot, generated));
|
|
67
|
-
if (built !== null) {
|
|
68
|
-
for (const copy of copies) {
|
|
55
|
+
if (typeof scriptsOf(repoRoot, 'package.json').codegen !== 'string') findings.push(found(WIRE_GENERATED, 'package.json', `the app keeps contract snapshots (${reads.join(', ')}) but its package.json declares no \`codegen\` script; wire types are generated from the snapshots, never typed by hand`));
|
|
56
|
+
for (const app of apps) {
|
|
57
|
+
const generated = `fe/apps/${app}/src/modules/api/__generated__`;
|
|
58
|
+
const built = newestBelow(path.join(repoRoot, generated));
|
|
59
|
+
if (built === null) continue;
|
|
60
|
+
for (const snapshot of snapshots) {
|
|
69
61
|
let changed;
|
|
70
|
-
try { changed = fs.statSync(path.join(repoRoot,
|
|
71
|
-
if (built + STALE_MS < changed) findings.push(found(WIRE_GENERATED, generated, `${generated} is older than ${
|
|
62
|
+
try { changed = fs.statSync(path.join(repoRoot, snapshot)).mtimeMs; } catch { continue; }
|
|
63
|
+
if (built + STALE_MS < changed) findings.push(found(WIRE_GENERATED, generated, `${generated} is older than ${snapshot}; run \`npm run codegen\` so the wire types match the contract snapshot`, { app, snapshot }));
|
|
72
64
|
}
|
|
73
65
|
}
|
|
74
66
|
return findings;
|
|
75
67
|
}
|
|
76
68
|
|
|
69
|
+
/** R59 over the app: next-intl is a dependency of the one package.json or of a shared fe/packages/* manifest. */
|
|
70
|
+
function intlDependencyFindings({ repoRoot, files }) {
|
|
71
|
+
const declares = (file) => { const pkg = readJson(repoRoot, file); return pkg ? ['dependencies', 'devDependencies'].some((section) => pkg[section]?.['next-intl'] !== undefined) : false; };
|
|
72
|
+
const shared = files.filter((file) => /^fe\/packages\/[^/]+\/package\.json$/.test(file));
|
|
73
|
+
return ['package.json', ...shared].some(declares) ? [] : [found(I18N_PLACEMENT, 'package.json', 'the app does not depend on next-intl (nor does a shared fe package); every fe app uses next-intl with the [locale] segment')];
|
|
74
|
+
}
|
|
75
|
+
|
|
77
76
|
function placementFindings({ repoRoot, app, tracked }) {
|
|
78
77
|
const base = `apps/${app}`;
|
|
79
78
|
const findings = [];
|
|
80
|
-
const declares = (file) => { const pkg = readJson(repoRoot, file); return pkg ? ['dependencies', 'devDependencies'].some((section) => pkg[section]?.['next-intl'] !== undefined) : false; };
|
|
81
|
-
const manifest = `${base}/package.json`;
|
|
82
|
-
// The next-intl stack is written once per repository: in the app, or in the shared `packages/<family>-i18n` the apps call.
|
|
83
|
-
const shared = tracked.filter((file) => /^packages\/[^/]+\/package\.json$/.test(file));
|
|
84
|
-
if (tracked.includes(manifest) && ![manifest, 'package.json', ...shared].some(declares)) findings.push(found(I18N_PLACEMENT, manifest, `${app} does not depend on next-intl (nor does the root or a shared package); every app uses next-intl with the [locale] segment`, { app }));
|
|
85
79
|
if (!tracked.includes(`${base}/src/proxy.ts`)) findings.push(found(I18N_PLACEMENT, `${base}/src/proxy.ts`, `${app} has no src/proxy.ts; locale routing lives in proxy.ts`, { app }));
|
|
86
80
|
for (const file of tracked.filter((f) => new RegExp(`^${base}/src/middleware\.[cm]?[jt]s$`).test(f))) findings.push(found(I18N_PLACEMENT, file, `${file} is a middleware file; Next 16 routes through src/proxy.ts`, { app }));
|
|
87
81
|
const appDir = `${base}/src/app/`;
|
|
@@ -118,7 +112,14 @@ function catalogFindings({ repoRoot, app, tracked }) {
|
|
|
118
112
|
return findings;
|
|
119
113
|
}
|
|
120
114
|
|
|
121
|
-
/** The
|
|
115
|
+
/** The fe side's tree findings of R59 and R60 for every `next` app; `repoRoot` is the fe side folder, `files` side-relative. */
|
|
122
116
|
export function frontendFindings({ repoRoot, files, repo }) {
|
|
123
|
-
return repo.apps.filter((a) => a.kind === 'next').flatMap((a) => [
|
|
117
|
+
return repo.apps.filter((a) => a.kind === 'next').flatMap((a) => [placementFindings, catalogFindings].flatMap((check) => check({ repoRoot, app: a.name, tracked: files })));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** The app's front-end findings that read the root (R52, R59's dependency); `repoRoot` is the app root, `files` app-relative. */
|
|
121
|
+
export function appFrontendFindings({ repoRoot, files, repo }) {
|
|
122
|
+
const apps = repo.sides.fe.apps.filter((a) => a.kind === 'next').map((a) => a.name);
|
|
123
|
+
// The snapshots are the paths the fe side declares it reads of the be side (hfs.json sides.fe.reads: be/contracts/).
|
|
124
|
+
return [...wireFindings({ repoRoot, files, apps, reads: repo.sides.fe.reads }), ...intlDependencyFindings({ repoRoot, files })];
|
|
124
125
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// peer-integrations.mjs - HFS_PEER_INTEGRATION_MISSING (R111): an app declares the runtime peer a driver integration needs.
|
|
2
|
+
// The catalog knowledge/hfs/peer-integrations.yaml says "if the app depends on every package of `when` (at the named major,
|
|
3
|
+
// when one is named), it must declare `requires`". npm does not install such a peer for the integration, the type checker and
|
|
4
|
+
// the unit specs never load it, and the api fails only when it boots (@nestjs/apollo on @nestjs/platform-express 11, Express 5,
|
|
5
|
+
// needs @as-integrations/express5). The app root package.json is the one manifest of both sides: a `when` package counts in its
|
|
6
|
+
// dependencies or devDependencies, the peer counts only in its dependencies (a production install omits devDependencies).
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
9
|
+
import { found, readJson } from './read.mjs';
|
|
10
|
+
|
|
11
|
+
export const PEER_INTEGRATION_MISSING = 'HFS_PEER_INTEGRATION_MISSING';
|
|
12
|
+
const MANIFEST = 'package.json';
|
|
13
|
+
const CATALOG_FILE = new URL('../../../knowledge/hfs/peer-integrations.yaml', import.meta.url);
|
|
14
|
+
|
|
15
|
+
/** The pairs of the catalog: [{ id, when: [{ package, major? }], requires, why }]. */
|
|
16
|
+
export function peerIntegrationPairs() {
|
|
17
|
+
const catalog = parseYaml(fs.readFileSync(CATALOG_FILE, 'utf8'));
|
|
18
|
+
if (catalog?.schema !== 'starci/hfs-peer-integrations@1' || !Array.isArray(catalog.pairs)) throw new Error('knowledge/hfs/peer-integrations.yaml is not a starci/hfs-peer-integrations@1 catalog');
|
|
19
|
+
return catalog.pairs;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The major a dependency spec allows (`11.2.5`, `^11.0.0`, `~11.1`, `>=11 <12`), or null when the spec names none (a tag, a link, `*`). */
|
|
23
|
+
export function majorOf(spec) {
|
|
24
|
+
const match = /^\s*(?:[\^~]|>=?|=)?\s*v?(\d+)(?:\.|\s|$)/.exec(String(spec));
|
|
25
|
+
return match ? Number(match[1]) : null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The findings of R111 over the app root package.json of `repoRoot`; `pairs` defaults to the catalog. */
|
|
29
|
+
export function peerIntegrationFindings({ repoRoot, files, pairs = peerIntegrationPairs() }) {
|
|
30
|
+
if (!files.includes(MANIFEST)) return [];
|
|
31
|
+
const pkg = readJson(repoRoot, MANIFEST);
|
|
32
|
+
if (!pkg) return [];
|
|
33
|
+
const runtime = pkg.dependencies ?? {};
|
|
34
|
+
const declared = { ...(pkg.devDependencies ?? {}), ...runtime };
|
|
35
|
+
const findings = [];
|
|
36
|
+
for (const pair of pairs) {
|
|
37
|
+
const applies = pair.when.every((entry) => typeof declared[entry.package] === 'string' && (entry.major === undefined || majorOf(declared[entry.package]) === entry.major));
|
|
38
|
+
if (!applies || typeof runtime[pair.requires] === 'string') continue;
|
|
39
|
+
const because = pair.when.map((entry) => `${entry.package} ${declared[entry.package]}`).join(' with ');
|
|
40
|
+
const where = typeof declared[pair.requires] === 'string' ? ` (it is only a devDependency, which a production install omits)` : '';
|
|
41
|
+
findings.push(found(PEER_INTEGRATION_MISSING, MANIFEST, `${MANIFEST} depends on ${because} but does not declare ${pair.requires} in its dependencies${where}: ${pair.why.trim()} Add ${pair.requires} to the dependencies.`, { pair: pair.id, requires: pair.requires }));
|
|
42
|
+
}
|
|
43
|
+
return findings;
|
|
44
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
// pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `hfs
|
|
2
|
-
// .github/workflows/ci.yml a `run:` step that runs `hfs
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
1
|
+
// pipeline.mjs - HFS_CI_MISSING_CANON (R13): CI runs the pinned `hfs lint`; pre-push runs typecheck and lint.
|
|
2
|
+
// .github/workflows/ci.yml a `run:` step that runs `hfs lint` (the one lint entry: eslint, the repository check, stylelint): `npx hfs lint`,
|
|
3
|
+
// `npx @starci/hfs[@<version>] lint`, or `npm run <script>` of the root package.json whose command runs `hfs lint`
|
|
4
|
+
// (`npm run lint`, arguments after `--` allowed); a version named in the step is the @starci/hfs pin of
|
|
5
|
+
// canon-pins.yaml (an installed one is pinned by the pin check, R15)
|
|
6
|
+
// .husky/pre-push `npm run typecheck` and `npm run lint`, each on a line of its own
|
|
6
7
|
// A missing file is the slot manifest's finding (HFS_SLOT_REQUIRED_MISSING); a file that is present and lacks the step is this rule's.
|
|
7
8
|
// The redirect of `core.hooksPath` away from husky is the architecture machine's (HFS_HOOKS_PATH_REDIRECTED).
|
|
8
9
|
import { found, readJson, readText } from './read.mjs';
|
|
@@ -12,9 +13,8 @@ export const CI_FILE = '.github/workflows/ci.yml';
|
|
|
12
13
|
export const PRE_PUSH_FILE = '.husky/pre-push';
|
|
13
14
|
export const hfsPackage = '@starci/hfs';
|
|
14
15
|
const RUN_LINE = /^\s*(?:-\s+)?run:\s*(.+?)\s*$/;
|
|
15
|
-
const
|
|
16
|
+
const HFS_LINT = /^(?:npx\s+(?:--no-install\s+|-y\s+)?)?(?:@starci\/hfs|hfs)(?:@(\S+))?\s+lint(\s.*)?$/;
|
|
16
17
|
const NPM_RUN = /^npm run ([\w:.-]+)(?:\s+--\s+(.*))?$/;
|
|
17
|
-
const FAST = /(^|\s)--fast(\s|$)/;
|
|
18
18
|
|
|
19
19
|
const commandsOf = (text) => text.split(/\r?\n/).map((line) => RUN_LINE.exec(line)?.[1]).filter(Boolean).map((command) => command.replace(/^["']|["']$/g, ''));
|
|
20
20
|
|
|
@@ -33,17 +33,16 @@ export function pipelineFindings({ repoRoot, files, pins }) {
|
|
|
33
33
|
const ci = files.includes(CI_FILE) ? readText(repoRoot, CI_FILE) : null;
|
|
34
34
|
if (ci !== null) {
|
|
35
35
|
const scripts = readJson(repoRoot, 'package.json')?.scripts ?? {};
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs hfs check at ${whole[0][1]}, but ${hfsPackage} is pinned at ${pinned}; run the pinned version`, { step: 'hfs check', pinned }));
|
|
36
|
+
const lints = commandsOf(ci).flatMap((command) => expanded(command, scripts).split(/\s*&&\s*/)).map((command) => HFS_LINT.exec(command)).filter(Boolean);
|
|
37
|
+
if (!lints.length) findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} has no step that runs \`hfs lint\` (\`npm run lint\`, or \`npx hfs lint\`); CI runs the one lint entry, which includes the repository check`, { step: 'hfs lint' }));
|
|
38
|
+
else if (pinned && !lints.some((match) => match[1] === undefined || match[1] === pinned)) {
|
|
39
|
+
findings.push(found(CI_MISSING_CANON, CI_FILE, `${CI_FILE} runs hfs lint at ${lints[0][1]}, but ${hfsPackage} is pinned at ${pinned}; run the pinned version`, { step: 'hfs lint', pinned }));
|
|
41
40
|
}
|
|
42
41
|
}
|
|
43
42
|
const prePush = files.includes(PRE_PUSH_FILE) ? readText(repoRoot, PRE_PUSH_FILE) : null;
|
|
44
43
|
if (prePush !== null) {
|
|
45
44
|
const lines = prePush.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !line.startsWith('#'));
|
|
46
|
-
for (const step of ['typecheck', 'lint
|
|
45
|
+
for (const step of ['typecheck', 'lint']) {
|
|
47
46
|
if (!runsStep(lines, step)) findings.push(found(CI_MISSING_CANON, PRE_PUSH_FILE, `${PRE_PUSH_FILE} does not run \`npm run ${step}\`; pre-push runs typecheck and lint`, { step }));
|
|
48
47
|
}
|
|
49
48
|
}
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
// keeps one goes quietly false (nivo-backend, 2026-09-30: `node --test scripts/provision-keycloak.spec.mjs` after the script was deleted).
|
|
5
5
|
// Every argument of the command that is a repository-relative file path (an extension, no glob, no variable, not absolute, not a `..`
|
|
6
6
|
// path into another repository) must be a tracked file or an existing one; a path an ignored slot owns (dist/, coverage/) is a build
|
|
7
|
-
// product and is not judged. One finding per record, kind and missing path.
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// product and is not judged. One finding per record, kind and missing path. The `.starciwork` of an app sits at the app root, and a
|
|
8
|
+
// record names the side its implementation lives in (`repository: be` or `repository: fe`, the repositories of workspace.yaml, the
|
|
9
|
+
// sides of hfs.json); a proof command runs from the app root, so every record of a side, or naming none, is judged against the app root.
|
|
10
|
+
// A record naming a repository that is not a side of this app runs elsewhere and is not judged.
|
|
10
11
|
import fs from 'node:fs';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
13
|
-
import { repositoryName } from '../repo-identity.mjs';
|
|
14
14
|
import { found, readText } from './read.mjs';
|
|
15
15
|
|
|
16
16
|
export const PROOF_COMMAND_FILE_MISSING = 'HFS_PROOF_COMMAND_FILE_MISSING';
|
|
@@ -39,18 +39,21 @@ function proofCommands(record) {
|
|
|
39
39
|
return Object.entries(demands).flatMap(([kind, demand]) => (typeof demand?.command === 'string' ? [{ kind, command: demand.command }] : []));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
/**
|
|
43
|
-
|
|
42
|
+
/**
|
|
43
|
+
* The findings of R105 over the tracked paths `files` of the app at `repoRoot`; `resolver` names the ignored slots and `sides` the side
|
|
44
|
+
* names of hfs.json (be, fe), the repositories a record of this app names.
|
|
45
|
+
*/
|
|
46
|
+
export function proofCommandFindings({ repoRoot, files, resolver, sides }) {
|
|
44
47
|
const findings = [];
|
|
45
48
|
const tracked = new Set(files);
|
|
46
|
-
const own =
|
|
49
|
+
const own = new Set(sides);
|
|
47
50
|
for (const file of files) {
|
|
48
51
|
if (!RECORD.test(file)) continue;
|
|
49
52
|
const text = readText(repoRoot, file);
|
|
50
53
|
if (text === null || !text.includes('requiresProof')) continue;
|
|
51
54
|
let record;
|
|
52
55
|
try { record = parseYaml(text); } catch { continue; }
|
|
53
|
-
if (typeof record?.repository === 'string' && record.repository
|
|
56
|
+
if (typeof record?.repository === 'string' && !own.has(record.repository)) continue;
|
|
54
57
|
for (const { kind, command } of proofCommands(record)) {
|
|
55
58
|
for (const target of commandFiles(command)) {
|
|
56
59
|
if (tracked.has(target) || fs.existsSync(path.join(repoRoot, target))) continue;
|
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
// - a unit spec is `<name>.service.spec.ts` beside its service, in a slot whose `tests` is `unit-beside` (the slot and the lint rule
|
|
3
3
|
// `unit-test-colocated` judge the name);
|
|
4
4
|
// - integration, e2e and contract specs are `src/tests/{integration,e2e,contract}/...`, the slots whose `tests` is `e2e`;
|
|
5
|
-
// - an app's own folder (`apps/<app>/src/`, the slots that name an `appKind`) holds the app's composition spec, which the architecture
|
|
6
|
-
// machine's app-composition check judges.
|
|
7
5
|
// Every other tracked `*.spec.*`, `*.test.*` or `*-spec.*` file is a finding, `scripts/` and `tools/` included: an operational script
|
|
8
6
|
// carries no spec, and a spec that guards one moves into a layer or goes. A file no slot owns (`tools/x.spec.ts`) is judged too, so the
|
|
9
7
|
// finding names the rule rather than only the missing slot. Which folder is a layer is read from the slot manifest, never spelled here.
|
|
@@ -23,7 +21,7 @@ export function specPlacementFindings({ files, resolver }) {
|
|
|
23
21
|
if (!isSpecFile(file) || file.includes('node_modules/')) continue;
|
|
24
22
|
const classified = resolver.classifyPath(file);
|
|
25
23
|
const slot = classified.status === 'owned' ? resolver.slot(classified.slot) : null;
|
|
26
|
-
if (slot &&
|
|
24
|
+
if (slot && TEST_SLOT_TESTS.has(slot.tests)) continue;
|
|
27
25
|
const where = slot ? `slot ${slot.id}` : 'no slot';
|
|
28
26
|
findings.push(found(SPEC_PLACEMENT, file, `${file} is a spec outside the test layers (${where}); a unit spec is <name>.service.spec.ts beside its service and an integration, e2e or contract spec sits under src/tests/{integration,e2e,contract}; an operational script or a tool carries no spec`));
|
|
29
27
|
}
|