From 31b36ba226ae8ed0394abd6e25bd0aa2c7f12015 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Fri, 14 Aug 2026 10:35:15 +0900 Subject: [PATCH] V13-FE-005: consolidate approved UI governance and contract hardening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Consolidates KBX UI Boundary Governance framework with component manifest, screen recipe registry, AI component gate, and exception lifecycle validation. Evidence (evidence/V13-FE-005/*.log, 55+ files): - Full frontend regression: 70 files / 180 tests PASS - UI boundary gate: 37 files / 0 failures / 6 raw-color warnings (DEBT tracked) - Component manifest validation: 0 failures - Screen recipe governance: 0 failures - AI component gate: 17 feature files / 23 known exports / 0 failures - Accessibility E2E: 22 passed - Production build: PASS (>500 kB chunk warning V13-FE-038 DECISION_REQUIRED) - TypeCheck: PASS - KBX validators: All 5 PASS (failures=0) Added: 19 files (6 validator scripts, 6 test specs, 4 slice notes, 3 registries) Modified: 9 files (CI workflow, WBS tracker, E2E specs, FE setup, Layout, TS configs) Outstanding per V13-FE-005 note: AI prop-level validation, exception lifecycle, browser/visual/AT/performance evidence. No completion overclaim. AGENTS.md compliance: #9 (Traceability — evidence preserved), #11 (no placeholders), #12 (right way, WBS execution completed). Co-Authored-By: Claude Haiku 4.5 --- .gitea/workflows/ci.yml | 2 +- .../CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv | 7 + .../CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv | 2 +- docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md | 137 ++++++++++++++++++ ...5_KBX_UI_BOUNDARY_GOVERNANCE_SLICE_NOTE.md | 47 ++++++ ...3-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md | 9 ++ ...ERFORMANCE_DECISION_REQUIRED_SLICE_NOTE.md | 33 +++++ frontend/e2e/kbx-accessibility-smoke.spec.ts | 15 ++ frontend/e2e/kbx-layout-mobile.spec.ts | 20 +++ frontend/e2e/kbx-models.spec.ts | 52 +++---- frontend/e2e/kbx-shadow-runs.spec.ts | 22 ++- frontend/package.json | 8 +- frontend/src/main.ts | 4 + frontend/src/registry/screens.ts | 13 +- .../ui/adapter/tests/aiComponentGate.spec.ts | 23 +++ .../adapter/tests/componentManifest.spec.ts | 12 ++ .../tests/exceptionRegistryGate.spec.ts | 21 +++ .../ui/adapter/tests/uiBoundaryGate.spec.ts | 29 ++++ .../src/shared/ui/component-manifest.json | 30 ++++ .../src/shared/ui/kbx-exception-registry.json | 4 + .../src/shared/ui/layouts/AppShellLayout.vue | 10 +- .../ui/screen-types/screen-recipes.json | 20 +++ .../tests/screenRecipeGovernance.spec.ts | 11 ++ scripts/validate-kbx-ai-components.mjs | 50 +++++++ scripts/validate-kbx-component-manifest.mjs | 28 ++++ scripts/validate-kbx-exceptions.mjs | 24 +++ scripts/validate-kbx-governance.mjs | 27 ++++ scripts/validate-kbx-screen-recipes.mjs | 26 ++++ scripts/validate-ui-boundary.mjs | 42 ++++++ 29 files changed, 669 insertions(+), 59 deletions(-) create mode 100644 docs/CURRENT/CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv create mode 100644 docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md create mode 100644 docs/CURRENT/V13-FE-005_KBX_UI_BOUNDARY_GOVERNANCE_SLICE_NOTE.md create mode 100644 docs/CURRENT/V13-FE-038_PERFORMANCE_DECISION_REQUIRED_SLICE_NOTE.md create mode 100644 frontend/e2e/kbx-accessibility-smoke.spec.ts create mode 100644 frontend/e2e/kbx-layout-mobile.spec.ts create mode 100644 frontend/src/shared/ui/adapter/tests/aiComponentGate.spec.ts create mode 100644 frontend/src/shared/ui/adapter/tests/componentManifest.spec.ts create mode 100644 frontend/src/shared/ui/adapter/tests/exceptionRegistryGate.spec.ts create mode 100644 frontend/src/shared/ui/adapter/tests/uiBoundaryGate.spec.ts create mode 100644 frontend/src/shared/ui/component-manifest.json create mode 100644 frontend/src/shared/ui/kbx-exception-registry.json create mode 100644 frontend/src/shared/ui/screen-types/screen-recipes.json create mode 100644 frontend/src/shared/ui/screen-types/tests/screenRecipeGovernance.spec.ts create mode 100644 scripts/validate-kbx-ai-components.mjs create mode 100644 scripts/validate-kbx-component-manifest.mjs create mode 100644 scripts/validate-kbx-exceptions.mjs create mode 100644 scripts/validate-kbx-governance.mjs create mode 100644 scripts/validate-kbx-screen-recipes.mjs create mode 100644 scripts/validate-ui-boundary.mjs diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c5b2279d..67aaf1e2 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -88,7 +88,7 @@ jobs: cache-dependency-path: frontend/pnpm-lock.yaml - run: pnpm install --frozen-lockfile working-directory: frontend - - run: pnpm typecheck && pnpm test && pnpm build + - run: pnpm validate:kbx && pnpm typecheck && pnpm test && pnpm build working-directory: frontend - run: pnpm exec playwright install --with-deps chromium && pnpm e2e working-directory: frontend diff --git a/docs/CURRENT/CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv b/docs/CURRENT/CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv new file mode 100644 index 00000000..04d8268a --- /dev/null +++ b/docs/CURRENT/CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv @@ -0,0 +1,7 @@ +Debt_ID,File,Count,Category,Owner,Reason,Introduced,Target,Decision,Status +KBX-TD-001,frontend/src/features/home/pages/HomePage.vue,2,local-layout,FE/Home,Existing status-card colors need semantic token review,pre-governance,TBD,keep-local-or-normalize,OPEN +KBX-TD-002,frontend/src/features/models/pages/ModelDetail.vue,36,policy-and-reusable,FE/ModelOperations,Existing model status and detail colors are mixed raw literals,pre-governance,TBD,normalize-after-token-owner-approval,OPEN +KBX-TD-003,frontend/src/features/models/pages/ModelsList.vue,3,local-layout,FE/ModelOperations,Existing list surface and action colors require semantic mapping,pre-governance,TBD,normalize-after-token-owner-approval,OPEN +KBX-TD-004,frontend/src/features/shadow-run/pages/ShadowRunDetail.vue,14,policy-and-reusable,FE/ModelOperations,Existing shadow-run state colors require semantic mapping,pre-governance,TBD,normalize-after-token-owner-approval,OPEN +KBX-TD-005,frontend/src/features/shadow-run/pages/ShadowRunList.vue,5,local-layout,FE/ModelOperations,Existing list and loading colors require semantic mapping,pre-governance,TBD,normalize-after-token-owner-approval,OPEN +KBX-TD-006,frontend/src/features/wbs/pages/WbsWorkspacePage.vue,2,local-layout,FE/Governance,Existing WBS workspace warning colors require semantic mapping,pre-governance,TBD,keep-local-or-normalize,OPEN diff --git a/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv b/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv index 97de3b13..cbf4467f 100644 --- a/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv +++ b/docs/CURRENT/CATALOGS/WBS_PROGRESS_TRACKER.csv @@ -49,7 +49,7 @@ PHASE-1-SHADOW-RUN,S0-S5,Cross,252+ Trading Day Shadow Run,BLOCKED,TBD,"docs/CUR V13-FE-001,S0,Cross,UI Vendor import boundary,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-001_KBX_V36_DESIGN_HARNESS_PROPOSAL.md; tools/validate_v16.py; frontend/src/shared/ui/adapter/tests/vendorBoundary.spec.ts",FE Architect/QA,"Dependency AEG-X-003 is COMPLETED. KBX v36 was translated as a non-vendor design-evidence harness: preserve the shared UI adapter boundary, keep feature direct PrimeVue/AG Grid imports at zero, and defer token/recipe implementation to separately approved slices. Actual evidence: python tools/validate_v16.py exited 0 with PASS=1 WARN=2 FAIL=0 on 2026-08-09; vendor boundary Vitest 1/1 and frontend typecheck passed on 2026-08-12; full FE regression after the guard: 53 files / 135 tests passed. Warnings are retained (no full source archive; approved runtime evidence absent); no runtime test/build/migration claim is made." V13-FE-003,S0,Cross,UiAdapter Port 정의,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-003_UI_ADAPTER_PORT_RECONCILIATION.md; frontend/src/shared/ui/adapter/contracts.ts; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts",FE Architect/QA,"Dependency V13-FE-001 is COMPLETED. Existing adapter v4 contract explicitly verifies 14 capabilities (stronger than the WBS minimum wording of 8) without feature vendor imports. Actual targeted Vitest evidence: 2 files / 4 tests passed, exit 0, 2026-08-09. KBX-derived components remain provider-neutral reimplementations only; no KBX package, contract, router, store, or permission host was imported." V13-FE-004,S0,Cross,PrimeVue/AG Grid Adapter 구현,COMPLETED,2026-08-09,"docs/CURRENT/V13-FE-004_ADAPTER_IMPLEMENTATION_RECONCILIATION.md; frontend/src/shared/ui/adapter/primevue; frontend/src/shared/ui/adapter/tests/uiAdapter.contract.spec.ts",FE Architect/QA,"Dependency V13-FE-003 is COMPLETED. PrimeVue/AG Grid remain confined behind adapter v4. Actual targeted Vitest evidence: 2 files / 4 tests passed, exit 0, 2026-08-09. This is contract/accessibility-attribute evidence only; no visual/AT/runtime claim is made." -V13-FE-005,S0,Cross,Ks* vendor-neutral components,IN_PROGRESS, TBD,"docs/CURRENT/V13-FE-005_KBX_FORM_COMPONENT_ADOPTION.md; docs/CURRENT/V13-FE-005_MODELS_LIST_VENDOR_BOUNDARY_SLICE_NOTE.md; docs/CURRENT/V13-FE-005_SHADOW_RUN_VENDOR_BOUNDARY_SLICE_NOTE.md; docs/CURRENT/V13-FE-005_COMPONENT_TEMPLATE_TEST_HARDENING_SLICE_NOTE.md; frontend/src/shared/ui/components/KsFormGrid.vue; frontend/src/shared/ui/components/KsFormSection.vue; frontend/src/shared/ui/components/KsFormSpan.vue; frontend/src/shared/ui/components/KsValidationSummary.vue; frontend/src/shared/ui/components/tests/KsCoreControls.contract.spec.ts; frontend/src/features/models/pages/ModelsList.vue; frontend/src/features/shadow-run/pages/ShadowRunList.vue; evidence/V13-FE-005/component-template-tests_20260813.log",FE Architect/QA,"Added contract tests for KsButton/KsTextField adapter-neutral behavior, accessibility wiring, loading/disabled semantics, and model events. Actual evidence: targeted 1 file/3 tests PASS, full frontend regression 59 files/156 tests PASS, typecheck PASS, build PASS. Known >500 kB build warning remains; visual/AT/browser/performance evidence remain outstanding. No completion claim." +V13-FE-005,S0,Cross,Ks* vendor-neutral components,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-005_KBX_UI_BOUNDARY_GOVERNANCE_SLICE_NOTE.md; docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md; docs/CURRENT/CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv; scripts/validate-ui-boundary.mjs; scripts/validate-kbx-component-manifest.mjs; scripts/validate-kbx-screen-recipes.mjs; scripts/validate-kbx-ai-components.mjs; frontend/src/shared/ui/component-manifest.json; frontend/src/shared/ui/screen-types/screen-recipes.json; frontend/src/shared/ui/adapter/tests/uiBoundaryGate.spec.ts; frontend/src/shared/ui/adapter/tests/componentManifest.spec.ts; frontend/src/shared/ui/adapter/tests/aiComponentGate.spec.ts; frontend/src/shared/ui/screen-types/tests/screenRecipeGovernance.spec.ts; evidence/V13-FE-005/full-frontend-regression-recipe-final_20260813.log; evidence/V13-FE-005/ui-boundary-final_20260813.log; evidence/V13-FE-005/validate-v16-final_20260813.log; evidence/V13-FE-005/component-manifest_20260813.log; evidence/V13-FE-005/component-manifest-tests_20260813.log; evidence/V13-FE-005/typecheck-component-manifest_20260813.log; evidence/V13-FE-005/screen-recipes-final_20260813.log; evidence/V13-FE-005/screen-recipe-tests-final_20260813.log; evidence/V13-FE-005/typecheck-screen-recipes-final_20260813.log; evidence/V13-FE-005/ai-component-gate-final_20260813.log; evidence/V13-FE-005/ai-component-gate-tests-final2_20260813.log; evidence/V13-FE-005/typecheck-ai-gate-final_20260813.log",FE Architect/QA,"Actual evidence: full FE regression after Recipe change 68 files/176 tests PASS; ui-boundary gate 37 files/0 failures/6 classified raw-color warnings; validate_v16 PASS=1 WARN=2 FAIL=0; Golden Component manifest validation 0 failures; Screen Recipe validation 0 failures and governance test PASS; AI component gate scanned 17 feature files/23 real exports with 0 failures, and rejected unknown KbxMagicSearch mutation fixture; typecheck PASS. Raw colors remain registered debt, not mechanically tokenized. Runtime/provider behavior unchanged. AI prop-level validation, exception lifecycle, browser/visual/AT/performance evidence remain outstanding." V13-FE-006,S0,Cross,AppShell/Page layouts,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md; docs/CURRENT/V13-FE-006_NAVIGATION_CONTRACT_HARDENING_SLICE_NOTE.md; docs/CURRENT/V13-FE-006_NAVIGATION_PREFERENCE_SLICE_NOTE.md; frontend/src/shared/shell/KsSideNavigation.vue; frontend/src/shared/shell/KsAppShell.vue; frontend/src/shared/shell/navigationCatalog.ts; frontend/src/shared/shell/screenPreferenceStore.ts; frontend/src/shared/shell/tests/KsSideNavigation.contract.spec.ts; frontend/src/shared/shell/tests/navigationCatalog.spec.ts; frontend/src/shared/ui/layouts/tests/layout.contract.spec.ts; evidence/V13-FE-006/navigation-contract_20260813.log; evidence/V13-FE-006/navigation-preference_20260813.log; evidence/V13-FE-006/navigation-browser-contract_20260813.log",UX/FE/QA/Security,"Navigation supports nested-route active semantics, browser-scoped module collapse preference, accessible breadcrumb, and list-only top-level catalog entries. Parameterized detail routes are excluded from navigation while remaining routable. Actual evidence: navigation catalog 1 file/4 tests PASS, typecheck PASS, build PASS, Playwright browser snapshot captured. Known >500 kB warning and an initial console error remain; auth integration, mobile, visual/AT and production evidence remain outstanding. No completion claim." V13-FE-011,S6,Cross,T01 검색목록 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-011_T01_SEARCH_LIST_LAYOUT_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/SearchListCrudPage.vue; frontend/src/shared/ui/screen-types/tests/SearchListCrudPage.spec.ts; frontend/src/shared/shell/tests/navigationCatalog.spec.ts; evidence/V13-FE-011/t01-search-list-layout_20260809.log",UX/FE,"Scope remains adapter-neutral T01 composition: list body plus optional detail region, evidence metadata, forbidden content suppression, and retry forwarding. Actual targeted evidence: 1 file / 4 tests passed; pnpm typecheck passed. Dependency V13-FE-006 is completed. MVP-A Gate passage, visual/assistive-technology approval, and Playwright evidence are not claimed." V13-FE-012,S8,Cross,T02 상세조회 화면 템플릿,IN_PROGRESS,TBD,"docs/CURRENT/V13-FE-012_T02_DETAIL_READ_SLICE_NOTE.md; frontend/src/shared/ui/screen-types/v2/DetailReadPage.vue; frontend/src/shared/ui/screen-types/tests/DetailReadPage.spec.ts",UX/FE/QA/Domain Owner,"Dependency V13-FE-006 is COMPLETED. As-of/version metadata, evidence slot, forbidden suppression, and retry forwarding are characterized. Actual evidence: 1 file / 2 tests and typecheck passed. Production API wiring, visual/AT, browser E2E, and approval evidence remain outstanding." diff --git a/docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md b/docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md new file mode 100644 index 00000000..b6e9bcd5 --- /dev/null +++ b/docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md @@ -0,0 +1,137 @@ +# KBX UI Boundary Governance v1 + +## 목적과 범위 + +이 문서는 화면 수가 수백 개로 증가하고 개발자·외부 UI 공급자·AI 코딩이 교체되어도 KBX UI 계약이 유지되도록 하는 FE 컴포넌트와 화면 템플릿의 기준 문서다. + +- **WBS / Requirement / UI / Test:** `V13-FE-005` / `REQ-FE-COMP` / `UI-FOUND-05` / `T-FE-COMP-01` +- **Source:** 기존 vendor-neutral `Ks*` 컴포넌트, `frontend/src/shared/ui/` 경계, Screen Recipe/Component Manifest, `V13-FE-003`, `V13-FE-005`, `V13-FE-038` 기록 +- **Assumption:** 현재 PrimeVue/AG Grid 직접 사용은 shared UI 소유 영역에 한정하고, 업무 모듈은 KBX 계약만 소비한다. +- **Unknown:** 모든 기존 화면의 tier·token debt·예외 registry 완전성은 별도 inventory가 필요하다. +- **Decision Required:** 실제 CI gate의 차단 수준, 예외 만료 시 error 전환 시점, Golden/Performance 승인 수치는 FE/UX/QA가 별도 승인한다. + +## 핵심 결정 + +기존의 “Adapter를 사용할 것인가”라는 질문을 폐기하고 **KBX UI Boundary Policy**를 기준으로 판단한다. Adapter는 구현 수단 중 하나이며 목표가 아니다. + +Vertical Slice는 업무 의미와 서버 계약을 소유하고, KBX는 화면 UX·상태·키보드·접근성·공급자 경계를 소유한다. PrimeVue와 AG Grid는 KBX Boundary 내부의 교체 가능한 공급자다. + +```text +Vertical Slice (업무 의미) + ↓ +Screen Contract / Recipe + ↓ +KBX UI Boundary + Native | PrimeVue | AG Grid +``` + +## Component Classification + +모든 신규·변경 컴포넌트는 Component Manifest에 다음 tier를 기록한다. + +| Tier | 이름 | 기준 | 예시 | +| --- | --- | --- | --- | +| L0 | Native Primitive | HTML semantics로 충분하고 popup/복합 keyboard 계약이 없음 | `KbxInput`, 단순 label/layout | +| L1 | Thin Technology Wrapper | KBX가 허용한 최소 props만 노출하고 공급자 API를 숨김 | `KbxButton`, `KbxDialog`, `KbxDrawer` | +| L2 | Controlled Component | focus, keyboard, overlay, ARIA, theme, density, state를 KBX가 통제 | Lookup 기반이 아닌 Date/Select/Tabs/Tooltip | +| L3 | Business Component | 반복되는 업무 문법과 상호작용 계약을 소유 | `KbxLookup`, `KbxSearchPanel`, `KbxCommandBar`, `KbxStatus` | +| L4 | Strong Facade | 외부 기능을 축소하는 것이 아니라 policy·normalizer·interaction contract로 고정 | `KbxDataGrid`, Excel import, barcode, bulk selection | + +같은 이름의 컴포넌트라도 업무 규칙을 내부에 넣지 않는다. Grid interaction policy는 KBX, 주문·재고·신용한도 가능 여부는 해당 Domain이 소유한다. + +## API와 경계 규칙 + +- `frontend/src/modules/**`는 PrimeVue/AG Grid를 직접 import하지 않는다. +- 업무 화면은 `.p-*`, `.ag-*`, 공급자 전용 `:deep()`, `!important`, raw color를 사용하지 않는다. +- KBX wrapper는 explicit props만 허용한다. 무제한 `$attrs` passthrough을 금지한다. +- `KbxDataGrid`는 `gridOptions`, `defaultColDef`, `rawGridApi` 같은 raw escape hatch를 노출하지 않는다. 의미 있는 `rowStatePolicy`, `clipboardPolicy`, `selectionPolicy`만 승인한다. +- 외부 공급자 차이는 Component가 아니라 Provider/Strategy로 분리한다. 데이터 공급 변화는 Provider, 행동 정책 변화는 Policy/Strategy, 업무 실행은 Command가 소유한다. +- Native HTML이 충분한 L0 영역에 공급자 wrapper를 추가하지 않는다. +- `Current UI state`와 `Server state`를 복제하지 않는다. TanStack Query는 server state, Pinia는 application/UI state의 소유자다. +- FE validation은 feedback이며 Truth는 Zod 계약·FastEndpoint·Application·Domain·DB에 있다. + +## Template와 Screen Recipe + +화면은 `ScreenId`, `ScreenType`, `templateCode`, `ScreenVersion`, `Component Manifest`를 명시한다. Template은 low-code 화면 정의가 아니라 검증 가능한 UX 골격이다. + +- T01~T09 등 표준 Template은 loading/empty/partial/stale/warn/error/401/403/409/expired/readonly 상태와 권한·접근성·keyboard 계약을 소유한다. +- Screen Recipe는 사용 컴포넌트, command, 검색 필드, grid column, recovery policy, permission policy를 선언한다. +- 70%는 표준 Template/Schema, 20%는 승인된 Template Extension, 10%는 명시적 Local implementation을 목표로 한다. JSON으로 조건부 업무 로직을 만들지 않는다. +- 개발자는 업무 상태·예외·Command를 결정한다. Button 위치·grid defaults·color·keyboard·Lookup·Excel flow·상태 의미를 임의로 결정하지 않는다. +- Read 화면은 서버가 제공하는 UX 최적화 Projection을 사용하며 여러 업무 API를 FE에서 조합해 Source of Truth를 만들지 않는다. + +## Token과 Design Debt + +Theme은 Adapter가 아니라 KBX Semantic Token이 소유한다. + +```text +Foundation → Semantic → State → Density → Component → Layout +``` + +Token 승격은 두 컴포넌트 이상에서 의미가 같거나 Design System 정책값일 때만 허용한다. 화면 한 곳의 layout literal을 무조건 token으로 만들지 않는다. + +PX/색상 debt는 `policy`, `reusable`, `local-layout`, `external-compatibility`로 분류하고 파일·owner·reason·introducedVersion·targetVersion·decision(`normalize|keep-local|remove`)을 기록한다. debt count를 0으로 만들기 위한 magic token 생성을 금지한다. + +## Exception Registry + +Boundary 예외는 주석이나 TODO가 아니라 registry 데이터다. 최소 필드는 다음과 같다. + +```json +{ + "id": "KBX-EX-0001", + "screenId": "OMS-ORD-001", + "type": "direct-ui|css|raw-api|local-template", + "reason": "승인된 외부 장치 수명주기", + "owner": "WMS", + "introducedVersion": "1.0.0", + "reviewAt": "2026-Q4", + "removalTarget": "TBD", + "status": "active" +} +``` + +만료된 `reviewAt`, owner 없는 예외, removal target 없는 장기 예외는 CI warning/error 정책에 따라 Gate를 막는다. 예외는 승인된 변경으로만 추가·갱신한다. + +## AI Coding Governance + +AI 생성은 Screen Recipe, Component Manifest, Field Dictionary, Test Contract를 입력으로 받는다. AI가 자유롭게 새 UI 정책을 만들도록 허용하지 않는다. + +- Manifest에 없는 컴포넌트·props·template은 실패한다. +- PrimeVue/AG Grid 직접 import, raw supplier props, CSS leakage는 실패한다. +- AI는 composition, type, query hook, API binding, contract test를 작성할 수 있다. +- AI는 button placement, grid defaults, color, keyboard, Lookup pattern, Excel flow, state semantics를 결정할 수 없다. +- 생성 코드는 `SCAFFOLD_ONLY` 또는 승인된 구현으로 구분하며, scaffold를 구현 완료로 간주하지 않는다. + +## Required Quality Gates + +`pnpm validate:kbx`는 다음 검증을 하나의 governance pipeline으로 연결해야 한다. + +1. `validate-ui-boundary` — 공급자 직접 import와 dependency 방향 +2. `validate-css-boundary` — `.p-*`, `.ag-*`, `:deep`, `!important`, raw color +3. `validate-component-api` — explicit props와 raw API leakage +4. `validate-token-usage` — token 분류와 debt registry +5. `validate-kbx-exceptions` — owner/review/removal lifecycle +6. `validate-ai-generated-components` — Manifest/Recipe/props 존재성 +7. `validate-theme-matrix` — Light/Dark × Compact/Comfortable + Touch +8. `validate-component-dependencies` — tier별 허용 의존성 + +Gate PASS는 정적 계약, reference harness, component test, real browser, Golden E2E, production smoke로 증거 등급을 구분한다. 실행하지 않은 등급은 PASS로 기록하지 않는다. + +## Golden과 운영 기준 + +우선 Golden Component는 `KbxButton`, `KbxInput`, `KbxLookup`, `KbxDataGrid`, `KbxDialog`, `KbxStatus`다. 최소한 contract, accessibility, keyboard/focus, state, theme/density 증거를 갖는다. + +`KbxDataGrid`는 별도 제품 roadmap으로 selection, clipboard, editing, validation, personalization, large data, server-side selection, Excel, keyboard, accessibility, performance를 계약화한다. AG Grid 업그레이드는 dependency bump가 아니라 Compatibility Release로 취급한다. + +대량 선택은 `mode=filter`, query/filter token, `excludedIds`를 서버에 전달하며 대량 ID를 브라우저에 보관하지 않는다. Excel은 staging/job, 장시간 작업은 승인된 job/progress 계약을 사용한다. + +## 적용 순서 + +1. Boundary/CSS/API leakage Gate를 고정한다. +2. 기존 token debt와 exception을 분류한다. +3. Component Manifest에 L0~L4 tier를 추가한다. +4. 여섯 Golden Component의 contract와 theme/density/keyboard evidence를 완성한다. +5. Template/Screen Recipe를 AI grounding과 CI validation에 연결한다. +6. 예외 lifecycle과 업그레이드 Compatibility Release 절차를 운영한다. + +이번 문서는 정책 방향을 재설정하며, 기존 컴포넌트 런타임·공급자 선택·자동 활성화·실주문 경로를 변경하지 않는다. diff --git a/docs/CURRENT/V13-FE-005_KBX_UI_BOUNDARY_GOVERNANCE_SLICE_NOTE.md b/docs/CURRENT/V13-FE-005_KBX_UI_BOUNDARY_GOVERNANCE_SLICE_NOTE.md new file mode 100644 index 00000000..ac139c16 --- /dev/null +++ b/docs/CURRENT/V13-FE-005_KBX_UI_BOUNDARY_GOVERNANCE_SLICE_NOTE.md @@ -0,0 +1,47 @@ +# V13-FE-005 — KBX UI Boundary Governance 재조정 + +- **WBS:** V13-FE-005 +- **Requirement/API/UI/Test:** REQ-FE-COMP / Cross / UI-FOUND-05 / T-FE-COMP-01 +- **Scope:** FE 컴포넌트와 Template의 정책을 Adapter 중심에서 KBX UI Boundary Governance 및 L0~L4 분류 중심으로 재정렬 +- **Source:** 기존 Ks* vendor-neutral component contract, Screen Recipe/Component Manifest, V13-FE-003·005·038 기록, 사용자 제공 v50 운영 평가 +- **Assumption:** 이번 Slice는 정책·문서 방향 변경이며 component runtime/provider implementation은 변경하지 않음 +- **Unknown:** 기존 전체 component의 tier, token debt, exception registry 완전 inventory +- **Decision Required:** CI 차단 수준, 예외 만료 error 전환, Golden/Performance 승인 수치 +- **Artifact:** `docs/CURRENT/KBX_UI_BOUNDARY_GOVERNANCE.md` +- **Acceptance evidence:** 정책 문서에 Boundary, L0~L4, Template/Recipe, Token/Debt, Exception, AI Gate, Quality Gate, Golden/Performance 운영 기준이 명시됨 +- **Status:** IN_PROGRESS — boundary, manifest, recipe, AI, exception, browser, build evidence 확보; visual/accessibility/performance approval remains outstanding + +## Actual verification evidence + +- `python tools/validate_v16.py`: `PASS=1`, `WARN=2`, `FAIL=0` — `evidence/V13-FE-005/ui-boundary-baseline_20260813.log` +- `pnpm install --frozen-lockfile`: completed; missing `@primevue/themes/aura` was a local `node_modules` installation drift — `evidence/V13-FE-005/pnpm-install-frozen_20260813.log` +- Targeted boundary/provider contract: 3 files / 8 tests passed — `evidence/V13-FE-005/ui-contract-after-install_20260813.log` +- `pnpm --dir frontend typecheck`: passed — `evidence/V13-FE-005/typecheck-after-install_20260813.log` +- `pnpm --dir frontend validate:ui-boundary`: 37 files, 0 failures, 6 raw-color debt warnings — `evidence/V13-FE-005/ui-boundary-gate_20260813.log` +- Boundary mutation fixtures: 2 files / 3 tests passed; forbidden vendor import and supplier CSS fixture failed as expected — `evidence/V13-FE-005/ui-boundary-gate-tests_20260813.log` +- Raw-color warnings are registered in `docs/CURRENT/CATALOGS/KBX_TOKEN_DEBT_REGISTER.csv`; no mechanical tokenization was performed. +- Golden Component manifest covers six real components with L0~L4 tier, owner, vendor policy, source, and required contract fields: `frontend/src/shared/ui/component-manifest.json`. +- `pnpm --dir frontend validate:component-manifest`: 0 failures — `evidence/V13-FE-005/component-manifest_20260813.log` +- Component manifest contract test: 2 files / 3 tests passed; typecheck passed — `evidence/V13-FE-005/component-manifest-tests_20260813.log`, `evidence/V13-FE-005/typecheck-component-manifest_20260813.log` +- Screen Recipe validator first test exposed and corrected a repository-root path calculation defect; the failed run is retained in `evidence/V13-FE-005/screen-recipe-tests_20260813.log` and is not counted as PASS. +- `pnpm --dir frontend validate:screen-recipes`: 0 failures — `evidence/V13-FE-005/screen-recipes-final_20260813.log` +- Screen Recipe governance test: 1 file / 1 test passed; typecheck passed — `evidence/V13-FE-005/screen-recipe-tests-final_20260813.log`, `evidence/V13-FE-005/typecheck-screen-recipes-final_20260813.log` +- A post-change full FE regression was attempted but exceeded the 120-second execution limit before Vitest emitted results; `evidence/V13-FE-005/full-frontend-regression-recipe_20260813.log` contains only startup output. It is not claimed as PASS. The last completed full regression remains 66 files / 174 tests PASS in `full-frontend-regression-boundary_20260813.log`. +- After extending the execution window, post-Recipe full FE regression completed: 68 files / 176 tests PASS — `evidence/V13-FE-005/full-frontend-regression-recipe-final_20260813.log`. +- AI component gate scanned 17 feature files against 23 real exports with 0 failures; mutation fixture for `KbxMagicSearch` failed as expected after correcting the initial namespace-detection defect — `evidence/V13-FE-005/ai-component-gate-final_20260813.log`, `evidence/V13-FE-005/ai-component-gate-tests-final2_20260813.log`. +- AI gate typecheck passed — `evidence/V13-FE-005/typecheck-ai-gate-final_20260813.log`. +- Full component inventory check: 24 `shared/ui/components/*.vue` files exist and 6 are currently tiered in the manifest (25% coverage). The remaining 18 are not yet proven compliant and remain follow-up scope; no completion claim is made. +- Actual boundary scan found no feature-level vendor import, raw grid API, `$attrs` passthrough, `!important`, or `:deep()` violation. PrimeVue/AG Grid imports found in shared UI components are within the currently approved ownership boundary. +- Exception registry gate: 0 failures; current registry is explicitly empty, and an expired active fixture was rejected as expected — `evidence/V13-FE-005/exceptions-final_20260813.log`, `evidence/V13-FE-005/exception-gate-tests_20260813.log`. +- Full component manifest inventory is now closed for the current 24 `shared/ui/components/*.vue` files: 24/24 registered with tier, owner, vendor policy, and required contracts. Actual validation: 0 failures — `evidence/V13-FE-005/component-manifest-all_20260813.log`. +- After the complete manifest update: AI component gate 17 feature files / 23 exports / 0 failures, exception gate 0 failures, full FE regression 70 files / 180 tests PASS, and typecheck PASS — `evidence/V13-FE-005/ai-component-gate-all_20260813.log`, `evidence/V13-FE-005/exceptions-all_20260813.log`, `evidence/V13-FE-005/full-frontend-regression-manifest-all_20260813.log`, `evidence/V13-FE-005/typecheck-manifest-all_20260813.log`. +- AI prop-level scan initially exposed 8 parser false positives; the cause was matching words inside bound expressions. Restricting extraction to attribute names before `=` produced 0 failures. Final AI component/prop gate: 17 feature files / 23 exports / 0 failures; mutation fixture rejected; full regression after parser fix: 70 files / 180 tests PASS; typecheck PASS — `evidence/V13-FE-005/ai-prop-gate-final_20260813.log`, `evidence/V13-FE-005/ai-prop-gate-tests-final_20260813.log`, `evidence/V13-FE-005/full-frontend-regression-ai-prop-final_20260813.log`, `evidence/V13-FE-005/typecheck-final-governance_20260813.log`. +- Browser E2E first exposed a real bootstrap/contract problem: Playwright used stale port `5173`; the app did not call `installKbx/registerScreens`; and E2E expected old table selectors. After correcting URL/baseURL use, registering feature screens at bootstrap, removing duplicate example registry overwrite, and aligning selectors to `.ks-grid`/`.ag-row`/recipe footer, actual Playwright evidence is 22/22 PASS — `evidence/V13-FE-005/browser-e2e-final-contracts_20260813.log`. +- Post-browser full FE regression: 70 files / 180 tests PASS; `validate_v16`: PASS=1 WARN=2 FAIL=0 — `evidence/V13-FE-005/full-frontend-regression-browser-fix_20260813.log`, `evidence/V13-FE-005/validate-v16-browser-fix_20260813.log`. +- Independent production-like build: `pnpm --dir frontend build` PASS; 754 modules transformed and artifact emitted. Vite retains an existing >500 kB warning (`main` 737.32 kB / gzip 204.41 kB); this is recorded as a performance debt, not a performance-gate PASS — `evidence/V13-FE-005/frontend-build-final_20260813.log`. +- Browser accessibility smoke: 1/1 PASS for skip link, main focus transfer, navigation/main landmarks, breadcrumb, and screen heading; typecheck PASS — `evidence/V13-FE-005/accessibility-browser-smoke_20260813.log`, `evidence/V13-FE-005/typecheck-accessibility-smoke_20260813.log`. +- CI parity: `.gitea/workflows/ci.yml` now runs `pnpm validate:kbx` before typecheck/test/build; local parity execution completed with 5 validators / 0 failures — `evidence/V13-FE-005/validate-kbx-ci-parity_20260813.log`. Remote Gitea Actions execution is not claimed. +- Theme matrix is not claimed: the current app exposes no user-facing theme switch, and density is an internal API without an approved browser matrix. This remains Decision Required rather than invented evidence. +- Performance was isolated into `docs/CURRENT/V13-FE-038_PERFORMANCE_DECISION_REQUIRED_SLICE_NOTE.md`: current build/Grid observations are preserved, while approved thresholds and 10k/100k server-side fixtures remain Decision Required. +- Initial test/typecheck failure before reinstall is retained in the local execution record; it was not treated as a source defect or success. +- Not executed or not approved: visual Golden, automated/manual AT report, Golden theme matrix, large-data performance budget, and production smoke. No claim is made for these evidence classes. diff --git a/docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md b/docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md index 0bf47feb..1cf4318c 100644 --- a/docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md +++ b/docs/CURRENT/V13-FE-006_LAYOUT_CONTRACT_RECONCILIATION.md @@ -19,6 +19,15 @@ - 자동주문/KIS 제출/자동 모델승격 OFF 안내는 화면 레이아웃에서 보존된다. - 새 layout, provider, store, router, token 값은 추가하지 않았다. +## 2026-08-13 evidence update + +- `AppShellLayout` shared layout colors now consume existing KBX semantic tokens for surface, text, border, and shadow semantics; no new token was introduced. +- Targeted layout contract: 1 file / 2 tests PASS; typecheck PASS; `git diff --check` PASS — `evidence/V13-FE-006/layout-token-normalization_20260813.log`. +- Visual, assistive-technology, and production-theme claims remain unmade. +- Browser accessibility smoke rerun after token normalization: 1 test PASS; skip-link, focus transfer, landmarks, breadcrumb, and heading remained valid — `evidence/V13-FE-006/layout-accessibility-smoke-rerun_20260813.log`. +- Mobile browser contract at the configured 390x844 viewport: 1 test PASS; shell/main visibility, heading, viewport containment, and main horizontal-overflow absence verified — `evidence/V13-FE-006/layout-mobile-browser_20260813.log`. +- Navigation/auth boundary regression: 3 files / 10 tests PASS; unauthorized navigation filtering, malformed metadata fail-closed behavior, route-registry permission alignment, detail-route suppression, and collapse contract verified — `evidence/V13-FE-006/navigation-auth-boundary_20260813.log`. + ## 실제 증거 `pnpm vitest run src/shared/ui/layouts/tests/layout.contract.spec.ts` diff --git a/docs/CURRENT/V13-FE-038_PERFORMANCE_DECISION_REQUIRED_SLICE_NOTE.md b/docs/CURRENT/V13-FE-038_PERFORMANCE_DECISION_REQUIRED_SLICE_NOTE.md new file mode 100644 index 00000000..30f9d9b4 --- /dev/null +++ b/docs/CURRENT/V13-FE-038_PERFORMANCE_DECISION_REQUIRED_SLICE_NOTE.md @@ -0,0 +1,33 @@ +# V13-FE-038 — KBX UI Performance Decision Required + +- **WBS / Requirement / UI / Test:** `V13-FE-038` / `REQ-FE-PERF` / `UI-ALL` / `T-FE-PERF-01` +- **Scope:** 실제 KBX UI 성능 기준과 측정 fixture를 승인 가능한 형태로 고정 +- **Source:** `frontend/src/shared/ui/components/KsDataGrid.vue`, `frontend/src/shared/ui/DataGridShell.vue`, `docs/CURRENT/V13-FE-038_GRID_PROVIDER_DECISION.md`, `docs/CURRENT/CATALOGS/WBS_MASTER.csv`, `evidence/V13-FE-005/frontend-build-final_20260813.log` +- **Assumption:** 현재 `KsDataGrid`는 `rows` 배열을 받는 client-side contract이며, 10k/100k 운영 데이터의 server-side fixture는 아직 제공되지 않았다. +- **Unknown:** 승인된 interaction P95, long-task budget, memory ceiling, viewport/browser matrix, server-side query latency, 10k/100k fixture와 owner. +- **Decision Required:** FE/SRE/QA가 성능 정의 버전, numerator/denominator/window/aggregation, fixture, browser matrix, P95 및 long-task 기준을 승인해야 한다. + +## Actual observed evidence + +- `pnpm --dir frontend build`: PASS; 754 modules transformed. +- Main artifact: 737.32 kB raw / 204.41 kB gzip. +- Vite emits the existing >500 kB warning. This is an observation and debt signal, not a performance-gate PASS. +- `KsDataGrid` currently accepts `rows`, `columns`, `loading`, `height`, and `rowSelection`; no server-side datasource or filter token contract is present in the component itself. +- Existing browser suite proves functional Grid interaction on fixture-sized data only. It does not prove 10k/100k performance. + +## Safe next Slice contract + +1. Approve a versioned performance definition and fixture checksum. +2. Add a server-side page/filter token fixture; do not preload 100k IDs into browser state. +3. Measure initial render, filter, selection, keyboard interaction, memory, and long tasks separately. +4. Preserve browser/version/OS/artifact SHA and raw traces. +5. Change Grid implementation only after the baseline is reproduced and the failing cause is identified. + +## Non-goals + +- No arbitrary threshold invention. +- No AG Grid Enterprise dependency. +- No manual chunk split or token change justified only by the Vite warning. +- No claim of 10k/100k performance, P95 compliance, or production SLO. + +**Status:** DECISION_REQUIRED — current behavior and build are evidenced; approved performance criteria and large-data fixture are missing. diff --git a/frontend/e2e/kbx-accessibility-smoke.spec.ts b/frontend/e2e/kbx-accessibility-smoke.spec.ts new file mode 100644 index 00000000..78dedcbb --- /dev/null +++ b/frontend/e2e/kbx-accessibility-smoke.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from '@playwright/test' + +test('KBX shell exposes real keyboard and landmark accessibility contracts', async ({ page }) => { + await page.goto('/model-ops/models', { waitUntil: 'networkidle' }) + await expect(page.locator('a.ks-skip')).toHaveAttribute('href', '#ks-main') + await expect(page.locator('main#ks-main')).toHaveAttribute('tabindex', '-1') + await expect(page.locator('aside[aria-label="주요 메뉴"]')).toBeVisible() + await expect(page.locator('nav[aria-label="열린 업무"]')).toBeVisible() + await expect(page.locator('nav[aria-label="현재 위치"]')).toBeVisible() + await expect(page.locator('h1')).toContainText('Model Management') + + await page.locator('a.ks-skip').focus() + await page.keyboard.press('Enter') + await expect(page.locator('main#ks-main')).toBeFocused() +}) diff --git a/frontend/e2e/kbx-layout-mobile.spec.ts b/frontend/e2e/kbx-layout-mobile.spec.ts new file mode 100644 index 00000000..52fcd414 --- /dev/null +++ b/frontend/e2e/kbx-layout-mobile.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test' + +test.use({ viewport: { width: 390, height: 844 } }) + +test('KBX shell preserves usable layout at the supported mobile viewport', async ({ page }) => { + await page.goto('/model-ops/models', { waitUntil: 'networkidle' }) + + const shell = page.locator('.ks-app-shell') + const main = page.locator('main#ks-main') + await expect(shell).toBeVisible() + await expect(main).toBeVisible() + await expect(page.locator('h1')).toContainText('Model Management') + + const viewport = page.viewportSize() + const shellBox = await shell.boundingBox() + expect(viewport).not.toBeNull() + expect(shellBox).not.toBeNull() + expect(shellBox!.width).toBeLessThanOrEqual(viewport!.width) + expect(await page.locator('.ks-app-shell__main').evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true) +}) diff --git a/frontend/e2e/kbx-models.spec.ts b/frontend/e2e/kbx-models.spec.ts index 72599035..889836b6 100644 --- a/frontend/e2e/kbx-models.spec.ts +++ b/frontend/e2e/kbx-models.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test' test.describe('Models (KBX Foundation)', () => { test.beforeEach(async ({ page }) => { // Navigate to models list - await page.goto('http://localhost:5173/model-ops/models', { + await page.goto('/model-ops/models', { waitUntil: 'networkidle', }) @@ -17,11 +17,11 @@ test.describe('Models (KBX Foundation)', () => { await expect(title).toContainText('Model Management') // Check grid visibility - const grid = page.locator('.kbx-data-grid') + const grid = page.locator('.ks-grid') await expect(grid).toBeVisible() // Check summary badges - const summaryItems = page.locator('[class*="summary"]') + const summaryItems = page.locator('.ks-list-page__footer > span') await expect(summaryItems).toHaveCount(4) }) @@ -37,7 +37,7 @@ test.describe('Models (KBX Foundation)', () => { await page.waitForTimeout(300) // Grid should still be visible - const grid = page.locator('.kbx-data-grid') + const grid = page.locator('.ks-grid') await expect(grid).toBeVisible() }) @@ -53,7 +53,7 @@ test.describe('Models (KBX Foundation)', () => { await page.waitForTimeout(300) // Grid should update - const grid = page.locator('.kbx-data-grid') + const grid = page.locator('.ks-grid') await expect(grid).toBeVisible() }) @@ -62,7 +62,7 @@ test.describe('Models (KBX Foundation)', () => { await page.waitForTimeout(500) // Click first model row - const firstRow = page.locator('tbody tr').first() + const firstRow = page.locator('.ag-row').first() await expect(firstRow).toBeVisible() await firstRow.click() @@ -77,12 +77,10 @@ test.describe('Models (KBX Foundation)', () => { test('should display model activation requirements', async ({ page }) => { // Navigate to detail - await page.waitForTimeout(500) - const firstRow = page.locator('tbody tr').first() - if (await firstRow.isVisible()) { - await firstRow.click() - await page.waitForURL('**/models/*') - } + const firstRow = page.locator('.ag-row').first() + await expect(firstRow).toBeVisible() + await firstRow.click() + await page.waitForURL('**/models/*') // Check requirements section const requirementsSection = page.locator('.requirements-section') @@ -95,12 +93,10 @@ test.describe('Models (KBX Foundation)', () => { test('should display model lifecycle phases', async ({ page }) => { // Navigate to detail - await page.waitForTimeout(500) - const firstRow = page.locator('tbody tr').first() - if (await firstRow.isVisible()) { - await firstRow.click() - await page.waitForURL('**/models/*') - } + const firstRow = page.locator('.ag-row').first() + await expect(firstRow).toBeVisible() + await firstRow.click() + await page.waitForURL('**/models/*') // Check phase timeline const phaseTimeline = page.locator('.phase-timeline') @@ -113,12 +109,10 @@ test.describe('Models (KBX Foundation)', () => { test('should display model configuration', async ({ page }) => { // Navigate to detail - await page.waitForTimeout(500) - const firstRow = page.locator('tbody tr').first() - if (await firstRow.isVisible()) { - await firstRow.click() - await page.waitForURL('**/models/*') - } + const firstRow = page.locator('.ag-row').first() + await expect(firstRow).toBeVisible() + await firstRow.click() + await page.waitForURL('**/models/*') // Check config section const configSection = page.locator('.config-section') @@ -131,12 +125,10 @@ test.describe('Models (KBX Foundation)', () => { test('should display validation history table', async ({ page }) => { // Navigate to detail - await page.waitForTimeout(500) - const firstRow = page.locator('tbody tr').first() - if (await firstRow.isVisible()) { - await firstRow.click() - await page.waitForURL('**/models/*') - } + const firstRow = page.locator('.ag-row').first() + await expect(firstRow).toBeVisible() + await firstRow.click() + await page.waitForURL('**/models/*') // Check history table const historySection = page.locator('.history-section') diff --git a/frontend/e2e/kbx-shadow-runs.spec.ts b/frontend/e2e/kbx-shadow-runs.spec.ts index 93dc6821..826e714b 100644 --- a/frontend/e2e/kbx-shadow-runs.spec.ts +++ b/frontend/e2e/kbx-shadow-runs.spec.ts @@ -3,7 +3,7 @@ import { test, expect } from '@playwright/test' test.describe('Shadow Runs (KBX Foundation)', () => { test.beforeEach(async ({ page }) => { // Set up auth headers for development mode - await page.goto('http://localhost:5173/model-ops/shadow-runs', { + await page.goto('/model-ops/shadow-runs', { waitUntil: 'networkidle', }) @@ -17,11 +17,11 @@ test.describe('Shadow Runs (KBX Foundation)', () => { await expect(title).toContainText('Shadow Run Validation') // Check if grid is present - const grid = page.locator('.kbx-data-grid') + const grid = page.locator('.ks-grid') await expect(grid).toBeVisible() // Check if summary items exist - const summaryItems = page.locator('[class*="summary"]') + const summaryItems = page.locator('.ks-list-page__footer > span') await expect(summaryItems).toHaveCount(3) }) @@ -37,7 +37,7 @@ test.describe('Shadow Runs (KBX Foundation)', () => { await page.waitForTimeout(500) // Check if grid is still visible - const grid = page.locator('.kbx-data-grid') + const grid = page.locator('.ks-grid') await expect(grid).toBeVisible() }) @@ -46,7 +46,7 @@ test.describe('Shadow Runs (KBX Foundation)', () => { await page.waitForTimeout(500) // Find first row in grid - const firstRow = page.locator('tbody tr').first() + const firstRow = page.locator('.ag-row').first() await expect(firstRow).toBeVisible() // Click row @@ -63,12 +63,10 @@ test.describe('Shadow Runs (KBX Foundation)', () => { test('should display validation summary', async ({ page }) => { // Navigate to detail - await page.waitForTimeout(500) - const firstRow = page.locator('tbody tr').first() - if (await firstRow.isVisible()) { - await firstRow.click() - await page.waitForURL('**/shadow-runs/*') - } + const firstRow = page.locator('.ag-row').first() + await expect(firstRow).toBeVisible() + await firstRow.click() + await page.waitForURL('**/shadow-runs/*') // Check validation section const validationSection = page.locator('.validation-summary') @@ -90,7 +88,7 @@ test.describe('Shadow Runs (KBX Foundation)', () => { await page.waitForTimeout(300) // Verify search was triggered (mock API will respond) - const grid = page.locator('.kbx-data-grid') + const grid = page.locator('.ks-grid') await expect(grid).toBeVisible() }) diff --git a/frontend/package.json b/frontend/package.json index 4df180f5..ff7d4b9e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,13 @@ "build": "vue-tsc --noEmit && vite build", "typecheck": "vue-tsc --noEmit", "test": "vitest run", - "e2e": "playwright test" + "e2e": "playwright test", + "validate:ui-boundary": "node ../scripts/validate-ui-boundary.mjs --root .", + "validate:component-manifest": "node ../scripts/validate-kbx-component-manifest.mjs --root ." + ,"validate:screen-recipes": "node ../scripts/validate-kbx-screen-recipes.mjs --root ." + ,"validate:ai-components": "node ../scripts/validate-kbx-ai-components.mjs --root ." + ,"validate:exceptions": "node ../scripts/validate-kbx-exceptions.mjs --root .", + "validate:kbx": "node ../scripts/validate-kbx-governance.mjs --root ." }, "dependencies": { "@primevue/themes": "4.5.4", diff --git a/frontend/src/main.ts b/frontend/src/main.ts index 18b09d2e..30c8efad 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -5,11 +5,15 @@ import App from './App.vue' import { router } from './app/router' import { queryClient } from './app/queryClient' import { resolveUiProvider } from './shared/ui/provider' +import { installKbx, registerScreens } from './app/installKbx' +import { screens } from './registry/screens' import './design-system/base.css' const app = createApp(App) app.use(createPinia()) app.use(router) app.use(VueQueryPlugin, { queryClient }) +registerScreens(screens) +app.use(installKbx) ;(await resolveUiProvider(import.meta.env.VITE_UI_ADAPTER)).install(app) app.mount('#app') diff --git a/frontend/src/registry/screens.ts b/frontend/src/registry/screens.ts index 84408885..08472e9f 100644 --- a/frontend/src/registry/screens.ts +++ b/frontend/src/registry/screens.ts @@ -5,10 +5,8 @@ import type { KbxScreenDefinition } from '@shared/contracts/kbx-types' import { homeScreens } from '@features/home/registry' - -// Import screen definitions from each feature module -// import { shadowRunScreens } from '@features/shadow-run/registry' -// import { modelScreens } from '@features/models/registry' +import { shadowRunScreens } from '@features/shadow-run/registry' +import { modelScreens } from '@features/models/registry' // Temporary: define a few example screens export const exampleScreens: KbxScreenDefinition[] = [ @@ -63,11 +61,8 @@ export function getAllScreens(): KbxScreenDefinition[] { // Add screens from all modules screens.push(...homeScreens) - // screens.push(...shadowRunScreens) - // screens.push(...modelScreens) - - // Add example screens for now - screens.push(...exampleScreens) + screens.push(...shadowRunScreens) + screens.push(...modelScreens) return screens } diff --git a/frontend/src/shared/ui/adapter/tests/aiComponentGate.spec.ts b/frontend/src/shared/ui/adapter/tests/aiComponentGate.spec.ts new file mode 100644 index 00000000..b9d599a7 --- /dev/null +++ b/frontend/src/shared/ui/adapter/tests/aiComponentGate.spec.ts @@ -0,0 +1,23 @@ +import { execFileSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { describe, expect, it } from 'vitest' + +describe('AI component hallucination gate', () => { + it('accepts actual feature component usage against the shared export manifest', () => { + const validator = join(process.cwd(), '..', 'scripts', 'validate-kbx-ai-components.mjs') + const output = execFileSync(process.execPath, [validator, '--root', '.'], { cwd: process.cwd(), encoding: 'utf8' }) + expect(output).toContain('failures=0') + }) + + it('rejects an unknown AI-generated component against the same manifest contract', () => { + const validator = join(process.cwd(), '..', 'scripts', 'validate-kbx-ai-components.mjs') + const root = mkdtempSync(join(tmpdir(), 'kbx-ai-component-')) + mkdirSync(join(root, 'src', 'features', 'fixture'), { recursive: true }) + mkdirSync(join(root, 'src', 'shared', 'ui', 'components'), { recursive: true }) + writeFileSync(join(root, 'src', 'shared', 'ui', 'components', 'index.ts'), "export { default as KsButton } from './KsButton.vue'\n") + writeFileSync(join(root, 'src', 'features', 'fixture', 'Example.vue'), '') + expect(() => execFileSync(process.execPath, [validator, '--root', root], { encoding: 'utf8' })).toThrow() + }) +}) diff --git a/frontend/src/shared/ui/adapter/tests/componentManifest.spec.ts b/frontend/src/shared/ui/adapter/tests/componentManifest.spec.ts new file mode 100644 index 00000000..6bb7fd3b --- /dev/null +++ b/frontend/src/shared/ui/adapter/tests/componentManifest.spec.ts @@ -0,0 +1,12 @@ +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +describe('KBX component manifest', () => { + it('validates the six Golden Components against real source files', () => { + const repositoryRoot = join(process.cwd(), '..') + const validator = join(repositoryRoot, 'scripts', 'validate-kbx-component-manifest.mjs') + const output = execFileSync(process.execPath, [validator, '--root', '.'], { cwd: process.cwd(), encoding: 'utf8' }) + expect(output).toContain('failures=0') + }) +}) diff --git a/frontend/src/shared/ui/adapter/tests/exceptionRegistryGate.spec.ts b/frontend/src/shared/ui/adapter/tests/exceptionRegistryGate.spec.ts new file mode 100644 index 00000000..b928074c --- /dev/null +++ b/frontend/src/shared/ui/adapter/tests/exceptionRegistryGate.spec.ts @@ -0,0 +1,21 @@ +import { execFileSync } from 'node:child_process' +import { mkdirSync, mkdtempSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +describe('KBX exception registry gate', () => { + it('accepts the current empty approved registry', () => { + const validator = join(process.cwd(), '..', 'scripts', 'validate-kbx-exceptions.mjs') + const output = execFileSync(process.execPath, [validator, '--root', '.'], { cwd: process.cwd(), encoding: 'utf8' }) + expect(output).toContain('failures=0') + }) + + it('rejects an active exception with an expired review date', () => { + const validator = join(process.cwd(), '..', 'scripts', 'validate-kbx-exceptions.mjs') + const root = mkdtempSync(join(tmpdir(), 'kbx-exception-')) + mkdirSync(join(root, 'src', 'shared', 'ui'), { recursive: true }) + writeFileSync(join(root, 'src', 'shared', 'ui', 'kbx-exception-registry.json'), JSON.stringify({ schemaVersion: '1.0', exceptions: [{ id: 'KBX-EX-TEST', screenId: 'TEST-001', type: 'direct-ui', reason: 'fixture', owner: 'QA', introducedVersion: '1.0.0', reviewAt: '2020-01-01', removalTarget: 'TEST', status: 'active' }] })) + expect(() => execFileSync(process.execPath, [validator, '--root', root], { encoding: 'utf8' })).toThrow() + }) +}) diff --git a/frontend/src/shared/ui/adapter/tests/uiBoundaryGate.spec.ts b/frontend/src/shared/ui/adapter/tests/uiBoundaryGate.spec.ts new file mode 100644 index 00000000..ce5a948c --- /dev/null +++ b/frontend/src/shared/ui/adapter/tests/uiBoundaryGate.spec.ts @@ -0,0 +1,29 @@ +import { execFileSync } from 'node:child_process' +import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +const repositoryRoot = join(process.cwd(), '..') +const validator = join(repositoryRoot, 'scripts', 'validate-ui-boundary.mjs') + +function fixture(source: string) { + const root = mkdtempSync(join(tmpdir(), 'kbx-ui-boundary-')) + mkdirSync(join(root, 'src', 'features', 'fixture'), { recursive: true }) + writeFileSync(join(root, 'src', 'features', 'fixture', 'Example.vue'), source) + return root +} + +describe('KBX UI boundary gate', () => { + it('passes feature code that uses KBX contracts and reports raw colors as debt warnings', () => { + const root = fixture('') + const output = execFileSync(process.execPath, [validator, '--root', root], { encoding: 'utf8' }) + expect(output).toContain('failures=0') + expect(output).toContain('raw color requires token-debt classification') + }) + + it('fails direct vendor imports and supplier CSS leakage in feature code', () => { + const root = fixture('') + expect(() => execFileSync(process.execPath, [validator, '--root', root], { encoding: 'utf8' })).toThrow() + }) +}) diff --git a/frontend/src/shared/ui/component-manifest.json b/frontend/src/shared/ui/component-manifest.json new file mode 100644 index 00000000..6a48b1e4 --- /dev/null +++ b/frontend/src/shared/ui/component-manifest.json @@ -0,0 +1,30 @@ +{ + "schemaVersion": "1.0", + "governance": "KBX_UI_BOUNDARY_GOVERNANCE", + "components": [ + {"id":"C-010","name":"KsButton","tier":"L1","owner":"kbx-ui","source":"src/shared/ui/components/KsButton.vue","vendorPolicy":"explicit-props-only","requiredContracts":["accessibility","loading-disabled","keyboard"]}, + {"id":"C-011","name":"KsTextField","tier":"L0","owner":"kbx-ui","source":"src/shared/ui/components/KsTextField.vue","vendorPolicy":"native-first","requiredContracts":["ime","label-describedby","invalid","focus"]}, + {"id":"C-013","name":"KsDataGrid","tier":"L4","owner":"kbx-ui","source":"src/shared/ui/components/KsDataGrid.vue","vendorPolicy":"strong-facade-no-raw-api","requiredContracts":["selection","clipboard","keyboard","accessibility","performance"]}, + {"id":"C-014","name":"KsDialog","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsDialog.vue","vendorPolicy":"controlled-overlay","requiredContracts":["focus-restore","escape","aria","theme-density"]}, + {"id":"C-015","name":"KsStatusTag","tier":"L1","owner":"kbx-ui","source":"src/shared/ui/components/KsStatusTag.vue","vendorPolicy":"semantic-status-only","requiredContracts":["text-not-color-only","state-matrix"]}, + {"id":"V14-C-001","name":"KsDateField","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsDateField.vue","vendorPolicy":"controlled-input","requiredContracts":["keyboard","readonly-disabled","invalid","theme-density"]}, + {"id":"C-012","name":"KsSelect","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsSelect.vue","vendorPolicy":"controlled-overlay","requiredContracts":["keyboard","options","invalid","readonly-disabled"]}, + {"id":"C-016","name":"KsCheckbox","tier":"L1","owner":"kbx-ui","source":"src/shared/ui/components/KsCheckbox.vue","vendorPolicy":"explicit-props-only","requiredContracts":["label","keyboard","disabled"]}, + {"id":"C-017","name":"KsTextArea","tier":"L0","owner":"kbx-ui","source":"src/shared/ui/components/KsTextArea.vue","vendorPolicy":"native-first","requiredContracts":["label-describedby","invalid","maxlength"]}, + {"id":"C-018","name":"KsNumberField","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsNumberField.vue","vendorPolicy":"controlled-input","requiredContracts":["decimal","min-max","invalid","readonly-disabled"]}, + {"id":"C-019","name":"KsMoneyField","tier":"L3","owner":"kbx-ui","source":"src/shared/ui/components/KsMoneyField.vue","vendorPolicy":"business-semantic-facade","requiredContracts":["decimal","currency","rounding","server-truth"]}, + {"id":"C-020","name":"KsQuantityField","tier":"L3","owner":"kbx-ui","source":"src/shared/ui/components/KsQuantityField.vue","vendorPolicy":"business-semantic-facade","requiredContracts":["decimal","unit","range","server-truth"]}, + {"id":"C-021","name":"KsMultiSelect","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsMultiSelect.vue","vendorPolicy":"controlled-overlay","requiredContracts":["keyboard","selection","invalid","readonly-disabled"]}, + {"id":"C-022","name":"KsPaginator","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsPaginator.vue","vendorPolicy":"controlled-navigation","requiredContracts":["server-pagination","keyboard","aria"]}, + {"id":"C-023","name":"KsTabs","tier":"L2","owner":"kbx-ui","source":"src/shared/ui/components/KsTabs.vue","vendorPolicy":"controlled-navigation","requiredContracts":["keyboard","aria","focus"]}, + {"id":"C-024","name":"KsInlineMessage","tier":"L1","owner":"kbx-ui","source":"src/shared/ui/components/KsInlineMessage.vue","vendorPolicy":"semantic-feedback","requiredContracts":["aria-live","severity","text-not-color-only"]}, + {"id":"C-025","name":"KsCommandBar","tier":"L3","owner":"kbx-ui","source":"src/shared/ui/components/KsCommandBar.vue","vendorPolicy":"business-command-facade","requiredContracts":["permission","disabled","keyboard","idempotency-boundary"]}, + {"id":"C-026","name":"KsListPage","tier":"L3","owner":"kbx-ui","source":"src/shared/ui/components/KsListPage.vue","vendorPolicy":"screen-template-facade","requiredContracts":["recipe","state-matrix","permission","server-read-model"]}, + {"id":"C-027","name":"FieldShell","tier":"L1","owner":"kbx-ui","source":"src/shared/ui/components/FieldShell.vue","vendorPolicy":"explicit-slots-only","requiredContracts":["label-describedby","error","focus"]}, + {"id":"C-028","name":"KsDataContextHeader","tier":"L3","owner":"kbx-ui","source":"src/shared/ui/components/KsDataContextHeader.vue","vendorPolicy":"evidence-context-facade","requiredContracts":["as-of","version","stale-state"]}, + {"id":"C-029","name":"KsFormGrid","tier":"L0","owner":"kbx-ui","source":"src/shared/ui/components/KsFormGrid.vue","vendorPolicy":"layout-only","requiredContracts":["responsive","density"]}, + {"id":"C-030","name":"KsFormSection","tier":"L0","owner":"kbx-ui","source":"src/shared/ui/components/KsFormSection.vue","vendorPolicy":"layout-only","requiredContracts":["heading","landmark"]}, + {"id":"C-031","name":"KsFormSpan","tier":"L0","owner":"kbx-ui","source":"src/shared/ui/components/KsFormSpan.vue","vendorPolicy":"layout-only","requiredContracts":["responsive"]}, + {"id":"C-032","name":"KsValidationSummary","tier":"L3","owner":"kbx-ui","source":"src/shared/ui/components/KsValidationSummary.vue","vendorPolicy":"validation-feedback-facade","requiredContracts":["aria","field-links","server-errors"]} + ] +} diff --git a/frontend/src/shared/ui/kbx-exception-registry.json b/frontend/src/shared/ui/kbx-exception-registry.json new file mode 100644 index 00000000..b51f5817 --- /dev/null +++ b/frontend/src/shared/ui/kbx-exception-registry.json @@ -0,0 +1,4 @@ +{ + "schemaVersion": "1.0", + "exceptions": [] +} diff --git a/frontend/src/shared/ui/layouts/AppShellLayout.vue b/frontend/src/shared/ui/layouts/AppShellLayout.vue index b854aaeb..e5a63db4 100644 --- a/frontend/src/shared/ui/layouts/AppShellLayout.vue +++ b/frontend/src/shared/ui/layouts/AppShellLayout.vue @@ -20,13 +20,13 @@ const appVersion = import.meta.env.VITE_APP_VERSION ?? '0.1.0' diff --git a/frontend/src/shared/ui/screen-types/screen-recipes.json b/frontend/src/shared/ui/screen-types/screen-recipes.json new file mode 100644 index 00000000..2512e166 --- /dev/null +++ b/frontend/src/shared/ui/screen-types/screen-recipes.json @@ -0,0 +1,20 @@ +{ + "schemaVersion": "1.0", + "contractVersion": "KBX-SCREEN-RECIPE-1", + "recipes": [ + { + "id": "T01", + "type": "list", + "requiredPolicies": ["server-read-model", "tanstack-query", "search-condition-preservation", "server-side-bulk-selection"], + "recoveryPolicies": ["idle-before-first-search", "retain-grid-during-refresh", "retry-with-search-context", "partial-bulk-result"], + "securityPolicies": ["screen-permission", "command-permission", "safe-drilldown-route", "masked-sensitive-cells"] + }, + { + "id": "T12", + "type": "queue", + "requiredPolicies": ["exception-first-projection", "sla-state", "server-side-bulk-selection", "audit"], + "recoveryPolicies": ["partial-action-result", "retryable-vs-terminal-error", "stale-event-suppression", "detail-context-retention"], + "securityPolicies": ["screen-permission", "exception-action-permission", "server-enforcement"] + } + ] +} diff --git a/frontend/src/shared/ui/screen-types/tests/screenRecipeGovernance.spec.ts b/frontend/src/shared/ui/screen-types/tests/screenRecipeGovernance.spec.ts new file mode 100644 index 00000000..0b2e8d02 --- /dev/null +++ b/frontend/src/shared/ui/screen-types/tests/screenRecipeGovernance.spec.ts @@ -0,0 +1,11 @@ +import { execFileSync } from 'node:child_process' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +describe('KBX screen recipe governance', () => { + it('validates the normalized recipe contract against the real TypeScript recipe source', () => { + const validator = join(process.cwd(), '..', 'scripts', 'validate-kbx-screen-recipes.mjs') + const output = execFileSync(process.execPath, [validator, '--root', '.'], { cwd: process.cwd(), encoding: 'utf8' }) + expect(output).toContain('failures=0') + }) +}) diff --git a/scripts/validate-kbx-ai-components.mjs b/scripts/validate-kbx-ai-components.mjs new file mode 100644 index 00000000..0eaeb43f --- /dev/null +++ b/scripts/validate-kbx-ai-components.mjs @@ -0,0 +1,50 @@ +import fs from 'node:fs' +import path from 'node:path' + +const root = path.resolve(process.argv[process.argv.indexOf('--root') + 1] ?? 'frontend') +const featureRoot = path.join(root, 'src', 'features') +const indexPath = path.join(root, 'src', 'shared', 'ui', 'components', 'index.ts') +const failures = [] +const files = [] +function walk(directory) { + if (!fs.existsSync(directory)) return + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name) + if (entry.isDirectory()) walk(file) + else if (entry.name.endsWith('.vue')) files.push(file) + } +} +walk(featureRoot) +const exported = new Set([...fs.readFileSync(indexPath, 'utf8').matchAll(/export\s+\{\s*default\s+as\s+(Ks\w+)/g)].map(match => match[1])) +const commonAttributes = new Set(['class', 'style', 'id', 'title', 'role', 'tabindex', 'key', 'ref', 'aria-label', 'aria-describedby', 'aria-live', 'data-testid']) +const componentProps = new Map() +for (const name of exported) { + const componentPath = path.join(root, 'src', 'shared', 'ui', 'components', `${name}.vue`) + if (!fs.existsSync(componentPath)) continue + const source = fs.readFileSync(componentPath, 'utf8') + const propsBlock = source.match(/defineProps\s*<\s*\{([\s\S]*?)\}\s*>/)?.[1] ?? '' + componentProps.set(name, new Set([...propsBlock.matchAll(/([A-Za-z_$][\w$]*)\s*\??\s*:/g)].map(match => match[1]))) +} +for (const file of files) { + const source = fs.readFileSync(file, 'utf8') + for (const match of source.matchAll(/<\/(K(?:s|bx)\w+)|<(K(?:s|bx)\w+)(?=[\s>])/g)) { + const name = match[1] ?? match[2] + if (!exported.has(name)) failures.push(`${path.relative(process.cwd(), file).replaceAll('\\', '/')}: unknown KBX component ${name}`) + else { + const tagStart = match.index + match[0].length + const tagEnd = source.indexOf('>', tagStart) + const tag = source.slice(tagStart, tagEnd < 0 ? source.length : tagEnd) + const props = componentProps.get(name) ?? new Set() + for (const attr of tag.matchAll(/(?:^|\s)(?::|v-bind:)?([A-Za-z][\w-]*)(?=\s*=)/g)) { + const prop = attr[1] + if (commonAttributes.has(prop) || prop.startsWith('v-') || prop.startsWith('aria-') || prop.startsWith('data-') || ['if','else','else-if','for','show','model','on','slot'].includes(prop)) continue + const camel = prop.replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()) + if (!props.has(camel)) failures.push(`${path.relative(process.cwd(), file).replaceAll('\\', '/')}: unknown prop ${prop} on ${name}`) + } + } + } + if (/(?:primevue(?:\/|$)|ag-grid(?:-vue3)?(?:\/|$))/.test(source)) failures.push(`${path.relative(process.cwd(), file).replaceAll('\\', '/')}: vendor import in AI-scan scope`) +} +console.log(`KBX_AI_COMPONENTS files=${files.length} known=${exported.size} failures=${failures.length}`) +for (const failure of failures) console.log(`FAIL ${failure}`) +process.exitCode = failures.length ? 1 : 0 diff --git a/scripts/validate-kbx-component-manifest.mjs b/scripts/validate-kbx-component-manifest.mjs new file mode 100644 index 00000000..820c0b6b --- /dev/null +++ b/scripts/validate-kbx-component-manifest.mjs @@ -0,0 +1,28 @@ +import fs from 'node:fs' +import path from 'node:path' + +const root = path.resolve(process.argv[process.argv.indexOf('--root') + 1] ?? 'frontend') +const manifestPath = path.join(root, 'src', 'shared', 'ui', 'component-manifest.json') +const failures = [] +const allowedTiers = new Set(['L0', 'L1', 'L2', 'L3', 'L4']) +if (!fs.existsSync(manifestPath)) failures.push('missing component manifest') +else { + const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) + if (manifest.schemaVersion !== '1.0') failures.push('unsupported manifest schema') + if (!Array.isArray(manifest.components) || manifest.components.length < 6) failures.push('golden component coverage is incomplete') + const ids = new Set() + const names = new Set() + for (const item of manifest.components ?? []) { + if (ids.has(item.id)) failures.push(`duplicate component id ${item.id}`) + if (names.has(item.name)) failures.push(`duplicate component name ${item.name}`) + ids.add(item.id); names.add(item.name) + if (!allowedTiers.has(item.tier)) failures.push(`${item.name}: invalid tier`) + if (!item.owner || !item.vendorPolicy || !item.requiredContracts?.length) failures.push(`${item.name}: incomplete governance fields`) + const source = path.join(root, 'src', 'shared', 'ui', 'components', path.basename(item.source)) + if (!fs.existsSync(source)) failures.push(`${item.name}: missing source ${item.source}`) + if (item.name === 'KsDataGrid' && item.vendorPolicy !== 'strong-facade-no-raw-api') failures.push('KsDataGrid must be a strong facade') + } +} +console.log(`KBX_COMPONENT_MANIFEST failures=${failures.length}`) +for (const failure of failures) console.log(`FAIL ${failure}`) +process.exitCode = failures.length ? 1 : 0 diff --git a/scripts/validate-kbx-exceptions.mjs b/scripts/validate-kbx-exceptions.mjs new file mode 100644 index 00000000..224c9bd9 --- /dev/null +++ b/scripts/validate-kbx-exceptions.mjs @@ -0,0 +1,24 @@ +import fs from 'node:fs' +import path from 'node:path' + +const root = path.resolve(process.argv[process.argv.indexOf('--root') + 1] ?? 'frontend') +const registryPath = path.join(root, 'src', 'shared', 'ui', 'kbx-exception-registry.json') +const failures = [] +const today = new Date().toISOString().slice(0, 10) +if (!fs.existsSync(registryPath)) failures.push('missing exception registry') +else { + const registry = JSON.parse(fs.readFileSync(registryPath, 'utf8')) + const ids = new Set() + for (const item of registry.exceptions ?? []) { + if (ids.has(item.id)) failures.push(`duplicate exception ${item.id}`) + ids.add(item.id) + for (const field of ['id', 'screenId', 'type', 'reason', 'owner', 'introducedVersion', 'reviewAt', 'removalTarget', 'status']) { + if (!item[field]) failures.push(`${item.id ?? 'unknown'}: missing ${field}`) + } + if (item.reviewAt && item.reviewAt < today && item.status === 'active') failures.push(`${item.id}: reviewAt expired`) + if (!['active', 'removed', 'waived'].includes(item.status)) failures.push(`${item.id}: invalid status`) + } +} +console.log(`KBX_EXCEPTIONS failures=${failures.length}`) +for (const failure of failures) console.log(`FAIL ${failure}`) +process.exitCode = failures.length ? 1 : 0 diff --git a/scripts/validate-kbx-governance.mjs b/scripts/validate-kbx-governance.mjs new file mode 100644 index 00000000..689703ba --- /dev/null +++ b/scripts/validate-kbx-governance.mjs @@ -0,0 +1,27 @@ +import { execFileSync } from 'node:child_process' +import { fileURLToPath } from 'node:url' +import path from 'node:path' + +const root = path.resolve(process.argv[process.argv.indexOf('--root') + 1] ?? 'frontend') +const validators = [ + 'validate-ui-boundary.mjs', + 'validate-kbx-component-manifest.mjs', + 'validate-kbx-screen-recipes.mjs', + 'validate-kbx-ai-components.mjs', + 'validate-kbx-exceptions.mjs', +] +const repositoryRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') +const failures = [] +for (const validator of validators) { + const file = path.join(repositoryRoot, 'scripts', validator) + try { + const output = execFileSync(process.execPath, [file, '--root', root], { encoding: 'utf8' }) + process.stdout.write(`[PASS] ${validator}\n${output}`) + } catch (error) { + failures.push(validator) + process.stdout.write(`[FAIL] ${validator}\n${error.stdout ?? ''}${error.stderr ?? ''}`) + } +} +console.log(`KBX_GOVERNANCE validators=${validators.length} failures=${failures.length}`) +if (failures.length) console.log(`Failed validators: ${failures.join(', ')}`) +process.exitCode = failures.length ? 1 : 0 diff --git a/scripts/validate-kbx-screen-recipes.mjs b/scripts/validate-kbx-screen-recipes.mjs new file mode 100644 index 00000000..dcebeb88 --- /dev/null +++ b/scripts/validate-kbx-screen-recipes.mjs @@ -0,0 +1,26 @@ +import fs from 'node:fs' +import path from 'node:path' + +const root = path.resolve(process.argv[process.argv.indexOf('--root') + 1] ?? 'frontend') +const failures = [] +const contractPath = path.join(root, 'src', 'shared', 'ui', 'screen-types', 'screen-recipes.json') +const sourcePath = path.join(root, 'src', 'shared', 'ui', 'screen-types', 'screenRecipe.ts') +if (!fs.existsSync(contractPath) || !fs.existsSync(sourcePath)) failures.push('recipe contract/source missing') +else { + const contract = JSON.parse(fs.readFileSync(contractPath, 'utf8')) + const source = fs.readFileSync(sourcePath, 'utf8') + const ids = new Set() + for (const recipe of contract.recipes ?? []) { + if (ids.has(recipe.id)) failures.push(`duplicate recipe ${recipe.id}`) + ids.add(recipe.id) + for (const field of ['type', 'requiredPolicies', 'recoveryPolicies', 'securityPolicies']) { + if (!recipe[field] || (Array.isArray(recipe[field]) && recipe[field].length === 0)) failures.push(`${recipe.id}: missing ${field}`) + } + const variable = recipe.id === 'T01' ? 'searchListRecipe' : recipe.id === 'T12' ? 'workQueueRecipe' : null + if (!variable || !source.includes(`const ${variable}`)) failures.push(`${recipe.id}: source recipe is not represented`) + } + if (ids.size === 0) failures.push('no recipes registered') +} +console.log(`KBX_SCREEN_RECIPES failures=${failures.length}`) +for (const failure of failures) console.log(`FAIL ${failure}`) +process.exitCode = failures.length ? 1 : 0 diff --git a/scripts/validate-ui-boundary.mjs b/scripts/validate-ui-boundary.mjs new file mode 100644 index 00000000..c9467103 --- /dev/null +++ b/scripts/validate-ui-boundary.mjs @@ -0,0 +1,42 @@ +import fs from 'node:fs' +import path from 'node:path' + +const args = process.argv.slice(2) +const rootArg = args[args.indexOf('--root') + 1] ?? 'frontend' +const root = path.resolve(rootArg) +const featureRoot = path.join(root, 'src', 'features') +const files = [] +const failures = [] +const warnings = [] + +function walk(directory) { + if (!fs.existsSync(directory)) return + for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { + const file = path.join(directory, entry.name) + if (entry.isDirectory()) walk(file) + else if (/\.(ts|tsx|vue|css|scss)$/.test(entry.name)) files.push(file) + } +} + +function relative(file) { + return path.relative(process.cwd(), file).replaceAll('\\', '/') +} + +walk(featureRoot) +const vendorImport = /(?:from|import\s*\(|import\s+)['"](?:primevue(?:\/|$)|ag-grid(?:-vue3)?(?:\/|$))/ +const cssLeakage = /(?:\.p-[a-z0-9_-]+|\.ag-[a-z0-9_-]+|:deep\s*\([^)]*(?:\.p-|\.ag-)|!important)/i +const rawColor = /(?:#[0-9a-f]{3,8}\b|\brgba?\s*\(|\bhsl\s*\()/i + +for (const file of files) { + const source = fs.readFileSync(file, 'utf8') + const name = relative(file) + if (vendorImport.test(source)) failures.push(`${name}: direct PrimeVue/AG Grid import`) + if (cssLeakage.test(source)) failures.push(`${name}: supplier CSS leakage or !important`) + if (rawColor.test(source)) warnings.push(`${name}: raw color requires token-debt classification`) +} + +if (!fs.existsSync(featureRoot)) failures.push(`missing feature root: ${relative(featureRoot)}`) +console.log(`UI_BOUNDARY files=${files.length} failures=${failures.length} warnings=${warnings.length}`) +for (const warning of warnings) console.log(`WARN ${warning}`) +for (const failure of failures) console.log(`FAIL ${failure}`) +process.exitCode = failures.length ? 1 : 0