@starci/hfs 3.0.0 → 4.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 +22 -0
- package/README.md +25 -21
- package/bin/hfs.mjs +59 -37
- package/lint/run.mjs +70 -39
- package/package.json +2 -2
- 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 +28 -9
- 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/modules/kernel/failure-codes.yaml +23 -32
- 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.mjs +104 -66
- package/runtime/scripts/checks/architecture/next-data.mjs +3 -2
- package/runtime/scripts/checks/architecture/registration.mjs +1 -1
- 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 +45 -20
- package/runtime/scripts/checks/typescript-programs.mjs +2 -2
- package/runtime/scripts/lib/hfs-check.mjs +156 -141
- package/runtime/scripts/lib/hfs-path-findings.mjs +13 -2
- package/runtime/scripts/lib/hfs-rules/contract.mjs +15 -42
- package/runtime/scripts/lib/hfs-rules/deps.mjs +4 -2
- 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/proof-commands.mjs +11 -8
- 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 +205 -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 +4 -2
- 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 +3 -2
- 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/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 -6
- package/templates/be/package-scripts/package.json +0 -19
- 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 -40
- package/templates/fe/gitignore +0 -3
- package/templates/fe/hooks/husky/pre-commit +0 -16
- package/templates/fe/hooks/husky/pre-push +0 -5
- package/templates/fe/package-scripts/package.json +0 -13
- 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
|
}
|
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
// - a dependency the root `overrides` pins to a version (a string that is not a `$name` reference) is declared at that version everywhere
|
|
5
5
|
// it is declared: an override the manifests disagree with is a second version in disguise;
|
|
6
6
|
// - the lockfile (package-lock.json, read, never installed) holds no nested copy of a dependency the workspace declares:
|
|
7
|
-
// `node_modules/<a>/node_modules/<name>` or `apps/<app>/node_modules/<name>` next to the hoisted `node_modules/<name>`.
|
|
7
|
+
// `node_modules/<a>/node_modules/<name>` or `apps/<app>/node_modules/<name>` next to the hoisted `node_modules/<name>`. A
|
|
8
|
+
// bundled copy (`inBundle`: it ships inside its parent's tarball, its bundleDependencies) is not one: no range, override or
|
|
9
|
+
// dedupe of the workspace can move it, so it is the parent's, not a second copy the workspace keeps.
|
|
8
10
|
import { found, readJson } from './read.mjs';
|
|
9
11
|
|
|
10
12
|
export const DEP_VERSION_SKEW = 'HFS_DEP_VERSION_SKEW';
|
|
@@ -54,7 +56,7 @@ export function depFindings({ repoRoot, files }) {
|
|
|
54
56
|
}
|
|
55
57
|
for (const [key, entry] of Object.entries(lock.packages)) {
|
|
56
58
|
const nested = NESTED.exec(key);
|
|
57
|
-
if (!nested || entry.link || HOISTED.test(key) || !declared.has(nested[1])) continue;
|
|
59
|
+
if (!nested || entry.link || entry.inBundle || HOISTED.test(key) || !declared.has(nested[1])) continue;
|
|
58
60
|
const name = nested[1];
|
|
59
61
|
findings.push(found(DEP_VERSION_SKEW, 'package-lock.json', `${key} is a nested copy of ${name}${entry.version ? ` ${entry.version}` : ''}${hoisted.has(name) ? ` next to the hoisted ${hoisted.get(name)}` : ''}; the workspace keeps one copy (align the ranges or add a root override)`, { dependency: name, lockPath: key, version: entry.version }));
|
|
60
62
|
}
|
|
@@ -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
|
+
}
|
|
@@ -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;
|