@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
|
@@ -444,6 +444,7 @@ BE_RAW_INJECT:
|
|
|
444
444
|
- "Token là chuỗi thay vì unique symbol"
|
|
445
445
|
- "Tiêm lớp hạ tầng (CacheService, AppLogger) không qua injector"
|
|
446
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"
|
|
447
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."
|
|
448
449
|
owner: op-retry
|
|
449
450
|
kind: check-finding
|
|
@@ -483,8 +484,9 @@ BE_SCHEMA_OWNER:
|
|
|
483
484
|
BE_SOURCE_FORM:
|
|
484
485
|
title: "Back-end source file breaks the naming or form canon"
|
|
485
486
|
title_vi: "Tệp nguồn BE sai quy ước tên hoặc hình thức"
|
|
486
|
-
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)."
|
|
487
488
|
causes_vi:
|
|
489
|
+
- "class MockExpertAgentService trong mã production thay vì adapter điều khiển bằng options"
|
|
488
490
|
- "Tệp dùng hậu tố bị cấm (use-case, repository, store, util, helper, types, constants)"
|
|
489
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"
|
|
490
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"
|
|
@@ -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
|
|
@@ -991,11 +993,11 @@ HFS_CI_MISSING_CANON:
|
|
|
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"
|
|
@@ -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:
|
|
@@ -1513,7 +1504,7 @@ HFS_SRC_LAYOUT_INVALID:
|
|
|
1513
1504
|
kind: check-finding
|
|
1514
1505
|
|
|
1515
1506
|
HFS_STACKS_IN_FE:
|
|
1516
|
-
title: "Stack declarations in
|
|
1507
|
+
title: "Stack declarations in the front end (fe/)"
|
|
1517
1508
|
title_vi: "Khai báo stack nằm trong repo giao diện"
|
|
1518
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."
|
|
1519
1510
|
causes_vi:
|
|
@@ -1610,7 +1601,7 @@ HFS_UNUSED_FILE:
|
|
|
1610
1601
|
kind: check-finding
|
|
1611
1602
|
|
|
1612
1603
|
HFS_WORK_IN_FE:
|
|
1613
|
-
title: "Work tree inside
|
|
1604
|
+
title: "Work tree inside the front end (fe/)"
|
|
1614
1605
|
title_vi: "Hồ sơ Work nằm trong repo giao diện"
|
|
1615
1606
|
meaning_vi: "Repo giao diện chứa thư mục .starciwork, trong khi hồ sơ Work chỉ được lưu ở repo backend."
|
|
1616
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))
|