@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
|
@@ -32,13 +32,14 @@ newCode:
|
|
|
32
32
|
metric: new_security_hotspots_reviewed
|
|
33
33
|
minReviewedPercent: 100
|
|
34
34
|
unreviewedMax: 0
|
|
35
|
-
# The whole code, not only the new code. Every finding of the HFS canon reaches Sonar through ONE
|
|
36
|
-
# (packages/hfs, contract
|
|
37
|
-
#
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
35
|
+
# The whole code, not only the new code. Every finding of the HFS canon reaches Sonar through ONE entry, `hfs lint`
|
|
36
|
+
# (packages/hfs, contract changes hfs-sonar-import and hfs-lint-entry), which writes ONE file, reports/lint.sonar.json
|
|
37
|
+
# (`npm run lint -- --sonar reports/lint.sonar.json`), named by sonar.externalIssuesReportPaths; the gate holds the open-issue count of
|
|
38
|
+
# the whole project at zero. The file carries three engines:
|
|
39
|
+
# starci-hfs the repository findings of `hfs check` (managed files, tree, pins, CI, contracts; the rule id is the finding code)
|
|
40
|
+
# eslint ESLint's json over the repository (the BE and FE canon plugins alike, per-file and project-graph rules)
|
|
41
|
+
# stylelint stylelint's json over the style glob (front ends)
|
|
42
|
+
# The engines share ONE placement rule (a finding on a file Sonar does not index is filed on the first source file); Sonar's own
|
|
42
43
|
# ESLint import (sonar.eslint.reportPaths) is not used because it drops such issues.
|
|
43
44
|
# A SonarQube gate condition cannot filter issues by engine, so the condition is the project's open issues (`violations`,
|
|
44
45
|
# overall code): the three engines above are the imports it counts, and every native Sonar issue counts with them. That is
|
|
@@ -245,10 +245,9 @@ BE_APP_BUSINESS_ROLE:
|
|
|
245
245
|
BE_APP_COMPOSITION_ONLY:
|
|
246
246
|
title: "Composition root has stray source"
|
|
247
247
|
title_vi: "Gốc ghép nối có tệp lạc chỗ"
|
|
248
|
-
meaning_vi: "Mã trong ứng dụng gốc không phải tệp mà slot ứng dụng cho phép (main.ts, app.module.ts, <app>.options.ts)."
|
|
248
|
+
meaning_vi: "Mã trong ứng dụng gốc không phải tệp mà slot ứng dụng cho phép (main.ts, app.module.ts, <app>.options.ts; operations.ts ở ứng dụng API)."
|
|
249
249
|
causes_vi:
|
|
250
250
|
- "Thêm tệp không đúng tên vào thư mục ứng dụng gốc"
|
|
251
|
-
- "Composition spec của app tự khai AppModule, import AppModule từ tệp khác app.module, hoặc không gọi AppModule.register( — module thật không bao giờ được khởi chạy"
|
|
252
251
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra; không cần ai can thiệp thêm."
|
|
253
252
|
owner: op-retry
|
|
254
253
|
kind: check-finding
|
|
@@ -445,6 +444,7 @@ BE_RAW_INJECT:
|
|
|
445
444
|
- "Token là chuỗi thay vì unique symbol"
|
|
446
445
|
- "Tiêm lớp hạ tầng (CacheService, AppLogger) không qua injector"
|
|
447
446
|
- "Dùng ModuleRef.get hoặc forwardRef để né vòng phụ thuộc"
|
|
447
|
+
- "constructor(private readonly timeoutMs: number) {} hoặc ReadonlyMap<string, X> không có Inject<Thing>() gắn token có tên"
|
|
448
448
|
nextStep_vi: "Khai báo token `export const THING: unique symbol = Symbol(\"<owner>.<thing>\")` và `export const InjectThing = (): TypedParameterDecorator<Thing> => injector<Thing>(THING)` trong <owner>.decorators.ts rồi dùng @InjectThing() ở mọi nơi; vòng phụ thuộc là lỗi kiến trúc cần gỡ, không dùng forwardRef. Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra và mục của luật trong BE-CONVENTION; không thêm ngoại lệ, không tắt luật."
|
|
449
449
|
owner: op-retry
|
|
450
450
|
kind: check-finding
|
|
@@ -484,8 +484,9 @@ BE_SCHEMA_OWNER:
|
|
|
484
484
|
BE_SOURCE_FORM:
|
|
485
485
|
title: "Back-end source file breaks the naming or form canon"
|
|
486
486
|
title_vi: "Tệp nguồn BE sai quy ước tên hoặc hình thức"
|
|
487
|
-
meaning_vi: "Tên tệp phải là kebab-case với hậu tố vai trò trong danh sách đóng của slots.yaml; chỉ export có tên (không export default); mọi export và thành viên công khai có JSDoc tiếng Anh; không emoji, và không có chữ tiếng Việt trong định danh, chuỗi ký tự, chú thích hay tên test ngoài catalog messages/ và slot fixture i18n."
|
|
487
|
+
meaning_vi: "Tên tệp phải là kebab-case với hậu tố vai trò trong danh sách đóng của slots.yaml; chỉ export có tên (không export default); mọi export và thành viên công khai có JSDoc tiếng Anh; không emoji, và không có chữ tiếng Việt trong định danh, chuỗi ký tự, chú thích hay tên test ngoài catalog messages/ và slot fixture i18n; mã production không khai báo lớp, hàm hay hằng export có từ Mock, Fake hoặc Stub trong tên (đồ giả nằm trong spec hoặc src/tests; driver ngoại tuyến là adapter điều khiển bằng options, đặt tên theo việc nó làm)."
|
|
488
488
|
causes_vi:
|
|
489
|
+
- "class MockExpertAgentService trong mã production thay vì adapter điều khiển bằng options"
|
|
489
490
|
- "Tệp dùng hậu tố bị cấm (use-case, repository, store, util, helper, types, constants)"
|
|
490
491
|
- "Lớp export không kết thúc bằng hậu tố vai trò của tệp và cũng không kết thúc bằng tên port (interface trong *.port.ts hoặc *.contracts.ts) mà nó implements"
|
|
491
492
|
- "Interface trong tệp *.input.ts, *.request.ts, *.response.ts, *.type.ts, *.args.ts hoặc *.rows.ts không kết thúc bằng Input, Request, Response, Type, Args hoặc Row"
|
|
@@ -516,6 +517,7 @@ BE_SPEC_QUALITY:
|
|
|
516
517
|
- "Spec chỉ có toHaveBeenCalled*, không khẳng định kết quả hay trạng thái"
|
|
517
518
|
- "`it.skip`, `describe.skip`, `xit`, `it.todo`, `.skipIf`/`.runIf` hoặc `(cond ? describe : describe.skip)` trong spec; hãy viết test thật hoặc xóa nó, còn việc bỏ qua khi thiếu sandbox nằm trong contract helper của world (`sandbox.describe(...)`)"
|
|
518
519
|
- "e2e gọi CommandBus, QueryBus hay handler trực tiếp; dùng sleep thay cho waitFor; tự gọi Test.createTestingModule; import SDK của nhà cung cấp mô hình; không đọc lại dòng đã lưu qua EntityManager"
|
|
520
|
+
- "Spec đơn vị của service khẳng định `expect.any(String)`, `expect.any(Number)` hoặc `expect.any(Date)` cho giá trị service tự sinh; service nhận bộ sinh id và đồng hồ qua token, spec cấp `fakeIds()` và `new FakeClock(...)` từ @starci/jest-preset rồi khẳng định đúng giá trị"
|
|
519
521
|
- "Spec đơn vị của service có providers khác với phụ thuộc constructor: thừa nhà cung cấp, thiếu nhà cung cấp, service không đứng đầu, token không suy ra được hoặc nhà cung cấp không phải lớp hay { provide, useValue }"
|
|
520
522
|
nextStep_vi: "Chuyển luật kiến trúc sang lint hoặc bộ kiểm tra; khẳng định kết quả hoặc trạng thái; e2e vào bằng transport thật, chờ bằng waitFor và dựng thế giới ở src/tests/e2e/setup. Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra."
|
|
521
523
|
owner: op-retry
|
|
@@ -539,7 +541,7 @@ BE_TEST_TOPOLOGY:
|
|
|
539
541
|
meaning_vi: "Cấu hình test lệch chuẩn: `<detail>`."
|
|
540
542
|
causes_vi:
|
|
541
543
|
- "Vi phạm luật R47: một `jest.config.js` (preset `@starci/jest-preset`) với bốn project `unit`, `integration`, `e2e`, `contract`; thư mục và hậu tố spec luôn khớp nhau; `diagnostics: false` + `isolatedModules: true`; `test:integration`, `test:e2e`, `test:contract` = `typecheck:tests && jest --selectProjects <tên>`; `src/tests/world/` là nơi duy nhất dựng hạ tầng test và chỉ chứa `global-setup.ts`, `global-teardown.ts`, `use-test-world.ts`, `fakes/` và file gốc có hậu tố vai trò (kể cả `test-world.config.ts`)."
|
|
542
|
-
- "Vi phạm luật R47 về stack thật: mọi dịch vụ khai trong `.starcistacks/<env>` (Postgres, Redis, Keycloak, mail host, k3s...) chạy THẬT trong world qua toxiproxy, world đọc dịch vụ và phiên bản image từ stack lúc chạy; `fakes/<provider>/` chỉ giả SaaS bên ngoài mà đội không vận hành. Ngoại lệ duy nhất: dịch vụ stateless cần phần cứng đặc biệt hoặc model bên ngoài (GPU inference, embedding tự host) được giả khi `test-world.config.ts` khai
|
|
544
|
+
- "Vi phạm luật R47 về stack thật: mọi dịch vụ khai trong `.starcistacks/<env>` (Postgres, Redis, Keycloak, mail host, k3s...) chạy THẬT trong world qua toxiproxy, world đọc dịch vụ và phiên bản image từ stack lúc chạy; `fakes/<provider>/` chỉ giả SaaS bên ngoài mà đội không vận hành. Ngoại lệ duy nhất: dịch vụ stateless cần phần cứng đặc biệt hoặc model bên ngoài (GPU inference, embedding tự host) được giả khi mục `stacks` của dịch vụ đó trong `test-world.config.ts` khai `{ fakedBy, reason }` với `reason` không rỗng; dịch vụ có trạng thái (database, cache, identity, storage, mail, queue, search hoặc có volume bền) không bao giờ được giả, và mục `fakedBy` trỏ tới dịch vụ hoặc fake (mục `fakes` hay thư mục `fakes/`) không tồn tại bị coi là cũ. `test-world.config.ts` khai world theo đúng một dạng export có tên `export const { useTestWorld, useSandbox } = defineTestWorld({...})` (`stack`, `stacks` theo từng dịch vụ, `fakes`); export default không được đọc và bị báo lỗi."
|
|
543
545
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra (nợ hàng loạt có codemod của HFS); không cần ai can thiệp thêm."
|
|
544
546
|
owner: op-retry
|
|
545
547
|
kind: check-finding
|
|
@@ -905,11 +907,11 @@ FE_TRANSPORT_OWNER:
|
|
|
905
907
|
kind: check-finding
|
|
906
908
|
|
|
907
909
|
FE_WIRE_GENERATED:
|
|
908
|
-
title: "Wire types generated from the contract
|
|
910
|
+
title: "Wire types generated from the be contract snapshots the fe side reads"
|
|
909
911
|
title_vi: "Kiểu wire gõ tay"
|
|
910
|
-
meaning_vi: "`<file>` tự gõ kiểu wire/ép kiểu phản hồi. Dùng kiểu sinh từ `
|
|
912
|
+
meaning_vi: "`<file>` tự gõ kiểu wire/ép kiểu phản hồi, hoặc kiểu sinh ra cũ hơn ảnh chụp hợp đồng. Dùng kiểu sinh từ `be/contracts/`."
|
|
911
913
|
causes_vi:
|
|
912
|
-
- "Vi phạm luật R52: Kiểu wire sinh từ
|
|
914
|
+
- "Vi phạm luật R52: Kiểu wire sinh từ ảnh chụp hợp đồng của be (`be/contracts/`, chạy `npm run codegen` ở gốc app); tài liệu GraphQL trong `.graphql`; cấm `as GraphqlResult<T>`; enum wire không phải `string`."
|
|
913
915
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra và mục của luật trong tiêu chuẩn HFS; nợ cũ cần quyết định thiết kế nên op không thêm ngoại lệ, chỉ tách hoặc chuyển về đúng chủ."
|
|
914
916
|
owner: op-retry
|
|
915
917
|
kind: check-finding
|
|
@@ -981,21 +983,21 @@ HFS_CANON_PIN_DRIFT:
|
|
|
981
983
|
kind: check-finding
|
|
982
984
|
|
|
983
985
|
HFS_CI_MISSING_CANON:
|
|
984
|
-
title: "CI runs the pinned
|
|
986
|
+
title: "CI runs the pinned `hfs lint`; pre-push runs typecheck and lint"
|
|
985
987
|
title_vi: "CI thiếu bước canon"
|
|
986
988
|
meaning_vi: "Workflow/husky thiếu bước `<step>`. Cấu trúc chỉ được kiểm khi agent chạy — CI phải tự kiểm."
|
|
987
989
|
causes_vi:
|
|
988
|
-
- "Vi phạm luật R13: CI phải chạy `npx @starci/hfs
|
|
990
|
+
- "Vi phạm luật R13: CI phải chạy `hfs lint` (`npm run lint` hoặc `npx @starci/hfs lint`, bản pin); pre-push phải có `typecheck` + `lint`."
|
|
989
991
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra (nợ hàng loạt có codemod của HFS); không cần ai can thiệp thêm."
|
|
990
992
|
owner: op-retry
|
|
991
993
|
kind: check-finding
|
|
992
994
|
|
|
993
995
|
HFS_CONTRACT_SNAPSHOT_DRIFT:
|
|
994
|
-
title: "
|
|
996
|
+
title: "The be side commits its contract snapshot and it equals the emit"
|
|
995
997
|
title_vi: "Bản hợp đồng lệch bản thật"
|
|
996
|
-
meaning_vi: "Hợp đồng `<file>` lệch bản thật (`<sha>` ≠ `<sha>`). Chạy `npm run contract:emit`
|
|
998
|
+
meaning_vi: "Hợp đồng `<file>` lệch bản thật (`<sha>` ≠ `<sha>`). Chạy `npm run contract:emit` ở gốc app rồi commit kết quả. Nếu emit không chạy được thì finding nêu nguyên văn lỗi của emit; không bao giờ bỏ qua âm thầm."
|
|
997
999
|
causes_vi:
|
|
998
|
-
- "Vi phạm luật R23:
|
|
1000
|
+
- "Vi phạm luật R23: phía be: `be/contracts/<app>/schema.graphql` và `be/contracts/<app>/openapi.json` (sinh từ bảng thao tác có kiểu `apps/<app>/src/operations.ts`) phải bằng bản `hfs emit-contracts` phát ra ngay lúc kiểm tra (đồ thị module đọc từ mã nguồn, không cần env, DB hay mạng); chỉ chạy ở lượt kiểm tra đầy đủ, `--fast` bỏ qua và nói rõ trong phần coverage. Phía fe đọc thẳng `be/contracts/` (đường đọc chéo duy nhất khai trong hfs.json `sides.fe.reads`), không giữ bản sao."
|
|
999
1001
|
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra; không cần ai can thiệp thêm."
|
|
1000
1002
|
owner: op-retry
|
|
1001
1003
|
kind: check-finding
|
|
@@ -1141,27 +1143,6 @@ HFS_HOOKS_PATH_REDIRECTED:
|
|
|
1141
1143
|
owner: op-retry
|
|
1142
1144
|
kind: check-finding
|
|
1143
1145
|
|
|
1144
|
-
HFS_INIT_EXISTS:
|
|
1145
|
-
title: "hfs.json already exists"
|
|
1146
|
-
title_vi: "hfs.json đã có sẵn"
|
|
1147
|
-
meaning_vi: "Lệnh hfs init không ghi đè bản khai báo đã có vì bản đó là quyết định của kho."
|
|
1148
|
-
causes_vi:
|
|
1149
|
-
- "Chạy hfs init trong kho đã có hfs.json"
|
|
1150
|
-
nextStep_vi: "Sửa hfs.json trực tiếp, hoặc dùng hfs init --stdout để xem bản dò tự động rồi so sánh."
|
|
1151
|
-
owner: op-retry
|
|
1152
|
-
kind: verb-refusal
|
|
1153
|
-
|
|
1154
|
-
HFS_INIT_UNDETECTED:
|
|
1155
|
-
title: "Repository profile or apps not detected"
|
|
1156
|
-
title_vi: "Không dò được loại kho hoặc ứng dụng"
|
|
1157
|
-
meaning_vi: "Lệnh hfs init không xác định chắc chắn kho là backend hay frontend, hoặc không thấy ứng dụng nào dưới apps/ để khai báo, nên không đoán mò."
|
|
1158
|
-
causes_vi:
|
|
1159
|
-
- "Không có next hay @nestjs/core trong phụ thuộc, hoặc có cả hai"
|
|
1160
|
-
- "Kho chưa có thư mục apps/<tên>/ (Nest cần thêm src/, Next cần next.config)"
|
|
1161
|
-
nextStep_vi: "Tạo bố cục apps/<tên>/ theo chuẩn hoặc viết hfs.json bằng tay theo modules/schemas/hfs-repo.schema.yaml."
|
|
1162
|
-
owner: op-retry
|
|
1163
|
-
kind: verb-refusal
|
|
1164
|
-
|
|
1165
1146
|
HFS_LINT_SUPPRESSION_FILE:
|
|
1166
1147
|
title: "A repository keeps no lint-suppression file, script or option"
|
|
1167
1148
|
title_vi: "Kho có tệp hoặc script ghi nhận bỏ qua lỗi lint"
|
|
@@ -1238,6 +1219,16 @@ HFS_PACKAGE_MANAGER_MIXED:
|
|
|
1238
1219
|
owner: other-op:backend.scaffold
|
|
1239
1220
|
kind: check-finding
|
|
1240
1221
|
|
|
1222
|
+
HFS_PEER_INTEGRATION_MISSING:
|
|
1223
|
+
title: "An app declares the runtime peer its driver integration needs"
|
|
1224
|
+
title_vi: "Thiếu gói peer mà tích hợp driver cần khi chạy"
|
|
1225
|
+
meaning_vi: "`package.json` ở gốc app phụ thuộc vào các gói của một cặp trong `knowledge/hfs/peer-integrations.yaml` (ví dụ `@nestjs/apollo` trên `@nestjs/platform-express` 11, tức Express 5) nhưng không khai báo gói `<requires>` trong dependencies, nên api không khởi động được."
|
|
1226
|
+
causes_vi:
|
|
1227
|
+
- "Vi phạm luật R111: npm không tự cài gói peer này, type-check và unit spec không nạp nó, nên lỗi chỉ lộ ra khi api khởi động (hai backend cũ thiếu `@as-integrations/express5` và api GraphQL không chạy được trên Express 5)."
|
|
1228
|
+
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra: thêm gói `<requires>` vào dependencies của `package.json` ở gốc app; không thêm ngoại lệ."
|
|
1229
|
+
owner: op-retry
|
|
1230
|
+
kind: check-finding
|
|
1231
|
+
|
|
1241
1232
|
HFS_PLAINTEXT_SECRET:
|
|
1242
1233
|
title: "Secrets exist only as `.starcistacks/<env>/secrets/<slug>.enc`"
|
|
1243
1234
|
title_vi: "Bí mật dạng rõ trong kho"
|
|
@@ -1283,7 +1274,7 @@ HFS_README_DESCRIPTION_INVALID:
|
|
|
1283
1274
|
HFS_README_DEVELOPMENT_INCOMPLETE:
|
|
1284
1275
|
title: "README Development section incomplete"
|
|
1285
1276
|
title_vi: "Mục Development trong README chưa đủ lệnh"
|
|
1286
|
-
meaning_vi: "Mục Development chưa nêu đủ các lệnh cài đặt, typecheck, lint
|
|
1277
|
+
meaning_vi: "Mục Development chưa nêu đủ các lệnh cài đặt, typecheck, lint, build và test (các script do hfs sync quản lý; test chạy bằng npm test)."
|
|
1287
1278
|
causes_vi:
|
|
1288
1279
|
- "Quên một hoặc vài lệnh"
|
|
1289
1280
|
- "Viết bằng lệnh của trình quản lý khác"
|
|
@@ -1413,7 +1404,7 @@ HFS_ROOT_MARKDOWN_FORBIDDEN:
|
|
|
1413
1404
|
kind: check-finding
|
|
1414
1405
|
|
|
1415
1406
|
HFS_ROOT_SRC_FORBIDDEN_FE:
|
|
1416
|
-
title: "Root src folder in
|
|
1407
|
+
title: "Root src folder in the front end (fe/)"
|
|
1417
1408
|
title_vi: "Repo giao diện có thư mục src ở gốc"
|
|
1418
1409
|
meaning_vi: "Repo giao diện có thư mục src ngay ở gốc, trong khi mã nguồn chỉ được đặt trong apps/<tên app>/src."
|
|
1419
1410
|
causes_vi:
|
|
@@ -1433,18 +1424,6 @@ HFS_RULE_OFF_WITHOUT_REPLACEMENT:
|
|
|
1433
1424
|
owner: op-retry
|
|
1434
1425
|
kind: check-finding
|
|
1435
1426
|
|
|
1436
|
-
HFS_SIZE_GROWTH:
|
|
1437
|
-
title: "Source file over the size budget grew"
|
|
1438
|
-
title_vi: "Tệp mã nguồn vượt hạn mức dòng vẫn tiếp tục dài thêm"
|
|
1439
|
-
meaning_vi: "Một tệp mã nguồn đã dài hơn hạn mức mềm của ruleParams.fileLines nhưng lại dài thêm so với phiên bản gốc của nhánh, hoặc một tệp mới tạo đã vượt hạn mức ngay từ đầu. Tệp quá dài khó đọc, khó sửa và là nơi lỗi dễ ẩn; quy tắc này cho phép tệp cũ đang quá dài được giữ nguyên hoặc thu nhỏ dần, nhưng không cho phép nó phình thêm."
|
|
1440
|
-
causes_vi:
|
|
1441
|
-
- "Op thêm mã mới vào một tệp vốn đã quá hạn mức thay vì tách phần mới sang tệp khác"
|
|
1442
|
-
- "Một tệp đang dưới hạn mức được thêm mã cho đến khi vượt hạn mức"
|
|
1443
|
-
- "Một tệp mới tạo dài hơn hạn mức, hoặc một tệp bị di chuyển sang đường dẫn mới nên bị coi là tệp mới"
|
|
1444
|
-
nextStep_vi: "Op vừa tạo ra bản này sẽ được chạy lại để tách phần mã mới (hoặc bớt phần cũ) sang tệp riêng đúng ô của nó, sao cho tệp không dài hơn phiên bản gốc và tệp mới nằm trong hạn mức."
|
|
1445
|
-
owner: op-retry
|
|
1446
|
-
kind: check-finding
|
|
1447
|
-
|
|
1448
1427
|
HFS_SIZE_SOFT_BACKLOG:
|
|
1449
1428
|
title: "Source file above the soft size"
|
|
1450
1429
|
title_vi: "Tệp mã vượt cỡ mềm (chỉ báo cáo)"
|
|
@@ -1478,12 +1457,12 @@ HFS_SLOT_NOT_ENABLED:
|
|
|
1478
1457
|
kind: check-finding
|
|
1479
1458
|
|
|
1480
1459
|
HFS_SLOT_REQUIRED_MISSING:
|
|
1481
|
-
title: "A slot is missing a required file (feature `index.ts`, app
|
|
1460
|
+
title: "A slot is missing a required file (feature `index.ts`, app `main.ts`, FE `global-error.tsx`)"
|
|
1482
1461
|
title_vi: "Ô thiếu tệp bắt buộc"
|
|
1483
1462
|
meaning_vi: "Một ô bắt buộc (hoặc một thực thể của ô, như một ứng dụng hay một tính năng) đòi có tệp hoặc thư mục này nhưng nó không nằm trong những gì Git theo dõi."
|
|
1484
1463
|
causes_vi:
|
|
1485
|
-
- "Vi phạm luật R02: slot bắt buộc thiếu tệp `requires` (vd feature thiếu `index.ts`, app thiếu
|
|
1486
|
-
- "Ứng dụng thiếu main.ts
|
|
1464
|
+
- "Vi phạm luật R02: slot bắt buộc thiếu tệp `requires` (vd feature thiếu `index.ts`, app thiếu `main.ts`, FE thiếu `global-error.tsx`)."
|
|
1465
|
+
- "Ứng dụng thiếu main.ts hoặc app.module.ts"
|
|
1487
1466
|
- "Kho thiếu tệp gốc bắt buộc như README.md, hfs.json, .starciwork, .starcistacks"
|
|
1488
1467
|
nextStep_vi: "Tạo tệp hoặc thư mục theo mẫu của ô rồi commit; tệp chưa git add vẫn tính là thiếu. Op vừa tạo ra bản này sẽ được chạy lại để sửa theo lời nhắn của bộ kiểm tra; không cần ai can thiệp thêm."
|
|
1489
1468
|
owner: op-retry
|
|
@@ -1525,7 +1504,7 @@ HFS_SRC_LAYOUT_INVALID:
|
|
|
1525
1504
|
kind: check-finding
|
|
1526
1505
|
|
|
1527
1506
|
HFS_STACKS_IN_FE:
|
|
1528
|
-
title: "Stack declarations in
|
|
1507
|
+
title: "Stack declarations in the front end (fe/)"
|
|
1529
1508
|
title_vi: "Khai báo stack nằm trong repo giao diện"
|
|
1530
1509
|
meaning_vi: "Repo giao diện đang chứa thư mục .starcistacks, trong khi khai báo stack chỉ được đặt ở repo backend."
|
|
1531
1510
|
causes_vi:
|
|
@@ -1622,7 +1601,7 @@ HFS_UNUSED_FILE:
|
|
|
1622
1601
|
kind: check-finding
|
|
1623
1602
|
|
|
1624
1603
|
HFS_WORK_IN_FE:
|
|
1625
|
-
title: "Work tree inside
|
|
1604
|
+
title: "Work tree inside the front end (fe/)"
|
|
1626
1605
|
title_vi: "Hồ sơ Work nằm trong repo giao diện"
|
|
1627
1606
|
meaning_vi: "Repo giao diện chứa thư mục .starciwork, trong khi hồ sơ Work chỉ được lưu ở repo backend."
|
|
1628
1607
|
causes_vi:
|
|
@@ -227,7 +227,7 @@ function externalTransportReexport(ts, sourceFile, selected) {
|
|
|
227
227
|
for (const statement of sourceFile.statements) {
|
|
228
228
|
if (!ts.isExportDeclaration(statement)) continue;
|
|
229
229
|
const clause = statement.exportClause;
|
|
230
|
-
const specifier = ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier.text : null;
|
|
230
|
+
const specifier = statement.moduleSpecifier && ts.isStringLiteralLike(statement.moduleSpecifier) ? statement.moduleSpecifier.text : null;
|
|
231
231
|
if (specifier && (specifier === '@nestjs/common' || TRANSPORT_PACKAGES.test(specifier))) {
|
|
232
232
|
if (!clause) {
|
|
233
233
|
if (specifier !== '@nestjs/common' || selected === null || [...selected].some(name => NEST_COMMON_TRANSPORT.has(name))) {
|
|
@@ -61,7 +61,12 @@ function existingRegularFile(root, relative) {
|
|
|
61
61
|
}
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
/**
|
|
65
|
+
* The npm workspaces below `root`. The one package.json of an app is at `packageRoot` (the app root; `root` is its side folder):
|
|
66
|
+
* its workspace patterns under `<side>/` are this side's, read relative to the side folder, and every other pattern is the other
|
|
67
|
+
* side's.
|
|
68
|
+
*/
|
|
69
|
+
function workspaceDirectories(root, { packageRoot: appPackageRoot = root, side = null } = {}) {
|
|
65
70
|
const directories = new Set();
|
|
66
71
|
const queue = [root];
|
|
67
72
|
const visited = new Set();
|
|
@@ -89,11 +94,14 @@ function workspaceDirectories(root) {
|
|
|
89
94
|
const packageRoot = queue.shift();
|
|
90
95
|
if (visited.has(packageRoot)) continue;
|
|
91
96
|
visited.add(packageRoot);
|
|
92
|
-
const
|
|
97
|
+
const sideRoot = side !== null && packageRoot === root;
|
|
98
|
+
const pkg = readJson(path.join(sideRoot ? appPackageRoot : packageRoot, 'package.json'));
|
|
93
99
|
const patterns = Array.isArray(pkg?.workspaces) ? pkg.workspaces : pkg?.workspaces?.packages;
|
|
94
100
|
for (const pattern of Array.isArray(patterns) ? patterns : []) {
|
|
95
101
|
if (typeof pattern !== 'string' || !pattern.trim()) throw Error('package.json workspace entries must be non-empty paths.');
|
|
96
|
-
const
|
|
102
|
+
const written = slash(pattern.trim()).replace(/^\.\//, '');
|
|
103
|
+
if (sideRoot && !written.startsWith(`${side}/`)) continue;
|
|
104
|
+
const normalized = sideRoot ? written.slice(side.length + 1) : written;
|
|
97
105
|
const segments = normalized.split('/');
|
|
98
106
|
if (path.isAbsolute(normalized) || segments.some(segment => segment === '..' || (segment.includes('*') && segment !== '*'))) {
|
|
99
107
|
throw Error(`Unsupported local workspace pattern: ${normalized}.`);
|
|
@@ -121,7 +129,10 @@ function workspaceDirectories(root) {
|
|
|
121
129
|
for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
|
|
122
130
|
for (const [name, value] of Object.entries(pkg?.[section] ?? {})) {
|
|
123
131
|
if (typeof value !== 'string' || !value.startsWith('file:')) continue;
|
|
124
|
-
const absolute = path.resolve(packageRoot, value.slice('file:'.length));
|
|
132
|
+
const absolute = path.resolve(sideRoot ? appPackageRoot : packageRoot, value.slice('file:'.length));
|
|
133
|
+
// The app's one package.json also lists the other side's file dependencies (inside the app, outside this side): not this
|
|
134
|
+
// side's. A path that leaves the app is judged like any other: a package of the same repository, or refused.
|
|
135
|
+
if (sideRoot && !isInside(root, absolute) && isInside(appPackageRoot, absolute)) continue;
|
|
125
136
|
const label = `${section}.${name} file dependency`;
|
|
126
137
|
if (isInside(root, absolute)) { admit(absolute, label, true); continue; }
|
|
127
138
|
// A sibling package of the same repository: this project consumes it, so the path is real and
|
|
@@ -248,15 +259,19 @@ const GRAMMAR_PACKAGE = '@starci/grammar';
|
|
|
248
259
|
* source, every app and every workspace package that depends on the Grammar package is a consumer. null when no app has
|
|
249
260
|
* a globals.css to judge (the contract is then reported unavailable, never passed).
|
|
250
261
|
*/
|
|
251
|
-
function derivedGrammar(root,
|
|
262
|
+
function derivedGrammar(root, packageRoot, workspaces, apps = []) {
|
|
252
263
|
const styleSources = apps.map(app => `apps/${app.name}/src/app/globals.css`).filter(relative => existingRegularFile(root, relative));
|
|
253
264
|
if (!styleSources.length) return null;
|
|
254
|
-
|
|
265
|
+
// The apps have no package.json of their own: the app root's one manifest declares their dependencies, and it is a consumer
|
|
266
|
+
// when it declares the Grammar package; so is every workspace package that does.
|
|
267
|
+
const declaresGrammar = (pkg) => Boolean(pkg && [pkg.dependencies, pkg.peerDependencies, pkg.devDependencies].some(section => section && Object.hasOwn(section, GRAMMAR_PACKAGE)));
|
|
268
|
+
const appManifest = slash(path.relative(root, path.join(packageRoot, 'package.json')));
|
|
269
|
+
const consumerManifests = declaresGrammar(readJson(path.join(packageRoot, 'package.json'))) ? [appManifest] : [];
|
|
255
270
|
for (const workspace of workspaces) {
|
|
256
271
|
if (!workspace.startsWith('packages/')) continue;
|
|
257
272
|
const manifest = `${workspace}/package.json`;
|
|
258
273
|
const pkg = readJson(path.join(root, ...manifest.split('/')));
|
|
259
|
-
if (pkg
|
|
274
|
+
if (declaresGrammar(pkg)) consumerManifests.push(manifest);
|
|
260
275
|
}
|
|
261
276
|
if (!consumerManifests.length) return null;
|
|
262
277
|
return { package: GRAMMAR_PACKAGE, entry: `${GRAMMAR_PACKAGE}/common`, styleEntry: `${GRAMMAR_PACKAGE}/common.css`, styleSources, consumerManifests, peers: ['react', '@heroui/react'] };
|
|
@@ -272,10 +287,14 @@ export function loadArchitectureConfig(repositoryRoot, { hfs } = {}) {
|
|
|
272
287
|
if (!fs.lstatSync(root).isDirectory()) throw Error('Repository root must be a directory.');
|
|
273
288
|
const opened = hfs ?? openHfs({ repoRoot: root });
|
|
274
289
|
const { profile, apps } = opened.repo;
|
|
275
|
-
|
|
276
|
-
const
|
|
290
|
+
// A side of an app (the side folder is the root the machine judges) keeps its dependencies in the app root's one package.json.
|
|
291
|
+
const side = opened.repo.side ?? null;
|
|
292
|
+
const packageRoot = side === null ? root : path.dirname(root);
|
|
293
|
+
const workspaces = workspaceDirectories(root, { packageRoot, side });
|
|
294
|
+
const appDirs = apps.map(app => `apps/${app.name}`);
|
|
295
|
+
const inferred = inferredLayout(root, [...new Set([...workspaces, ...appDirs])].sort());
|
|
277
296
|
const kinds = [profile === 'be' ? 'backend' : 'frontend'];
|
|
278
|
-
const projects = discoveredProjects(root, workspaces);
|
|
297
|
+
const projects = discoveredProjects(root, [...new Set([...workspaces, ...appDirs])].sort());
|
|
279
298
|
if (!projects.length) throw Error('The repository has no tsconfig.json to derive a TypeScript project from.');
|
|
280
299
|
const backend = {
|
|
281
300
|
modules: ['src/modules'],
|
|
@@ -290,11 +309,12 @@ export function loadArchitectureConfig(repositoryRoot, { hfs } = {}) {
|
|
|
290
309
|
hooks: inferred.hooks,
|
|
291
310
|
modules: inferred.modules,
|
|
292
311
|
transport: inferred.transport,
|
|
293
|
-
grammar: profile === 'fe' ? derivedGrammar(root,
|
|
312
|
+
grammar: profile === 'fe' ? derivedGrammar(root, packageRoot, workspaces, apps) : null,
|
|
294
313
|
};
|
|
295
314
|
if (profile === 'fe') assertFrontendRolesDisjoint(root, frontend);
|
|
296
315
|
return {
|
|
297
316
|
root,
|
|
317
|
+
packageRoot,
|
|
298
318
|
repository: enclosingRepository(root),
|
|
299
319
|
kinds,
|
|
300
320
|
projects,
|
|
@@ -276,7 +276,7 @@ function contractTypeStatusFromType(ts, checker, type, seen = new Set(), depth =
|
|
|
276
276
|
}
|
|
277
277
|
for (const anonymous of ['Record', 'Partial', 'Pick', 'Omit', 'Required']) if (builtinSymbol(ts, named, anonymous)) return 'inline';
|
|
278
278
|
if (type.aliasSymbol) return 'named';
|
|
279
|
-
if (type.symbol?.getDeclarations?.().some(declaration => ts.isEnumDeclaration(declaration))) return 'named';
|
|
279
|
+
if ((type.symbol?.getDeclarations?.() ?? []).some(declaration => ts.isEnumDeclaration(declaration))) return 'named';
|
|
280
280
|
if (type.flags & ts.TypeFlags.TypeParameter) return 'named';
|
|
281
281
|
if (type.flags & ts.TypeFlags.Boolean) return 'scalar';
|
|
282
282
|
if (type.flags & (ts.TypeFlags.Union | ts.TypeFlags.Intersection)) return 'inline';
|
|
@@ -590,7 +590,7 @@ function checkMessageReadonly(config, context, checker, declaration, localFiles,
|
|
|
590
590
|
}
|
|
591
591
|
const type = classInstanceType(ts, checker, selected);
|
|
592
592
|
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
593
|
-
const declarations = base.symbol?.getDeclarations?.().filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
593
|
+
const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
594
594
|
if (declarations.length !== 1) {
|
|
595
595
|
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
|
|
596
596
|
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) visitClass(declarations[0]);
|
|
@@ -654,7 +654,7 @@ function checkInjectedClass(config, context, checker, declaration, classKind, ta
|
|
|
654
654
|
if (classKind) {
|
|
655
655
|
const type = classInstanceType(ts, checker, declaration);
|
|
656
656
|
for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
|
|
657
|
-
const declarations = base.symbol?.getDeclarations?.().filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
657
|
+
const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
|
|
658
658
|
if (declarations.length !== 1) {
|
|
659
659
|
reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
|
|
660
660
|
} else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
|
|
@@ -709,7 +709,7 @@ export function checkBackendContracts(config, context) {
|
|
|
709
709
|
publicReasons.push(`${owner.entry} uses export =, so named capability API discovery is unavailable`);
|
|
710
710
|
}
|
|
711
711
|
for (const symbol of exportedSymbols(ts, checker, entry)) {
|
|
712
|
-
const declarations = symbol.getDeclarations?.().filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
|
|
712
|
+
const declarations = (symbol.getDeclarations?.() ?? []).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName))) ?? [];
|
|
713
713
|
if (!declarations.length) continue;
|
|
714
714
|
const representative = declarations[0];
|
|
715
715
|
if (isFrameworkHelper(representative.getSourceFile(), representative, framework)) continue;
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import { treeOf } from './required-files.mjs';
|
|
2
2
|
import { allowsFile } from '../../lib/hfs-allows.mjs';
|
|
3
|
+
import { isFeTestPath } from '../../lib/hfs-rules/fe-no-tests.mjs';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* R94 `fe-slot-allows` (FE_SLOT_FILE_ROLE). A front-end slot that owns a whole directory (`fe.route`, `fe.feature`,
|
|
6
7
|
* `fe.components`, `fe.hooks`, `fe.modules.api`) also says what its instances hold (`allows` in knowledge/hfs/slots.yaml).
|
|
7
8
|
* 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
|
|
9
|
-
*
|
|
9
|
+
* finding - `page.tsx` in a hooks domain, a `[lang]` segment under `app/`, a stray file beside a component or at a feature
|
|
10
|
+
* 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
11
|
* 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
|
+
* named by it. A test path (a `.spec.tsx` or `.test.tsx` beside a component, a test directory or test tooling) is FE_NO_TESTS's
|
|
13
|
+
* (R97, contract-change fe-no-tests): that rule is the one finding of such a file, so this check leaves it alone, as
|
|
14
|
+
* HFS_PATH_NO_SLOT does (scripts/lib/hfs-path-findings.mjs).
|
|
12
15
|
*/
|
|
13
16
|
export const FE_SLOT_ALLOWS_RULE_IDS = ['FE_SLOT_FILE_ROLE'];
|
|
14
17
|
|
|
@@ -21,6 +24,7 @@ export function checkFeSlotAllows({ config, graph }) {
|
|
|
21
24
|
let files = 0;
|
|
22
25
|
const report = (file, message, extra = {}) => violations.push({ ruleId: RULE, path: file, line: 1, column: 1, message, ...extra });
|
|
23
26
|
for (const file of [...tree.files].sort()) {
|
|
27
|
+
if (isFeTestPath(file)) continue; // FE_NO_TESTS's, the one finding of that file
|
|
24
28
|
const classified = resolver.classifyPath(file);
|
|
25
29
|
if (classified.status !== 'owned') continue;
|
|
26
30
|
const slot = resolver.slot(classified.slot);
|
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
// framework-pinned.mjs - the Next.js files that only load from a source root (the directory holding
|
|
2
|
-
// app/)
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// inc-846867b9a34e). Read by the architecture check (FE_SOURCE_LAYOUT_INVALID) and the Next name-shape
|
|
6
|
-
// check (FE_SOURCE_NAME_SHAPE).
|
|
2
|
+
// app/). Authored once in knowledge/patterns/fe/folder.yaml FE-FOLDER-1 (frameworkPinnedRootFiles), never
|
|
3
|
+
// hard-coded here (supervisor ruling, nivo wf-nivo-fe-debt-mug06w7h inc-2e42a24b74e4). Read by the architecture
|
|
4
|
+
// check (FE_SOURCE_LAYOUT_INVALID).
|
|
7
5
|
//
|
|
8
|
-
// An unreadable or malformed list is a broken install, not "no pinned files":
|
|
6
|
+
// An unreadable or malformed list is a broken install, not "no pinned files": the reader throws
|
|
9
7
|
// ARCH_KNOWLEDGE_UNAVAILABLE so the caller reports an error instead of judging with a different contract.
|
|
10
8
|
import fs from 'node:fs';
|
|
11
|
-
import path from 'node:path';
|
|
12
9
|
import { fileURLToPath } from 'node:url';
|
|
13
10
|
import { parseYaml } from '../../../engine/yaml.mjs';
|
|
14
11
|
|
|
@@ -21,11 +18,6 @@ function unavailable(file, detail) {
|
|
|
21
18
|
return Error(`ARCH_KNOWLEDGE_UNAVAILABLE: ${file} ${detail}`);
|
|
22
19
|
}
|
|
23
20
|
|
|
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
21
|
function load(file) {
|
|
30
22
|
if (cache.has(file)) return cache.get(file);
|
|
31
23
|
let rule;
|
|
@@ -38,21 +30,7 @@ function load(file) {
|
|
|
38
30
|
if (!Array.isArray(list) || !list.length || list.some(name => typeof name !== 'string' || !EXACT_NAME.test(name))) {
|
|
39
31
|
throw unavailable(file, 'FE-FOLDER-1 frameworkPinnedRootFiles must be a nonempty list of exact file names.');
|
|
40
32
|
}
|
|
41
|
-
const
|
|
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 };
|
|
33
|
+
const loaded = { files: new Set(list) };
|
|
56
34
|
if (file === FRAMEWORK_PINNED_KNOWLEDGE) cache.set(file, loaded);
|
|
57
35
|
return loaded;
|
|
58
36
|
}
|
|
@@ -61,23 +39,3 @@ function load(file) {
|
|
|
61
39
|
export function frameworkPinnedRootFiles(file = FRAMEWORK_PINNED_KNOWLEDGE) {
|
|
62
40
|
return load(file).files;
|
|
63
41
|
}
|
|
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
|
-
}
|
|
@@ -108,13 +108,15 @@ function checkGrammar(config, context) {
|
|
|
108
108
|
// Node resolution per consumer: in an npm-workspaces monorepo a consumer whose range differs from the
|
|
109
109
|
// hoisted copy gets its own apps/<app>/node_modules/<package> (nivo-fe: apps/app on 0.5.0 beside a hoisted
|
|
110
110
|
// 0.4.11). Every consumer is judged against the copy it actually resolves, never the hoisted one alone.
|
|
111
|
+
// An app installs once, at its root (config.packageRoot): a side folder has no node_modules of its own.
|
|
112
|
+
const installRoot = config.packageRoot ?? config.root;
|
|
111
113
|
const installedFrom = (from) => {
|
|
112
|
-
for (let dir = from; isInside(
|
|
114
|
+
for (let dir = from; isInside(installRoot, dir); dir = path.dirname(dir)) {
|
|
113
115
|
const candidate = path.join(dir, 'node_modules', ...grammar.package.split('/'));
|
|
114
116
|
if (fs.existsSync(path.join(candidate, 'package.json'))) return candidate;
|
|
115
|
-
if (dir ===
|
|
117
|
+
if (dir === installRoot || path.dirname(dir) === dir) break;
|
|
116
118
|
}
|
|
117
|
-
return path.join(
|
|
119
|
+
return path.join(installRoot, 'node_modules', ...grammar.package.split('/'));
|
|
118
120
|
};
|
|
119
121
|
const consumers = grammar.consumerManifests.map(relative => {
|
|
120
122
|
const root = path.dirname(path.join(config.root, ...relative.split('/')));
|
|
@@ -123,7 +125,7 @@ function checkGrammar(config, context) {
|
|
|
123
125
|
manifest: readJson(path.join(config.root, ...relative.split('/'))) ?? {} };
|
|
124
126
|
});
|
|
125
127
|
const installs = [...new Map(consumers.map(consumer => [consumer.packageRoot, consumer])).values()];
|
|
126
|
-
const packageRoot = installs[0]?.packageRoot ?? local?.root ?? path.join(
|
|
128
|
+
const packageRoot = installs[0]?.packageRoot ?? local?.root ?? path.join(installRoot, 'node_modules', ...grammar.package.split('/'));
|
|
127
129
|
const manifestFile = path.join(packageRoot, 'package.json');
|
|
128
130
|
const manifest = readJson(manifestFile);
|
|
129
131
|
const ownerOf = (fileName) => consumers.filter(consumer => isInside(consumer.root, fileName))
|
|
@@ -5,7 +5,7 @@ import { relativePath } from './typescript.mjs';
|
|
|
5
5
|
* The file and owner graph the HFS architecture checks share. Every production source file the TypeScript context
|
|
6
6
|
* loaded becomes a node classified by the slot manifest (config.hfs is the resolver of scripts/lib/hfs-slots.mjs);
|
|
7
7
|
* every import, re-export and type-only import between two of them becomes an edge. Nothing here judges: the checks
|
|
8
|
-
* in tiers.mjs, reachability.mjs, dead-exports.mjs, required-files.mjs
|
|
8
|
+
* in tiers.mjs, reachability.mjs, dead-exports.mjs, required-files.mjs and clones.mjs read it.
|
|
9
9
|
*
|
|
10
10
|
* graph.profile 'be' | 'fe'
|
|
11
11
|
* graph.resolver the slot resolver (config.hfs)
|