chore: fix prettier formatting, regenerate phase9 manifest, add missing config files (.env.example, .node-version, .nvmrc, CI workflow)
CI / test (push) Failing after 28s

This commit is contained in:
root
2026-06-25 20:12:15 -04:00
parent c43620a005
commit b7a640735c
36 changed files with 3402 additions and 1857 deletions
+10
View File
@@ -0,0 +1,10 @@
# Production-safe example environment (Phase 14 integration defaults).
# All demo and analytics integrations are blocked by default.
NEXT_PUBLIC_DEMO_ENABLED=false
DEMO_SUBMISSION_MODE=blocked
DEMO_REQUEST_TIMEOUT_MS=4000
DEMO_IDEMPOTENCY_TTL_SECONDS=900
DEMO_TEST_SCENARIOS_ENABLED=false
DEMO_LOCAL_TEST_MODE=false
NEXT_PUBLIC_ANALYTICS_MODE=disabled
SITE_ORIGIN=http://127.0.0.1:3000
+27
View File
@@ -0,0 +1,27 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 11.9.0
- uses: actions/setup-node@v4
with:
node-version: 24.17.0
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm test:integration
- run: pnpm test:security
- run: pnpm test:privacy
- run: pnpm test:browser
- run: pnpm test:a11y
- run: pnpm test:visual
+1
View File
@@ -0,0 +1 @@
24.17.0
+1
View File
@@ -0,0 +1 @@
24.17.0
@@ -38,7 +38,7 @@ The implementation must:
Breakpoints are layout decisions, not device labels. Breakpoints are layout decisions, not device labels.
| Mode | CSS range | Reference frames | Required behavior | | Mode | CSS range | Reference frames | Required behavior |
|---|---:|---|---| | ------- | ----------------: | ---------------- | ------------------------------------------------------------------------------------------------------- |
| Compact | 0767 px | 320, 390 | One-column sections; mobile drawer; stacked CTAs; product preview simplified; no horizontal page scroll | | Compact | 0767 px | 320, 390 | One-column sections; mobile drawer; stacked CTAs; product preview simplified; no horizontal page scroll |
| Medium | 7681119 px | 768, 1024 | Two-column where specified; mobile drawer remains; 4-column workflow allowed; balanced content widths | | Medium | 7681119 px | 768, 1024 | Two-column where specified; mobile drawer remains; 4-column workflow allowed; balanced content widths |
| Wide | 1120 px and above | 1280, 1440 | Desktop navigation and controls; 12-column composition; hero 7/5 split; max container 1280 px | | Wide | 1120 px and above | 1280, 1440 | Desktop navigation and controls; 12-column composition; hero 7/5 split; max container 1280 px |
+1 -1
View File
@@ -4,7 +4,7 @@
**Phases:** 1 — Design Foundation through 9 — Developer Handoff **Phases:** 1 — Design Foundation through 9 — Developer Handoff
| ID | Decision | Status | Rationale | Consequence | | ID | Decision | Status | Rationale | Consequence |
|---|---|---|---|---| | ----- | ---------------------------------------------------------------------------------------------------------- | -------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| D-001 | Use restrained operational blue as the main brand family | Approved | Supports trust, clarity, and product-led presentation | Blue carries navigation, links, selection, and routine primary actions | | D-001 | Use restrained operational blue as the main brand family | Approved | Supports trust, clarity, and product-led presentation | Blue carries navigation, links, selection, and routine primary actions |
| D-002 | Reserve orange for demo conversion and tightly related emphasis | Approved | Preserves conversion salience and prevents visual noise | Orange is not a routine navigation or generic action color | | D-002 | Reserve orange for demo conversion and tightly related emphasis | Approved | Preserves conversion salience and prevents visual noise | Orange is not a routine navigation or generic action color |
| D-003 | Use Inter Variable for English and French | Approved | Screen-oriented, broad Latin support, strong UI and marketing utility | One Latin family across website and product-facing UI | | D-003 | Use Inter Variable for English and French | Approved | Screen-oriented, broad Latin support, strong UI and marketing utility | One Latin family across website and product-facing UI |
+39 -39
View File
@@ -8,13 +8,13 @@
"files": [ "files": [
{ {
"path": "09_DEVELOPER_HANDOFF_v1.0.md", "path": "09_DEVELOPER_HANDOFF_v1.0.md",
"bytes": 8091, "bytes": 8378,
"sha256": "e82d486f1e566d1bf0263f1baa7dd8035ae62da5c36bb6bfa417e61331ec8d51" "sha256": "6cb5a76ee42e48caadd672e852520aeed0a974a9e4642be924c6b0b3176e8b1f"
}, },
{ {
"path": "DECISION_LOG_v9.0.md", "path": "DECISION_LOG_v9.0.md",
"bytes": 38775, "bytes": 63034,
"sha256": "85569a7418f8cad57d094ee9ec1d83b7727b5ab91a405cbda26de0271a8d76a1" "sha256": "8e00d2cfa72b2bad65d855827a0fdfd09cce3e30e9885e46d8f8c6c7feb5f09f"
}, },
{ {
"path": "DEFINITION_OF_DONE_v1.0.md", "path": "DEFINITION_OF_DONE_v1.0.md",
@@ -108,8 +108,8 @@
}, },
{ {
"path": "components/COMPONENT_CONTRACTS_v1.0.md", "path": "components/COMPONENT_CONTRACTS_v1.0.md",
"bytes": 9800, "bytes": 9698,
"sha256": "84b455fe725bff11b6ab54bb13373b1aa52991b062a36fad07db47a940187a53" "sha256": "c36fe8a5221d0f76f0b74894d7227135adb7bae45eca0a7554a31b9433218052"
}, },
{ {
"path": "components/COMPONENT_STATE_MATRIX_v1.0.csv", "path": "components/COMPONENT_STATE_MATRIX_v1.0.csv",
@@ -138,23 +138,23 @@
}, },
{ {
"path": "implementation/component-contracts_v1.0.ts", "path": "implementation/component-contracts_v1.0.ts",
"bytes": 2844, "bytes": 3041,
"sha256": "177dd2420b804290cf1296e43f41a7e759c8859b3253073fbdc523a81f6f94c0" "sha256": "45fb0222d99308b2f28014572316b13c2d7308bc16b1bd02f63b6e2257148399"
}, },
{ {
"path": "implementation/hreflang-generator_v1.0.ts", "path": "implementation/hreflang-generator_v1.0.ts",
"bytes": 751, "bytes": 799,
"sha256": "ab919a36a5d08a28c38ae51b4ddd0d84cef5d3ec3bed13649ebd26c3fb80ac8a" "sha256": "aa9b5e2eb602b2f651b1195a2d0ff7c2a0e6157bf702d3b27ab25b5b86249ba8"
}, },
{ {
"path": "implementation/localization-config_v1.0.ts", "path": "implementation/localization-config_v1.0.ts",
"bytes": 772, "bytes": 806,
"sha256": "bd8ca09f121bfe516ece817948a81d58136e9205fbd2c05dad2ce87f2d7a1782" "sha256": "fd884ce4cd807ccdd196bd4c09e34128b3a26df73e0f267c120f270179c852d0"
}, },
{ {
"path": "implementation/theme-config_v1.0.ts", "path": "implementation/theme-config_v1.0.ts",
"bytes": 725, "bytes": 752,
"sha256": "f56d37da4129d5830ba9ebd77b18cd6084a144e8514bfd8a8152387ecf76607c" "sha256": "5ae9687c6ffa832ae12b70fa8272a61853960eea8a3cc18e3a68ef1f120046f8"
}, },
{ {
"path": "interaction/ANALYTICS_EVENT_SPEC_v1.0.csv", "path": "interaction/ANALYTICS_EVENT_SPEC_v1.0.csv",
@@ -168,28 +168,28 @@
}, },
{ {
"path": "interaction/analytics-contract_v1.0.ts", "path": "interaction/analytics-contract_v1.0.ts",
"bytes": 832, "bytes": 918,
"sha256": "2716eb69cc98032005e068ab7bdc265a4c896a8e1a1bed682c7fdd4f45368123" "sha256": "d79b32503a5f8346816c647b8a7f45a62468b96f4fc9525d6d5147b5d2f2ec45"
}, },
{ {
"path": "interaction/demo-form-contract_v1.0.ts", "path": "interaction/demo-form-contract_v1.0.ts",
"bytes": 632, "bytes": 699,
"sha256": "194db77f7cfca417dbe35e6f2c2ea94cdd02806b83b1d0f1993fe509d8c10154" "sha256": "073cf70224f93312d177f77abd7987704205628209e30c44824ae45f929ee59e"
}, },
{ {
"path": "interaction/theme-bootstrap_v1.0.js", "path": "interaction/theme-bootstrap_v1.0.js",
"bytes": 753, "bytes": 757,
"sha256": "7b89a849bdcd72e1049714ebf76f0c0decb39e44261480f0cb08bb7dfe734e4a" "sha256": "a90b31cc2b91fd085b8cd4442b85fce56127501602b9b7f90af75fd3c8f4c3b0"
}, },
{ {
"path": "localization/LOCALIZATION_IMPLEMENTATION_v1.0.md", "path": "localization/LOCALIZATION_IMPLEMENTATION_v1.0.md",
"bytes": 2062, "bytes": 2069,
"sha256": "87a494d7ef7ed9f5012ed1699edf90e38995aa89a0efc3800db854e1d57e1024" "sha256": "63773e2f4777a6a322570d07d8badac8867dc4d6eef2d71bf05cbf81dfd2c85b"
}, },
{ {
"path": "localization/ROUTE_MANIFEST_v1.0.json", "path": "localization/ROUTE_MANIFEST_v1.0.json",
"bytes": 1573, "bytes": 1433,
"sha256": "fa29b8d31da1f36f87d216a1228fd1bbe1cde337636950b40b0024a10efaf49a" "sha256": "e08ac94ba1f3899bb02dfa934695f530d1089f9f03b0fadaa0feabd466ad8ec4"
}, },
{ {
"path": "localization/TRANSLATION_KEY_INVENTORY_v1.0.csv", "path": "localization/TRANSLATION_KEY_INVENTORY_v1.0.csv",
@@ -198,18 +198,18 @@
}, },
{ {
"path": "localization/locales/ar/homepage.json", "path": "localization/locales/ar/homepage.json",
"bytes": 18615, "bytes": 17419,
"sha256": "fe08aabd60203dbdebc718117f5e497b52a453a8a6fb671fe0bca28651bf0d47" "sha256": "19c1b9d0c90c017da1679b2cde4f123a98625ab6dae8002f58354839309f0474"
}, },
{ {
"path": "localization/locales/en/homepage.json", "path": "localization/locales/en/homepage.json",
"bytes": 13869, "bytes": 12769,
"sha256": "a5ed956c7f07fad016ca39b40b770c25e8beeae2799ff0fece82dbf087f18e51" "sha256": "ba88daa108e3df63285327d0f7d7be6c0df3d971d800d48617e1e62c3414acfb"
}, },
{ {
"path": "localization/locales/fr/homepage.json", "path": "localization/locales/fr/homepage.json",
"bytes": 15512, "bytes": 14564,
"sha256": "24079356bd47431ffeb66b7eb2981e108c03080205861b1bb42514b03b945a0a" "sha256": "5a3dfe1dc43c99a30cfc275309b5084b5d9f583a33ea136d388babb0714de719"
}, },
{ {
"path": "qa/ACCESSIBILITY_CHECKLIST_v1.0.csv", "path": "qa/ACCESSIBILITY_CHECKLIST_v1.0.csv",
@@ -243,18 +243,18 @@
}, },
{ {
"path": "qa/validate-phase9-package_v1.0.mjs", "path": "qa/validate-phase9-package_v1.0.mjs",
"bytes": 2534, "bytes": 2766,
"sha256": "631892318cf95c0530a605e5bf08def9b06c93af79b555205f3aeb8a2bcd87ed" "sha256": "8bd42770f659b51e63d73b282d7d101d6d817650f8c9b8875cad1b0eb3eece4e"
}, },
{ {
"path": "qa/visual-regression-scenarios_v1.0.json", "path": "qa/visual-regression-scenarios_v1.0.json",
"bytes": 1401, "bytes": 1209,
"sha256": "08a04f676cbb93e73b4c6bc112a55ebf287cb97be3dd3176ab80161e6de743c2" "sha256": "27d961c33c32775485475f7037b8d3bbd13f634b65fe2f59d6eea898860edf5f"
}, },
{ {
"path": "reference/FINDINGS_REPORT_v1.0.md", "path": "reference/FINDINGS_REPORT_v1.0.md",
"bytes": 3062, "bytes": 4135,
"sha256": "ba34afd535856d0eaa8ed7e174c23a7b5f0eae87344c8fcee5daafe00c068ce4" "sha256": "20eaba0c9fafd166220a64b2f0ede3c36b3bd9fc6f32982c1f6c7745f9311965"
}, },
{ {
"path": "reference/PHASE_8_HANDOFF_v1.0.md", "path": "reference/PHASE_8_HANDOFF_v1.0.md",
@@ -288,13 +288,13 @@
}, },
{ {
"path": "tokens/semantic-tokens_v1.0.css", "path": "tokens/semantic-tokens_v1.0.css",
"bytes": 3553, "bytes": 3589,
"sha256": "99912762e078bfc78b4878fc46b8c7d03d90c073e9c236942a58d83f6585a1b9" "sha256": "f28b2f03139a6aa89b85d00574f4fe56aa90d4ca1e03e80a1fb6eb8e5d9fa621"
}, },
{ {
"path": "tokens/tokens_v1.0.ts", "path": "tokens/tokens_v1.0.ts",
"bytes": 442, "bytes": 442,
"sha256": "ec265270251f8dd01d9166aeca3fcec9be2b5e534d825c9058fb6ed3f51e3224" "sha256": "3a1a2bb1bd29e55bf024a468a7c95bf2ec29d3cdc479f580050e8503c19c40a7"
}, },
{ {
"path": "validation/CONTENT_AND_STRUCTURE_VALIDATION_v1.0.json", "path": "validation/CONTENT_AND_STRUCTURE_VALIDATION_v1.0.json",
@@ -1,10 +1,7 @@
# Component Contracts # Component Contracts
Every component below is implemented once for all locales, directions, and themes. Props are semantic; components do not fetch translations internally unless the architecture establishes a single typed content boundary. Every component below is implemented once for all locales, directions, and themes. Props are semantic; components do not fetch translations internally unless the architecture establishes a single typed content boundary.
## SiteHeader ## SiteHeader
**Purpose:** Primary navigation, brand, locale/theme controls, login, demo CTA **Purpose:** Primary navigation, brand, locale/theme controls, login, demo CTA
@@ -23,7 +20,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## MobileDrawer ## MobileDrawer
**Purpose:** Compact/medium navigation overlay **Purpose:** Compact/medium navigation overlay
@@ -42,7 +38,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## LocaleSelector ## LocaleSelector
**Purpose:** Switch locale by stable route ID **Purpose:** Switch locale by stable route ID
@@ -61,7 +56,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## ThemeSelector ## ThemeSelector
**Purpose:** Set Light/Dark/System preference **Purpose:** Set Light/Dark/System preference
@@ -80,7 +74,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## HeroSection ## HeroSection
**Purpose:** Communicate category, promise, primary and secondary actions **Purpose:** Communicate category, promise, primary and secondary actions
@@ -99,7 +92,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## ProductPreview ## ProductPreview
**Purpose:** Illustrative/evidence-governed product visual **Purpose:** Illustrative/evidence-governed product visual
@@ -118,7 +110,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## LifecycleProof ## LifecycleProof
**Purpose:** Show operational coverage without customer logos **Purpose:** Show operational coverage without customer logos
@@ -137,7 +128,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## ComparisonSection ## ComparisonSection
**Purpose:** Fragmented versus connected workflow **Purpose:** Fragmented versus connected workflow
@@ -156,7 +146,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## WorkflowSteps ## WorkflowSteps
**Purpose:** Four-step rental flow and tour source **Purpose:** Four-step rental flow and tour source
@@ -175,7 +164,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## RoleCards ## RoleCards
**Purpose:** Explain value by operating role **Purpose:** Explain value by operating role
@@ -194,7 +182,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## ModuleGrid ## ModuleGrid
**Purpose:** Describe approved product modules **Purpose:** Describe approved product modules
@@ -213,7 +200,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## MeasurementPlan ## MeasurementPlan
**Purpose:** Show KPI definitions without invented outcomes **Purpose:** Show KPI definitions without invented outcomes
@@ -232,7 +218,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## IntegrationCategories ## IntegrationCategories
**Purpose:** Neutral category list **Purpose:** Neutral category list
@@ -251,7 +236,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## SecurityChecklist ## SecurityChecklist
**Purpose:** Evidence-request checklist **Purpose:** Evidence-request checklist
@@ -270,7 +254,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## PricingPreview ## PricingPreview
**Purpose:** Explain pricing factors and demo route **Purpose:** Explain pricing factors and demo route
@@ -289,7 +272,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## FAQAccordion ## FAQAccordion
**Purpose:** Localized questions and answers **Purpose:** Localized questions and answers
@@ -308,7 +290,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## FinalCTA ## FinalCTA
**Purpose:** Final conversion block and approved secondary contact **Purpose:** Final conversion block and approved secondary contact
@@ -327,7 +308,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## SiteFooter ## SiteFooter
**Purpose:** Localized navigation, legal, evidence status internal only **Purpose:** Localized navigation, legal, evidence status internal only
@@ -346,7 +326,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## DemoDialog ## DemoDialog
**Purpose:** Qualification form and submission states **Purpose:** Qualification form and submission states
@@ -365,7 +344,6 @@ Every component below is implemented once for all locales, directions, and theme
**Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant. **Content rule:** Block size is content-driven. Unsupported evidence suppresses the component or uses the explicitly approved neutral variant.
## ProductTourDialog ## ProductTourDialog
**Purpose:** Four-step guided overview **Purpose:** Four-step guided overview
@@ -1,26 +1,136 @@
export type Locale = "en" | "fr" | "ar"; export type Locale = 'en' | 'fr' | 'ar';
export type Direction = "ltr" | "rtl"; export type Direction = 'ltr' | 'rtl';
export type EvidenceStatus = "approved" | "illustrative" | "pending" | "expired" | "rejected"; export type EvidenceStatus = 'approved' | 'illustrative' | 'pending' | 'expired' | 'rejected';
export type ThemePreference = "light" | "dark" | "system"; export type ThemePreference = 'light' | 'dark' | 'system';
export interface LinkModel { id: string; label: string; href: string; external?: boolean; } export interface LinkModel {
export interface SiteHeaderProps { locale: Locale; direction: Direction; links: LinkModel[]; login?: LinkModel; onDemo(): void; } id: string;
export interface DrawerProps extends SiteHeaderProps { open: boolean; onOpenChange(open: boolean): void; returnFocusRef?: HTMLElement | null; } label: string;
export interface LocaleSelectorProps { locale: Locale; routeId: string; hash?: string; onLocaleChange(locale: Locale): void; } href: string;
export interface ThemeSelectorProps { preference: ThemePreference; onPreferenceChange(value: ThemePreference): void; } external?: boolean;
export interface HeroModel { eyebrow: string; title: string; body: string; primaryLabel: string; secondaryLabel: string; } }
export interface ProductPreviewModel { status: EvidenceStatus; caption: string; locale: Locale; direction: Direction; theme: "light"|"dark"; assetId?: string; } export interface SiteHeaderProps {
export interface LifecycleItem { id: string; title: string; description: string; icon: string; } locale: Locale;
export interface ComparisonModel { title: string; body: string; before: { title: string; items: string[] }; after: { title: string; items: string[] }; } direction: Direction;
export interface WorkflowStep { id: string; ordinal: string; title: string; description: string; } links: LinkModel[];
export interface RoleCard { id: string; title: string; description: string; } login?: LinkModel;
export interface ModuleCard { id: string; title: string; description: string; evidenceStatus: EvidenceStatus; } onDemo(): void;
export interface MetricDefinition { id: string; title: string; definition: string; publishedValue?: never; } }
export interface IntegrationCategory { id: string; label: string; verifiedPartners?: never; } export interface DrawerProps extends SiteHeaderProps {
export interface SecurityCheck { id: string; title: string; description: string; certificationBadge?: never; } open: boolean;
export interface PricingModel { mode: "quote-only" | "approved-pricing"; title: string; body: string; factors: string[]; note: string; } onOpenChange(open: boolean): void;
export interface FaqItem { id: string; question: string; answer: string; } returnFocusRef?: HTMLElement | null;
export interface DemoFormValues { fullName: string; workEmail: string; company: string; fleetSize: string; market?: string; preferredLanguage?: Locale; message?: string; } }
export type DemoFormState = "idle"|"validating"|"submitting"|"success"|"recoverable_error"|"terminal_error"; export interface LocaleSelectorProps {
export interface DemoFormProps { state: DemoFormState; values: DemoFormValues; fieldErrors: Partial<Record<keyof DemoFormValues,string>>; formError?: string; onSubmit(values: DemoFormValues): Promise<void>; } locale: Locale;
export interface TourProps { steps: WorkflowStep[]; activeIndex: number; open: boolean; onOpenChange(open:boolean): void; onStepChange(index:number): void; } routeId: string;
hash?: string;
onLocaleChange(locale: Locale): void;
}
export interface ThemeSelectorProps {
preference: ThemePreference;
onPreferenceChange(value: ThemePreference): void;
}
export interface HeroModel {
eyebrow: string;
title: string;
body: string;
primaryLabel: string;
secondaryLabel: string;
}
export interface ProductPreviewModel {
status: EvidenceStatus;
caption: string;
locale: Locale;
direction: Direction;
theme: 'light' | 'dark';
assetId?: string;
}
export interface LifecycleItem {
id: string;
title: string;
description: string;
icon: string;
}
export interface ComparisonModel {
title: string;
body: string;
before: { title: string; items: string[] };
after: { title: string; items: string[] };
}
export interface WorkflowStep {
id: string;
ordinal: string;
title: string;
description: string;
}
export interface RoleCard {
id: string;
title: string;
description: string;
}
export interface ModuleCard {
id: string;
title: string;
description: string;
evidenceStatus: EvidenceStatus;
}
export interface MetricDefinition {
id: string;
title: string;
definition: string;
publishedValue?: never;
}
export interface IntegrationCategory {
id: string;
label: string;
verifiedPartners?: never;
}
export interface SecurityCheck {
id: string;
title: string;
description: string;
certificationBadge?: never;
}
export interface PricingModel {
mode: 'quote-only' | 'approved-pricing';
title: string;
body: string;
factors: string[];
note: string;
}
export interface FaqItem {
id: string;
question: string;
answer: string;
}
export interface DemoFormValues {
fullName: string;
workEmail: string;
company: string;
fleetSize: string;
market?: string;
preferredLanguage?: Locale;
message?: string;
}
export type DemoFormState =
| 'idle'
| 'validating'
| 'submitting'
| 'success'
| 'recoverable_error'
| 'terminal_error';
export interface DemoFormProps {
state: DemoFormState;
values: DemoFormValues;
fieldErrors: Partial<Record<keyof DemoFormValues, string>>;
formError?: string;
onSubmit(values: DemoFormValues): Promise<void>;
}
export interface TourProps {
steps: WorkflowStep[];
activeIndex: number;
open: boolean;
onOpenChange(open: boolean): void;
onStepChange(index: number): void;
}
@@ -1,5 +1,5 @@
import manifest from "../localization/ROUTE_MANIFEST_v1.0.json"; import manifest from '../localization/ROUTE_MANIFEST_v1.0.json';
type Locale = "en"|"fr"|"ar"; type Locale = 'en' | 'fr' | 'ar';
export function localizedPath(routeId: string, locale: Locale): string { export function localizedPath(routeId: string, locale: Locale): string {
const route = manifest.routes.find((item) => item.id === routeId); const route = manifest.routes.find((item) => item.id === routeId);
if (!route) throw new Error(`Unknown route: ${routeId}`); if (!route) throw new Error(`Unknown route: ${routeId}`);
@@ -7,7 +7,15 @@ export function localizedPath(routeId:string, locale:Locale):string {
return slug ? `/${locale}/${slug}` : `/${locale}`; return slug ? `/${locale}/${slug}` : `/${locale}`;
} }
export function hreflangLinks(origin: string, routeId: string) { export function hreflangLinks(origin: string, routeId: string) {
const alternates = manifest.locales.map((locale) => ({ rel:"alternate", hreflang:locale, href:new URL(localizedPath(routeId, locale as Locale),origin).href })); const alternates = manifest.locales.map((locale) => ({
alternates.push({ rel:"alternate", hreflang:"x-default", href:new URL(localizedPath(routeId,"en"),origin).href }); rel: 'alternate',
hreflang: locale,
href: new URL(localizedPath(routeId, locale as Locale), origin).href,
}));
alternates.push({
rel: 'alternate',
hreflang: 'x-default',
href: new URL(localizedPath(routeId, 'en'), origin).href,
});
return alternates; return alternates;
} }
@@ -1,10 +1,20 @@
export const locales = ["en", "fr", "ar"] as const; export const locales = ['en', 'fr', 'ar'] as const;
export type Locale = typeof locales[number]; export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en"; export const defaultLocale: Locale = 'en';
export const directionByLocale: Record<Locale,"ltr"|"rtl"> = { en:"ltr", fr:"ltr", ar:"rtl" }; export const directionByLocale: Record<Locale, 'ltr' | 'rtl'> = { en: 'ltr', fr: 'ltr', ar: 'rtl' };
export const localeCookie = "hpc-locale"; export const localeCookie = 'hpc-locale';
export const localeStorageKey = "hpc.locale"; export const localeStorageKey = 'hpc.locale';
export const campaignQueryAllowlist = ["utm_source","utm_medium","utm_campaign","utm_content","gclid"] as const; export const campaignQueryAllowlist = [
export const rootDetectionOrder = ["cookie","accept-language","default-en"] as const; 'utm_source',
export function isLocale(value: unknown): value is Locale { return typeof value === "string" && (locales as readonly string[]).includes(value); } 'utm_medium',
export function isolateLtr(value: string): { dir:"ltr"; value:string } { return { dir:"ltr", value }; } 'utm_campaign',
'utm_content',
'gclid',
] as const;
export const rootDetectionOrder = ['cookie', 'accept-language', 'default-en'] as const;
export function isLocale(value: unknown): value is Locale {
return typeof value === 'string' && (locales as readonly string[]).includes(value);
}
export function isolateLtr(value: string): { dir: 'ltr'; value: string } {
return { dir: 'ltr', value };
}
@@ -1,8 +1,17 @@
export const themeCookie = "hpc-theme"; export const themeCookie = 'hpc-theme';
export const themeStorageKey = "hpc.theme.preference"; export const themeStorageKey = 'hpc.theme.preference';
export const themePreferences = ["light","dark","system"] as const; export const themePreferences = ['light', 'dark', 'system'] as const;
export type ThemePreference = typeof themePreferences[number]; export type ThemePreference = (typeof themePreferences)[number];
export type ResolvedTheme = "light"|"dark"; export type ResolvedTheme = 'light' | 'dark';
export function isThemePreference(v:unknown): v is ThemePreference { return typeof v === "string" && (themePreferences as readonly string[]).includes(v); } export function isThemePreference(v: unknown): v is ThemePreference {
export function resolveTheme(preference:ThemePreference, systemDark:boolean):ResolvedTheme { return preference === "system" ? (systemDark ? "dark" : "light") : preference; } return typeof v === 'string' && (themePreferences as readonly string[]).includes(v);
export const themeCookieAttributes = { path:"/", sameSite:"lax" as const, maxAgeSeconds:31536000, secureInProduction:true }; }
export function resolveTheme(preference: ThemePreference, systemDark: boolean): ResolvedTheme {
return preference === 'system' ? (systemDark ? 'dark' : 'light') : preference;
}
export const themeCookieAttributes = {
path: '/',
sameSite: 'lax' as const,
maxAgeSeconds: 31536000,
secureInProduction: true,
};
@@ -1,9 +1,41 @@
export type AnalyticsEventName = export type AnalyticsEventName =
| "page_view" | "section_view" | "navigation_click" | 'page_view'
| "locale_change" | "theme_change" | "mobile_nav_open" | "mobile_nav_close" | 'section_view'
| "product_tour_open" | "product_tour_step" | "product_tour_complete" | "product_tour_close" | 'navigation_click'
| "demo_open" | "demo_submit_attempt" | "demo_validation_error" | "demo_submit_success" | "demo_submit_failure" | "demo_close" | 'locale_change'
| "faq_open"; | 'theme_change'
export interface AnalyticsContext { routeId:string; locale:"en"|"fr"|"ar"; direction:"ltr"|"rtl"; resolvedTheme:"light"|"dark"; source?:string; step?:number; errorCode?:string; } | 'mobile_nav_open'
export interface AnalyticsAdapter { consentGranted():boolean; track(name:AnalyticsEventName, context:AnalyticsContext):void; } | 'mobile_nav_close'
export const forbiddenPayloadKeys = ["name","email","company","message","phone","fieldValue","serverResponse"] as const; | 'product_tour_open'
| 'product_tour_step'
| 'product_tour_complete'
| 'product_tour_close'
| 'demo_open'
| 'demo_submit_attempt'
| 'demo_validation_error'
| 'demo_submit_success'
| 'demo_submit_failure'
| 'demo_close'
| 'faq_open';
export interface AnalyticsContext {
routeId: string;
locale: 'en' | 'fr' | 'ar';
direction: 'ltr' | 'rtl';
resolvedTheme: 'light' | 'dark';
source?: string;
step?: number;
errorCode?: string;
}
export interface AnalyticsAdapter {
consentGranted(): boolean;
track(name: AnalyticsEventName, context: AnalyticsContext): void;
}
export const forbiddenPayloadKeys = [
'name',
'email',
'company',
'message',
'phone',
'fieldValue',
'serverResponse',
] as const;
@@ -1,5 +1,23 @@
export const fleetSizeValues = ["1-9","10-24","25-49","50-99","100-249","250+"] as const; export const fleetSizeValues = ['1-9', '10-24', '25-49', '50-99', '100-249', '250+'] as const;
export type FleetSize = typeof fleetSizeValues[number]; export type FleetSize = (typeof fleetSizeValues)[number];
export interface DemoRequest { fullName:string; workEmail:string; company:string; fleetSize:FleetSize; market?:string; preferredLanguage?:"en"|"fr"|"ar"; message?:string; idempotencyKey:string; source:string; } export interface DemoRequest {
export interface DemoSuccess { requestId:string; acceptedAt:string; nextStepCode:"sales_followup"|"manual_review"; } fullName: string;
export interface DemoFailure { code:"validation"|"rate_limited"|"duplicate"|"unavailable"|"unknown"; retryable:boolean; fieldErrors?:Record<string,string>; } workEmail: string;
company: string;
fleetSize: FleetSize;
market?: string;
preferredLanguage?: 'en' | 'fr' | 'ar';
message?: string;
idempotencyKey: string;
source: string;
}
export interface DemoSuccess {
requestId: string;
acceptedAt: string;
nextStepCode: 'sales_followup' | 'manual_review';
}
export interface DemoFailure {
code: 'validation' | 'rate_limited' | 'duplicate' | 'unavailable' | 'unknown';
retryable: boolean;
fieldErrors?: Record<string, string>;
}
@@ -3,10 +3,12 @@
var cookieMatch = document.cookie.match(/(?:^|; )hpc-theme=([^;]+)/); var cookieMatch = document.cookie.match(/(?:^|; )hpc-theme=([^;]+)/);
var cookieValue = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null; var cookieValue = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
var stored = null; var stored = null;
try { stored = localStorage.getItem("hpc.theme.preference"); } catch (_) {} try {
var preference = allowed[cookieValue] ? cookieValue : (allowed[stored] ? stored : "system"); stored = localStorage.getItem('hpc.theme.preference');
var dark = window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches; } catch (_) {}
var resolved = preference === "system" ? (dark ? "dark" : "light") : preference; var preference = allowed[cookieValue] ? cookieValue : allowed[stored] ? stored : 'system';
var dark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
var resolved = preference === 'system' ? (dark ? 'dark' : 'light') : preference;
var root = document.documentElement; var root = document.documentElement;
root.dataset.themePreference = preference; root.dataset.themePreference = preference;
root.dataset.theme = resolved; root.dataset.theme = resolved;
@@ -9,7 +9,7 @@ English is the authoring source. French and Arabic are independent production re
Set document attributes on the server: Set document attributes on the server:
```html ```html
<html lang="ar" dir="rtl" data-theme-preference="system" data-theme="dark"> <html lang="ar" dir="rtl" data-theme-preference="system" data-theme="dark"></html>
``` ```
Do not wait for hydration to set language, direction, or resolved theme. Do not wait for hydration to set language, direction, or resolved theme.
@@ -1,11 +1,7 @@
{ {
"version": "1.0", "version": "1.0",
"defaultLocale": "en", "defaultLocale": "en",
"locales": [ "locales": ["en", "fr", "ar"],
"en",
"fr",
"ar"
],
"rootDetection": true, "rootDetection": true,
"routes": [ "routes": [
{ {
@@ -16,15 +12,7 @@
"ar": "" "ar": ""
}, },
"indexable": true, "indexable": true,
"hashes": [ "hashes": ["top", "product", "workflow", "modules", "pricing", "faq", "contact"]
"top",
"product",
"workflow",
"modules",
"pricing",
"faq",
"contact"
]
}, },
{ {
"id": "login", "id": "login",
@@ -68,22 +56,12 @@
} }
], ],
"switching": { "switching": {
"preserveQueryAllowlist": [ "preserveQueryAllowlist": ["utm_source", "utm_medium", "utm_campaign", "utm_content", "gclid"],
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"gclid"
],
"preserveValidHash": true, "preserveValidHash": true,
"dropUnknownQuery": true "dropUnknownQuery": true
}, },
"hreflang": { "hreflang": {
"values": [ "values": ["en", "fr", "ar"],
"en",
"fr",
"ar"
],
"xDefaultLocale": "en", "xDefaultLocale": "en",
"selfCanonical": true "selfCanonical": true
} }
@@ -7,26 +7,11 @@
}, },
"header": { "header": {
"nav": [ "nav": [
[ ["product", "المنتج"],
"product", ["workflow", "سير العمل"],
"المنتج" ["modules", "الوحدات"],
], ["pricing", "التسعير"],
[ ["faq", "الأسئلة الشائعة"]
"workflow",
"سير العمل"
],
[
"modules",
"الوحدات"
],
[
"pricing",
"التسعير"
],
[
"faq",
"الأسئلة الشائعة"
]
], ],
"login": "تسجيل الدخول", "login": "تسجيل الدخول",
"demo": "احجز عرضًا توضيحيًا", "demo": "احجز عرضًا توضيحيًا",
@@ -50,22 +35,10 @@
"title": "سير عمل واحد يغطي دورة التأجير كاملة.", "title": "سير عمل واحد يغطي دورة التأجير كاملة.",
"body": "حافظ على ترابط سياق العميل والمركبة والعقد والمهام مع تقدم عملية التأجير.", "body": "حافظ على ترابط سياق العميل والمركبة والعقد والمهام مع تقدم عملية التأجير.",
"items": [ "items": [
[ ["الحجز", "سجّل الطلب والبيانات المطلوبة."],
"الحجز", ["المركبة", "تابع التوافر والجاهزية."],
"سجّل الطلب والبيانات المطلوبة." ["العقد", "اجمع مهام التسليم والسجلات في موضع واحد."],
], ["الإرجاع", "سجّل الحالة وأغلق أعمال المتابعة."]
[
"المركبة",
"تابع التوافر والجاهزية."
],
[
"العقد",
"اجمع مهام التسليم والسجلات في موضع واحد."
],
[
"الإرجاع",
"سجّل الحالة وأغلق أعمال المتابعة."
]
] ]
}, },
"comparison": { "comparison": {
@@ -92,48 +65,20 @@
"title": "حرّك عملية التأجير إلى الأمام من دون فقدان السياق.", "title": "حرّك عملية التأجير إلى الأمام من دون فقدان السياق.",
"body": "توضح كل خطوة القرار والمسؤول والإجراء التالي بدل دفن التشغيل داخل قائمة عامة من الميزات.", "body": "توضح كل خطوة القرار والمسؤول والإجراء التالي بدل دفن التشغيل داخل قائمة عامة من الميزات.",
"steps": [ "steps": [
[ ["01", "تسجيل الحجز", "سجّل التواريخ وبيانات السائق والمتطلبات والمصدر."],
"01", ["02", "تخصيص مركبة جاهزة", "طابق التوافر مع الفئة والموقع وحالة الجاهزية."],
"تسجيل الحجز", ["03", "إكمال التسليم", "تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."],
"سجّل التواريخ وبيانات السائق والمتطلبات والمصدر." ["04", "معالجة الإرجاع", "سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."]
],
[
"02",
"تخصيص مركبة جاهزة",
"طابق التوافر مع الفئة والموقع وحالة الجاهزية."
],
[
"03",
"إكمال التسليم",
"تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."
],
[
"04",
"معالجة الإرجاع",
"سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."
]
] ]
}, },
"roles": { "roles": {
"eyebrow": "مصمم لفريق التشغيل", "eyebrow": "مصمم لفريق التشغيل",
"title": "امنح كل دور السياق الذي يحتاجه من دون إنشاء نسخ متعددة من الحقيقة.", "title": "امنح كل دور السياق الذي يحتاجه من دون إنشاء نسخ متعددة من الحقيقة.",
"roles": [ "roles": [
[ ["المالك أو المدير", "اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط."],
"المالك أو المدير", ["فريق الحجوزات", "حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."],
"اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط." ["الأسطول والتشغيل", "اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."],
], ["مكتب الاستقبال", "حضّر مهام التسليم والإرجاع قبل انتظار العميل."]
[
"فريق الحجوزات",
"حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."
],
[
"الأسطول والتشغيل",
"اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."
],
[
"مكتب الاستقبال",
"حضّر مهام التسليم والإرجاع قبل انتظار العميل."
]
] ]
}, },
"modules": { "modules": {
@@ -141,30 +86,12 @@
"title": "نواة مترابطة منظّمة حول أعمال التأجير الفعلية.", "title": "نواة مترابطة منظّمة حول أعمال التأجير الفعلية.",
"body": "توضح حدود الوحدات المسؤوليات، بينما تمنع السجلات المشتركة فوضى النسخ واللصق المعتادة.", "body": "توضح حدود الوحدات المسؤوليات، بينما تمنع السجلات المشتركة فوضى النسخ واللصق المعتادة.",
"items": [ "items": [
[ ["الحجوزات", "التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد."],
"الحجوزات", ["جاهزية الأسطول", "توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."],
"التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد." ["عقود الإيجار", "متطلبات التسليم وحالة العقد والسجلات التشغيلية."],
], ["فحص المركبة", "فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."],
[ ["سجلات العملاء", "سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."],
"جاهزية الأسطول", ["التقارير التشغيلية", "تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."]
"توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."
],
[
"عقود الإيجار",
"متطلبات التسليم وحالة العقد والسجلات التشغيلية."
],
[
"فحص المركبة",
"فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."
],
[
"سجلات العملاء",
"سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."
],
[
"التقارير التشغيلية",
"تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."
]
] ]
}, },
"results": { "results": {
@@ -172,22 +99,10 @@
"title": "قِس التغيير، لا الخطاب التسويقي.", "title": "قِس التغيير، لا الخطاب التسويقي.",
"body": "لا ينبغي نشر أي نتيجة قبل اعتماد خط أساس حقيقي لعميل وفترة مقارنة وتعريف واضح للبيانات. لذلك تعرض هذه النسخة منهجية القياس بدل نسب مئوية مختلقة.", "body": "لا ينبغي نشر أي نتيجة قبل اعتماد خط أساس حقيقي لعميل وفترة مقارنة وتعريف واضح للبيانات. لذلك تعرض هذه النسخة منهجية القياس بدل نسب مئوية مختلقة.",
"metrics": [ "metrics": [
[ ["زمن معالجة الحجز", "من استلام طلب صالح للعمل حتى تأكيد الحجز."],
"زمن معالجة الحجز", ["زمن إعادة المركبة إلى الجاهزية", "من تسجيل الإرجاع حتى حالة جاهز للتأجير."],
"من استلام طلب صالح للعمل حتى تأكيد الحجز." ["معدل استخدام الأسطول", "مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."],
], ["متابعة حالات التأخير", "عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."]
[
"زمن إعادة المركبة إلى الجاهزية",
"من تسجيل الإرجاع حتى حالة جاهز للتأجير."
],
[
"معدل استخدام الأسطول",
"مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."
],
[
"متابعة حالات التأخير",
"عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."
]
], ],
"methodTitle": "المنهجية المطلوبة قبل نشر أي نتيجة", "methodTitle": "المنهجية المطلوبة قبل نشر أي نتيجة",
"method": [ "method": [
@@ -215,22 +130,10 @@
"title": "قيّم المنصة بالأدلة، لا بالشعارات.", "title": "قيّم المنصة بالأدلة، لا بالشعارات.",
"body": "لا تُنشر شهادات أو وعود بمستوى الخدمة قبل وجود وثائق معتمدة. أثناء التقييم، اطلب إجابات محددة حول الضوابط المهمة لنشاطك.", "body": "لا تُنشر شهادات أو وعود بمستوى الخدمة قبل وجود وثائق معتمدة. أثناء التقييم، اطلب إجابات محددة حول الضوابط المهمة لنشاطك.",
"checks": [ "checks": [
[ ["التحكم في الوصول", "الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم."],
"التحكم في الوصول", ["قابلية التدقيق", "التغييرات المسجلة والقابلة للبحث والتصدير."],
"الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم." ["معالجة البيانات", "الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."],
], ["استمرارية العمل", "النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."]
[
"قابلية التدقيق",
"التغييرات المسجلة والقابلة للبحث والتصدير."
],
[
"معالجة البيانات",
"الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."
],
[
"استمرارية العمل",
"النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."
]
] ]
}, },
"pricing": { "pricing": {
@@ -289,65 +192,32 @@
[ [
"المنتج", "المنتج",
[ [
[ ["سير العمل", "#workflow"],
"سير العمل", ["الوحدات", "#modules"],
"#workflow" ["التسعير", "#pricing"]
],
[
"الوحدات",
"#modules"
],
[
"التسعير",
"#pricing"
]
] ]
], ],
[ [
"الشركة", "الشركة",
[ [
[ ["تواصل مع المبيعات", "#contact"],
"تواصل مع المبيعات", ["احجز عرضًا توضيحيًا", "#demo"]
"#contact"
],
[
"احجز عرضًا توضيحيًا",
"#demo"
]
] ]
], ],
[ [
"الحساب", "الحساب",
[ [
[ ["تسجيل الدخول", "#login"],
"تسجيل الدخول", ["اللغة", "#language"],
"#login" ["المظهر", "#theme"]
],
[
"اللغة",
"#language"
],
[
"المظهر",
"#theme"
]
] ]
], ],
[ [
"قانوني", "قانوني",
[ [
[ ["الخصوصية", "#!"],
"الخصوصية", ["الشروط", "#!"],
"#!" ["إمكانية الوصول", "#!"]
],
[
"الشروط",
"#!"
],
[
"إمكانية الوصول",
"#!"
]
] ]
] ]
], ],
@@ -377,11 +247,7 @@
"أكثر من 500 مركبة", "أكثر من 500 مركبة",
"غير متأكد بعد" "غير متأكد بعد"
], ],
"langOptions": [ "langOptions": ["English", "Français", "العربية"],
"English",
"Français",
"العربية"
],
"successTitle": "تم تسجيل طلب العرض", "successTitle": "تم تسجيل طلب العرض",
"successBody": "اكتمل مسار الحجز في هذا النموذج الأولي. لم يُرسل أي طلب إلى نظام مبيعات فعلي.", "successBody": "اكتمل مسار الحجز في هذا النموذج الأولي. لم يُرسل أي طلب إلى نظام مبيعات فعلي.",
"submitAnother": "إرسال طلب آخر", "submitAnother": "إرسال طلب آخر",
@@ -7,26 +7,11 @@
}, },
"header": { "header": {
"nav": [ "nav": [
[ ["product", "Product"],
"product", ["workflow", "Workflow"],
"Product" ["modules", "Modules"],
], ["pricing", "Pricing"],
[ ["faq", "FAQ"]
"workflow",
"Workflow"
],
[
"modules",
"Modules"
],
[
"pricing",
"Pricing"
],
[
"faq",
"FAQ"
]
], ],
"login": "Log in", "login": "Log in",
"demo": "Book a demo", "demo": "Book a demo",
@@ -50,22 +35,10 @@
"title": "One workflow across the rental lifecycle.", "title": "One workflow across the rental lifecycle.",
"body": "Keep the customer, vehicle, agreement, and task context connected as the rental moves forward.", "body": "Keep the customer, vehicle, agreement, and task context connected as the rental moves forward.",
"items": [ "items": [
[ ["Reservation", "Capture the request and required details."],
"Reservation", ["Vehicle", "Track availability and readiness."],
"Capture the request and required details." ["Agreement", "Keep pickup tasks and records together."],
], ["Return", "Record condition and close follow-up work."]
[
"Vehicle",
"Track availability and readiness."
],
[
"Agreement",
"Keep pickup tasks and records together."
],
[
"Return",
"Record condition and close follow-up work."
]
] ]
}, },
"comparison": { "comparison": {
@@ -92,48 +65,23 @@
"title": "Move the rental forward without losing the thread.", "title": "Move the rental forward without losing the thread.",
"body": "Each step exposes the decision, owner, and next action instead of burying operations inside a generic feature list.", "body": "Each step exposes the decision, owner, and next action instead of burying operations inside a generic feature list.",
"steps": [ "steps": [
[ ["01", "Capture the reservation", "Record dates, driver details, requirements, and source."],
"01", ["02", "Assign a ready vehicle", "Match availability with class, location, and readiness."],
"Capture the reservation", ["03", "Complete pickup", "Confirm documents, inspection, agreement, and handover tasks."],
"Record dates, driver details, requirements, and source." ["04", "Process the return", "Record condition, close open work, and release the vehicle."]
],
[
"02",
"Assign a ready vehicle",
"Match availability with class, location, and readiness."
],
[
"03",
"Complete pickup",
"Confirm documents, inspection, agreement, and handover tasks."
],
[
"04",
"Process the return",
"Record condition, close open work, and release the vehicle."
]
] ]
}, },
"roles": { "roles": {
"eyebrow": "Built for the operating team", "eyebrow": "Built for the operating team",
"title": "Give each role the context it needs, without creating separate versions of the truth.", "title": "Give each role the context it needs, without creating separate versions of the truth.",
"roles": [ "roles": [
[ ["Owner or manager", "See workload, exceptions, and operating signals across the business."],
"Owner or manager", ["Reservations team", "Move requests from inquiry to confirmed rental with fewer handoffs."],
"See workload, exceptions, and operating signals across the business."
],
[
"Reservations team",
"Move requests from inquiry to confirmed rental with fewer handoffs."
],
[ [
"Fleet and operations", "Fleet and operations",
"Know which vehicles are available, blocked, due, or awaiting action." "Know which vehicles are available, blocked, due, or awaiting action."
], ],
[ ["Front desk", "Prepare pickup and return tasks before the customer is waiting."]
"Front desk",
"Prepare pickup and return tasks before the customer is waiting."
]
] ]
}, },
"modules": { "modules": {
@@ -141,26 +89,14 @@
"title": "A connected core, organized around real rental work.", "title": "A connected core, organized around real rental work.",
"body": "Module boundaries clarify responsibility while shared records prevent the usual copy-and-paste archaeology.", "body": "Module boundaries clarify responsibility while shared records prevent the usual copy-and-paste archaeology.",
"items": [ "items": [
[ ["Reservations", "Availability, dates, customer requirements, and status in one flow."],
"Reservations", ["Fleet readiness", "Vehicle availability, blocks, service state, and location context."],
"Availability, dates, customer requirements, and status in one flow." ["Rental agreements", "Pickup requirements, agreement status, and operational records."],
],
[
"Fleet readiness",
"Vehicle availability, blocks, service state, and location context."
],
[
"Rental agreements",
"Pickup requirements, agreement status, and operational records."
],
[ [
"Vehicle inspections", "Vehicle inspections",
"Structured pickup and return condition checks with follow-up tasks." "Structured pickup and return condition checks with follow-up tasks."
], ],
[ ["Customer records", "Rental history and contact context attached to active work."],
"Customer records",
"Rental history and contact context attached to active work."
],
[ [
"Operations reporting", "Operations reporting",
"Definitions and operational views that can be validated against source activity." "Definitions and operational views that can be validated against source activity."
@@ -172,22 +108,13 @@
"title": "Measure the change, not the marketing.", "title": "Measure the change, not the marketing.",
"body": "No outcome claim should appear until a real customer baseline, comparison period, and data definition are approved. The production page therefore shows the measurement plan instead of fictional percentages.", "body": "No outcome claim should appear until a real customer baseline, comparison period, and data definition are approved. The production page therefore shows the measurement plan instead of fictional percentages.",
"metrics": [ "metrics": [
[ ["Reservation handling time", "From usable request to confirmed reservation."],
"Reservation handling time", ["Vehicle turnaround time", "From return recorded to ready-for-rental status."],
"From usable request to confirmed reservation."
],
[
"Vehicle turnaround time",
"From return recorded to ready-for-rental status."
],
[ [
"Fleet utilization", "Fleet utilization",
"Available rental days compared with rented days, using one approved definition." "Available rental days compared with rented days, using one approved definition."
], ],
[ ["Overdue follow-up", "Open overdue rentals with an assigned owner and next action."]
"Overdue follow-up",
"Open overdue rentals with an assigned owner and next action."
]
], ],
"methodTitle": "Methodology required before publishing results", "methodTitle": "Methodology required before publishing results",
"method": [ "method": [
@@ -215,22 +142,10 @@
"title": "Evaluate the platform with evidence, not badges.", "title": "Evaluate the platform with evidence, not badges.",
"body": "Certifications and service-level claims remain unpublished until approved documentation exists. During evaluation, request concrete answers for the controls that matter to your operation.", "body": "Certifications and service-level claims remain unpublished until approved documentation exists. During evaluation, request concrete answers for the controls that matter to your operation.",
"checks": [ "checks": [
[ ["Access control", "Roles, permissions, administrator actions, and offboarding."],
"Access control", ["Auditability", "Which changes are recorded, searchable, and exportable."],
"Roles, permissions, administrator actions, and offboarding." ["Data handling", "Ownership, retention, export, deletion, and subprocessors."],
], ["Continuity", "Backup, recovery, incident communication, and support process."]
[
"Auditability",
"Which changes are recorded, searchable, and exportable."
],
[
"Data handling",
"Ownership, retention, export, deletion, and subprocessors."
],
[
"Continuity",
"Backup, recovery, incident communication, and support process."
]
] ]
}, },
"pricing": { "pricing": {
@@ -289,65 +204,32 @@
[ [
"Product", "Product",
[ [
[ ["Workflow", "#workflow"],
"Workflow", ["Modules", "#modules"],
"#workflow" ["Pricing", "#pricing"]
],
[
"Modules",
"#modules"
],
[
"Pricing",
"#pricing"
]
] ]
], ],
[ [
"Company", "Company",
[ [
[ ["Contact sales", "#contact"],
"Contact sales", ["Book a demo", "#demo"]
"#contact"
],
[
"Book a demo",
"#demo"
]
] ]
], ],
[ [
"Account", "Account",
[ [
[ ["Log in", "#login"],
"Log in", ["Language", "#language"],
"#login" ["Theme", "#theme"]
],
[
"Language",
"#language"
],
[
"Theme",
"#theme"
]
] ]
], ],
[ [
"Legal", "Legal",
[ [
[ ["Privacy", "#!"],
"Privacy", ["Terms", "#!"],
"#!" ["Accessibility", "#!"]
],
[
"Terms",
"#!"
],
[
"Accessibility",
"#!"
]
] ]
] ]
], ],
@@ -377,11 +259,7 @@
"More than 500 vehicles", "More than 500 vehicles",
"Not sure yet" "Not sure yet"
], ],
"langOptions": [ "langOptions": ["English", "Français", "العربية"],
"English",
"Français",
"العربية"
],
"successTitle": "Demo request recorded", "successTitle": "Demo request recorded",
"successBody": "This prototype has completed the booking flow. No request was sent to a real sales system.", "successBody": "This prototype has completed the booking flow. No request was sent to a real sales system.",
"submitAnother": "Submit another request", "submitAnother": "Submit another request",
@@ -7,26 +7,11 @@
}, },
"header": { "header": {
"nav": [ "nav": [
[ ["product", "Produit"],
"product", ["workflow", "Parcours"],
"Produit" ["modules", "Modules"],
], ["pricing", "Tarifs"],
[ ["faq", "FAQ"]
"workflow",
"Parcours"
],
[
"modules",
"Modules"
],
[
"pricing",
"Tarifs"
],
[
"faq",
"FAQ"
]
], ],
"login": "Se connecter", "login": "Se connecter",
"demo": "Réserver une démo", "demo": "Réserver une démo",
@@ -50,22 +35,10 @@
"title": "Un même parcours pour tout le cycle de location.", "title": "Un même parcours pour tout le cycle de location.",
"body": "Conservez le contexte du client, du véhicule, du contrat et des tâches au fil de la location.", "body": "Conservez le contexte du client, du véhicule, du contrat et des tâches au fil de la location.",
"items": [ "items": [
[ ["Réservation", "Saisir la demande et les informations nécessaires."],
"Réservation", ["Véhicule", "Suivre la disponibilité et l’état de préparation."],
"Saisir la demande et les informations nécessaires." ["Contrat", "Regrouper les tâches de départ et les documents."],
], ["Retour", "Enregistrer l’état et clôturer les actions de suivi."]
[
"Véhicule",
"Suivre la disponibilité et l’état de préparation."
],
[
"Contrat",
"Regrouper les tâches de départ et les documents."
],
[
"Retour",
"Enregistrer l’état et clôturer les actions de suivi."
]
] ]
}, },
"comparison": { "comparison": {
@@ -118,10 +91,7 @@
"eyebrow": "Conçu pour l’équipe opérationnelle", "eyebrow": "Conçu pour l’équipe opérationnelle",
"title": "Donnez à chaque rôle le contexte utile, sans multiplier les versions de la vérité.", "title": "Donnez à chaque rôle le contexte utile, sans multiplier les versions de la vérité.",
"roles": [ "roles": [
[ ["Direction ou responsable", "Voir la charge, les exceptions et les signaux dexploitation."],
"Direction ou responsable",
"Voir la charge, les exceptions et les signaux dexploitation."
],
[ [
"Équipe réservations", "Équipe réservations",
"Faire passer les demandes vers une location confirmée avec moins de transmissions." "Faire passer les demandes vers une location confirmée avec moins de transmissions."
@@ -130,10 +100,7 @@
"Parc et opérations", "Parc et opérations",
"Savoir quels véhicules sont disponibles, bloqués, attendus ou en attente daction." "Savoir quels véhicules sont disponibles, bloqués, attendus ou en attente daction."
], ],
[ ["Comptoir", "Préparer les tâches de départ et de retour avant que le client nattende."]
"Comptoir",
"Préparer les tâches de départ et de retour avant que le client nattende."
]
] ]
}, },
"modules": { "modules": {
@@ -141,30 +108,15 @@
"title": "Un socle connecté, organisé autour du travail réel de location.", "title": "Un socle connecté, organisé autour du travail réel de location.",
"body": "Les modules clarifient les responsabilités, tandis que les dossiers partagés évitent larchéologie du copier-coller.", "body": "Les modules clarifient les responsabilités, tandis que les dossiers partagés évitent larchéologie du copier-coller.",
"items": [ "items": [
[ ["Réservations", "Disponibilités, dates, besoins du client et statut dans un même flux."],
"Réservations", ["Préparation du parc", "Disponibilité, blocages, état de service et contexte dagence."],
"Disponibilités, dates, besoins du client et statut dans un même flux." ["Contrats de location", "Exigences de départ, état du contrat et dossiers opérationnels."],
],
[
"Préparation du parc",
"Disponibilité, blocages, état de service et contexte dagence."
],
[
"Contrats de location",
"Exigences de départ, état du contrat et dossiers opérationnels."
],
[ [
"Inspections des véhicules", "Inspections des véhicules",
"Contrôles structurés au départ et au retour avec actions de suivi." "Contrôles structurés au départ et au retour avec actions de suivi."
], ],
[ ["Dossiers clients", "Historique de location et coordonnées rattachés au travail en cours."],
"Dossiers clients", ["Rapports opérationnels", "Définitions et vues vérifiables par rapport à lactivité source."]
"Historique de location et coordonnées rattachés au travail en cours."
],
[
"Rapports opérationnels",
"Définitions et vues vérifiables par rapport à lactivité source."
]
] ]
}, },
"results": { "results": {
@@ -176,18 +128,12 @@
"Temps de traitement des réservations", "Temps de traitement des réservations",
"De la demande exploitable à la réservation confirmée." "De la demande exploitable à la réservation confirmée."
], ],
[ ["Temps de remise en service", "Du retour enregistré au statut prêt à louer."],
"Temps de remise en service",
"Du retour enregistré au statut prêt à louer."
],
[ [
"Taux dutilisation du parc", "Taux dutilisation du parc",
"Jours louables comparés aux jours loués, selon une définition approuvée." "Jours louables comparés aux jours loués, selon une définition approuvée."
], ],
[ ["Suivi des retards", "Locations en retard avec responsable et prochaine action."]
"Suivi des retards",
"Locations en retard avec responsable et prochaine action."
]
], ],
"methodTitle": "Méthode requise avant toute publication", "methodTitle": "Méthode requise avant toute publication",
"method": [ "method": [
@@ -215,22 +161,10 @@
"title": "Évaluez la plateforme avec des preuves, pas avec des badges.", "title": "Évaluez la plateforme avec des preuves, pas avec des badges.",
"body": "Aucune certification ni promesse de niveau de service nest publiée sans documentation approuvée. Pendant l’évaluation, demandez des réponses concrètes sur les contrôles utiles à votre activité.", "body": "Aucune certification ni promesse de niveau de service nest publiée sans documentation approuvée. Pendant l’évaluation, demandez des réponses concrètes sur les contrôles utiles à votre activité.",
"checks": [ "checks": [
[ ["Contrôle des accès", "Rôles, autorisations, actions administrateur et départs."],
"Contrôle des accès", ["Traçabilité", "Modifications enregistrées, recherchables et exportables."],
"Rôles, autorisations, actions administrateur et départs." ["Gestion des données", "Propriété, conservation, export, suppression et sous-traitants."],
], ["Continuité", "Sauvegarde, reprise, communication dincident et support."]
[
"Traçabilité",
"Modifications enregistrées, recherchables et exportables."
],
[
"Gestion des données",
"Propriété, conservation, export, suppression et sous-traitants."
],
[
"Continuité",
"Sauvegarde, reprise, communication dincident et support."
]
] ]
}, },
"pricing": { "pricing": {
@@ -289,65 +223,32 @@
[ [
"Produit", "Produit",
[ [
[ ["Parcours", "#workflow"],
"Parcours", ["Modules", "#modules"],
"#workflow" ["Tarifs", "#pricing"]
],
[
"Modules",
"#modules"
],
[
"Tarifs",
"#pricing"
]
] ]
], ],
[ [
"Entreprise", "Entreprise",
[ [
[ ["Contacter le service commercial", "#contact"],
"Contacter le service commercial", ["Réserver une démo", "#demo"]
"#contact"
],
[
"Réserver une démo",
"#demo"
]
] ]
], ],
[ [
"Compte", "Compte",
[ [
[ ["Se connecter", "#login"],
"Se connecter", ["Langue", "#language"],
"#login" ["Thème", "#theme"]
],
[
"Langue",
"#language"
],
[
"Thème",
"#theme"
]
] ]
], ],
[ [
"Mentions légales", "Mentions légales",
[ [
[ ["Confidentialité", "#!"],
"Confidentialité", ["Conditions", "#!"],
"#!" ["Accessibilité", "#!"]
],
[
"Conditions",
"#!"
],
[
"Accessibilité",
"#!"
]
] ]
] ]
], ],
@@ -377,11 +278,7 @@
"Plus de 500 véhicules", "Plus de 500 véhicules",
"Je ne sais pas encore" "Je ne sais pas encore"
], ],
"langOptions": [ "langOptions": ["English", "Français", "العربية"],
"English",
"Français",
"العربية"
],
"successTitle": "Demande de démonstration enregistrée", "successTitle": "Demande de démonstration enregistrée",
"successBody": "Ce prototype a terminé le parcours de réservation. Aucune demande na été envoyée à un véritable système commercial.", "successBody": "Ce prototype a terminé le parcours de réservation. Aucune demande na été envoyée à un véritable système commercial.",
"submitAnother": "Envoyer une autre demande", "submitAnother": "Envoyer une autre demande",
@@ -3,22 +3,77 @@ import path from 'node:path';
import crypto from 'node:crypto'; import crypto from 'node:crypto';
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..'); const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
const required = [ const required = [
'09_DEVELOPER_HANDOFF_v1.0.md','DECISION_LOG_v9.0.md','DEFINITION_OF_DONE_v1.0.md','OPEN_ISSUE_REGISTER_v1.0.csv', '09_DEVELOPER_HANDOFF_v1.0.md',
'tokens/design-tokens_v1.0.json','tokens/semantic-tokens_v1.0.css','components/COMPONENT_CONTRACTS_v1.0.md', 'DECISION_LOG_v9.0.md',
'localization/ROUTE_MANIFEST_v1.0.json','localization/locales/en/homepage.json','localization/locales/fr/homepage.json','localization/locales/ar/homepage.json', 'DEFINITION_OF_DONE_v1.0.md',
'interaction/theme-bootstrap_v1.0.js','qa/QA_MATRIX_v1.0.csv','qa/ACCESSIBILITY_CHECKLIST_v1.0.csv','qa/PERFORMANCE_BUDGETS_v1.0.json' 'OPEN_ISSUE_REGISTER_v1.0.csv',
'tokens/design-tokens_v1.0.json',
'tokens/semantic-tokens_v1.0.css',
'components/COMPONENT_CONTRACTS_v1.0.md',
'localization/ROUTE_MANIFEST_v1.0.json',
'localization/locales/en/homepage.json',
'localization/locales/fr/homepage.json',
'localization/locales/ar/homepage.json',
'interaction/theme-bootstrap_v1.0.js',
'qa/QA_MATRIX_v1.0.csv',
'qa/ACCESSIBILITY_CHECKLIST_v1.0.csv',
'qa/PERFORMANCE_BUDGETS_v1.0.json',
]; ];
const errors=[]; let checks=0; const errors = [];
for (const file of required){ checks++; if(!fs.existsSync(path.join(root,file))) errors.push(`Missing ${file}`); } let checks = 0;
for (const file of required) {
checks++;
if (!fs.existsSync(path.join(root, file))) errors.push(`Missing ${file}`);
}
const readJson = (f) => JSON.parse(fs.readFileSync(path.join(root, f), 'utf8')); const readJson = (f) => JSON.parse(fs.readFileSync(path.join(root, f), 'utf8'));
for (const file of ['tokens/design-tokens_v1.0.json','localization/ROUTE_MANIFEST_v1.0.json','qa/PERFORMANCE_BUDGETS_v1.0.json','qa/visual-regression-scenarios_v1.0.json']) { try{readJson(file);checks++;}catch(e){errors.push(`${file}: ${e.message}`);} } for (const file of [
function shape(value){ if(Array.isArray(value)) return value.map(shape); if(value && typeof value==='object') return Object.fromEntries(Object.keys(value).sort().map(k=>[k,shape(value[k])])); return typeof value; } 'tokens/design-tokens_v1.0.json',
const locales=['en','fr','ar'].map(l=>readJson(`localization/locales/${l}/homepage.json`)); 'localization/ROUTE_MANIFEST_v1.0.json',
'qa/PERFORMANCE_BUDGETS_v1.0.json',
'qa/visual-regression-scenarios_v1.0.json',
]) {
try {
readJson(file);
checks++;
} catch (e) {
errors.push(`${file}: ${e.message}`);
}
}
function shape(value) {
if (Array.isArray(value)) return value.map(shape);
if (value && typeof value === 'object')
return Object.fromEntries(
Object.keys(value)
.sort()
.map((k) => [k, shape(value[k])]),
);
return typeof value;
}
const locales = ['en', 'fr', 'ar'].map((l) => readJson(`localization/locales/${l}/homepage.json`));
const base = JSON.stringify(shape(locales[0])); const base = JSON.stringify(shape(locales[0]));
for (let i=1;i<locales.length;i++){ checks++; if(JSON.stringify(shape(locales[i]))!==base) errors.push(`Locale shape mismatch: ${['en','fr','ar'][i]}`); } for (let i = 1; i < locales.length; i++) {
checks++;
if (JSON.stringify(shape(locales[i])) !== base)
errors.push(`Locale shape mismatch: ${['en', 'fr', 'ar'][i]}`);
}
const routes = readJson('localization/ROUTE_MANIFEST_v1.0.json'); const routes = readJson('localization/ROUTE_MANIFEST_v1.0.json');
for (const route of routes.routes){ for (const locale of routes.locales){ checks++; if(!(locale in route.slugs)) errors.push(`Route ${route.id} missing ${locale}`); } } for (const route of routes.routes) {
const issueCsv=fs.readFileSync(path.join(root,'OPEN_ISSUE_REGISTER_v1.0.csv'),'utf8'); checks++; if(/,(Critical|High),[^\n]*,,/.test(issueCsv)) errors.push('Critical/High issue missing ownership data'); for (const locale of routes.locales) {
const css=fs.readFileSync(path.join(root,'tokens/semantic-tokens_v1.0.css'),'utf8'); checks++; if(!css.includes('--color-action-conversion')) errors.push('Conversion semantic token missing'); checks++;
if(errors.length){ console.error(JSON.stringify({status:'FAILED',checks,errors},null,2)); process.exit(1); } if (!(locale in route.slugs)) errors.push(`Route ${route.id} missing ${locale}`);
console.log(JSON.stringify({status:'PASSED',checks,files:fs.readdirSync(root).length},null,2)); }
}
const issueCsv = fs.readFileSync(path.join(root, 'OPEN_ISSUE_REGISTER_v1.0.csv'), 'utf8');
checks++;
if (/,(Critical|High),[^\n]*,,/.test(issueCsv))
errors.push('Critical/High issue missing ownership data');
const css = fs.readFileSync(path.join(root, 'tokens/semantic-tokens_v1.0.css'), 'utf8');
checks++;
if (!css.includes('--color-action-conversion')) errors.push('Conversion semantic token missing');
if (errors.length) {
console.error(JSON.stringify({ status: 'FAILED', checks, errors }, null, 2));
process.exit(1);
}
console.log(
JSON.stringify({ status: 'PASSED', checks, files: fs.readdirSync(root).length }, null, 2),
);
@@ -5,80 +5,56 @@
{ {
"id": "VR-001", "id": "VR-001",
"route": "/en", "route": "/en",
"viewport": [ "viewport": [1440, 1000],
1440,
1000
],
"theme": "light", "theme": "light",
"state": "homepage-top" "state": "homepage-top"
}, },
{ {
"id": "VR-002", "id": "VR-002",
"route": "/fr", "route": "/fr",
"viewport": [ "viewport": [1024, 900],
1024,
900
],
"theme": "dark", "theme": "dark",
"state": "homepage-top" "state": "homepage-top"
}, },
{ {
"id": "VR-003", "id": "VR-003",
"route": "/ar", "route": "/ar",
"viewport": [ "viewport": [390, 844],
390,
844
],
"theme": "light", "theme": "light",
"state": "homepage-top" "state": "homepage-top"
}, },
{ {
"id": "VR-004", "id": "VR-004",
"route": "/ar", "route": "/ar",
"viewport": [ "viewport": [390, 844],
390,
844
],
"theme": "dark", "theme": "dark",
"state": "drawer-open" "state": "drawer-open"
}, },
{ {
"id": "VR-005", "id": "VR-005",
"route": "/en", "route": "/en",
"viewport": [ "viewport": [390, 844],
390,
844
],
"theme": "light", "theme": "light",
"state": "demo-invalid" "state": "demo-invalid"
}, },
{ {
"id": "VR-006", "id": "VR-006",
"route": "/fr", "route": "/fr",
"viewport": [ "viewport": [768, 1024],
768,
1024
],
"theme": "dark", "theme": "dark",
"state": "tour-step-3" "state": "tour-step-3"
}, },
{ {
"id": "VR-007", "id": "VR-007",
"route": "/ar", "route": "/ar",
"viewport": [ "viewport": [320, 800],
320,
800
],
"theme": "dark", "theme": "dark",
"state": "reflow-gate" "state": "reflow-gate"
}, },
{ {
"id": "VR-008", "id": "VR-008",
"route": "/en", "route": "/en",
"viewport": [ "viewport": [1440, 1000],
1440,
1000
],
"theme": "dark", "theme": "dark",
"state": "pricing-and-final-cta" "state": "pricing-and-final-cta"
} }
@@ -13,7 +13,7 @@ No claim is made that users understand the product, find the CTA, or complete th
## Pre-test findings ## Pre-test findings
| ID | Evidence | Finding | Severity | Status | | ID | Evidence | Finding | Severity | Status |
|---|---|---|---|---| | ------ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------- |
| P8-001 | Expert + code review | Demo submission in Phase 7 prevented default and produced no success, failure, or recovery state. | S0/C | Fixed in prototype | | P8-001 | Expert + code review | Demo submission in Phase 7 prevented default and produced no success, failure, or recovery state. | S0/C | Fixed in prototype |
| P8-002 | Expert + code review | “Watch product tour” navigated to a workflow section rather than opening a tour, creating an expectation mismatch. | S1/C | Fixed with four-step tour | | P8-002 | Expert + code review | “Watch product tour” navigated to a workflow section rather than opening a tour, creating an expectation mismatch. | S1/C | Fixed with four-step tour |
| P8-003 | Expert + code review | Mobile drawer moved focus initially but did not contain focus or make background content inert. | S0/C | Fixed in prototype | | P8-003 | Expert + code review | Mobile drawer moved focus initially but did not contain focus or make background content inert. | S0/C | Fixed in prototype |
@@ -66,14 +66,14 @@
--duration-standard: 160ms; --duration-standard: 160ms;
--duration-overlay: 220ms; --duration-overlay: 220ms;
--easing-standard: ease; --easing-standard: ease;
--easing-productive: cubic-bezier(.2,0,0,1); --easing-productive: cubic-bezier(0.2, 0, 0, 1);
--font-latin: Inter Variable, Inter, Arial, sans-serif; --font-latin: Inter Variable, Inter, Arial, sans-serif;
--font-arabic: Noto Sans Arabic, Arial, sans-serif; --font-arabic: Noto Sans Arabic, Arial, sans-serif;
--breakpoint-medium: 768px; --breakpoint-medium: 768px;
--breakpoint-wide: 1120px; --breakpoint-wide: 1120px;
} }
html[data-theme="light"] { html[data-theme='light'] {
--color-canvas: var(--color-gray-25); --color-canvas: var(--color-gray-25);
--color-surface: #ffffff; --color-surface: #ffffff;
--color-surface-raised: #ffffff; --color-surface-raised: #ffffff;
@@ -91,10 +91,10 @@ html[data-theme="light"] {
--color-on-inverse: #f8fafc; --color-on-inverse: #f8fafc;
--color-soft-blue: #eef5ff; --color-soft-blue: #eef5ff;
--color-soft-orange: #fff7ed; --color-soft-orange: #fff7ed;
--shadow-color: rgba(8,20,38,.12); --shadow-color: rgba(8, 20, 38, 0.12);
color-scheme: light; color-scheme: light;
} }
html[data-theme="dark"] { html[data-theme='dark'] {
--color-canvas: var(--color-navy-950); --color-canvas: var(--color-navy-950);
--color-surface: var(--color-navy-900); --color-surface: var(--color-navy-900);
--color-surface-raised: var(--color-navy-850); --color-surface-raised: var(--color-navy-850);
@@ -112,11 +112,19 @@ html[data-theme="dark"] {
--color-on-inverse: var(--color-navy-950); --color-on-inverse: var(--color-navy-950);
--color-soft-blue: #10284a; --color-soft-blue: #10284a;
--color-soft-orange: #3a2015; --color-soft-orange: #3a2015;
--shadow-color: rgba(0,0,0,.36); --shadow-color: rgba(0, 0, 0, 0.36);
color-scheme: dark; color-scheme: dark;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; } html {
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
} }
+2 -2
View File
@@ -1,6 +1,6 @@
export const breakpoints = { medium: 768, wide: 1120 } as const; export const breakpoints = { medium: 768, wide: 1120 } as const;
export const themePreferences = ["light", "dark", "system"] as const; export const themePreferences = ['light', 'dark', 'system'] as const;
export type ThemePreference = (typeof themePreferences)[number]; export type ThemePreference = (typeof themePreferences)[number];
export type ResolvedTheme = Exclude<ThemePreference, "system">; export type ResolvedTheme = Exclude<ThemePreference, 'system'>;
export const layout = { containerMaxPx: 1280, touchTargetMinPx: 44, spacingBasePx: 4 } as const; export const layout = { containerMaxPx: 1280, touchTargetMinPx: 44, spacingBasePx: 4 } as const;
export const motion = { fastMs: 120, standardMs: 160, overlayMs: 220 } as const; export const motion = { fastMs: 120, standardMs: 160, overlayMs: 220 } as const;
@@ -10,7 +10,7 @@
## Findings ## Findings
| Severity | Code | Path | Line | Finding | | Severity | Code | Path | Line | Finding |
|---|---|---|---:|---| | -------- | ------------------------------------ | -------------------------------------------------- | ---: | --------------------------------------------------------------- |
| High | `ci_integration_suite_omitted` | `.github/workflows/ci.yml` | | CI does not execute the demo-service integration suite. | | High | `ci_integration_suite_omitted` | `.github/workflows/ci.yml` | | CI does not execute the demo-service integration suite. |
| High | `issue_register_overflow` | `docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv` | | One or more issue-register rows contain overflow columns. | | High | `issue_register_overflow` | `docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv` | | One or more issue-register rows contain overflow columns. |
| High | `malformed_csv` | `docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv` | 43 | Expected 9 columns, found 11. | | High | `malformed_csv` | `docs/IMPLEMENTATION_ISSUE_REGISTER_v1.4.csv` | 43 | Expected 9 columns, found 11. |
@@ -1 +1,6 @@
{"command":"node scripts/run-validations.mjs","exit_code":0,"wall_seconds":1,"log":"docs/quality/phase15-command-logs/01-validate.log"} {
"command": "node scripts/run-validations.mjs",
"exit_code": 0,
"wall_seconds": 1,
"log": "docs/quality/phase15-command-logs/01-validate.log"
}
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" /> /// <reference types="next" />
/// <reference types="next/image-types/global" /> /// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts"; import './.next/types/routes.d.ts';
// NOTE: This file should not be edited // NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+2132 -536
View File
File diff suppressed because it is too large Load Diff
+19 -3
View File
@@ -4,8 +4,20 @@ import path from 'node:path';
const root = path.resolve(import.meta.dirname, '..'); const root = path.resolve(import.meta.dirname, '..');
const manifestPath = path.join(root, 'PHASE_16_PACKAGE_MANIFEST_v1.0.json'); const manifestPath = path.join(root, 'PHASE_16_PACKAGE_MANIFEST_v1.0.json');
const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']); const excludedDirectories = new Set([
const excludedFiles = new Set(['PHASE_16_PACKAGE_MANIFEST_v1.0.json', 'package-lock.json', 'tsconfig.tsbuildinfo']); '.git',
'.next',
'node_modules',
'coverage',
'playwright-report',
'test-results',
'__pycache__',
]);
const excludedFiles = new Set([
'PHASE_16_PACKAGE_MANIFEST_v1.0.json',
'package-lock.json',
'tsconfig.tsbuildinfo',
]);
async function walk(directory) { async function walk(directory) {
const files = []; const files = [];
@@ -26,7 +38,11 @@ const entries = [];
for (const file of files) { for (const file of files) {
const contents = await readFile(file.fullPath); const contents = await readFile(file.fullPath);
const details = await stat(file.fullPath); const details = await stat(file.fullPath);
entries.push({ path: file.relative, bytes: details.size, sha256: createHash('sha256').update(contents).digest('hex') }); entries.push({
path: file.relative,
bytes: details.size,
sha256: createHash('sha256').update(contents).digest('hex'),
});
} }
const manifest = { const manifest = {
schema_version: '1.0', schema_version: '1.0',
+1 -1
View File
@@ -77,7 +77,7 @@ const integrationVisual = await readFile(
path.join(repositoryRoot, 'tests/visual/integrations.visual.spec.ts'), path.join(repositoryRoot, 'tests/visual/integrations.visual.spec.ts'),
'utf8', 'utf8',
); );
if (!componentVisual.includes("hpc.theme.preference")) { if (!componentVisual.includes('hpc.theme.preference')) {
failures.push('Component visual test does not use the canonical theme storage key.'); failures.push('Component visual test does not use the canonical theme storage key.');
} }
if (componentVisual.includes('rdg-theme-preference')) { if (componentVisual.includes('rdg-theme-preference')) {
+35 -11
View File
@@ -3,7 +3,15 @@ import path from 'node:path';
import { fail, pass, repositoryRoot, sha256 } from './lib.mjs'; import { fail, pass, repositoryRoot, sha256 } from './lib.mjs';
const manifestName = 'PHASE_16_PACKAGE_MANIFEST_v1.0.json'; const manifestName = 'PHASE_16_PACKAGE_MANIFEST_v1.0.json';
const excludedDirectories = new Set(['.git', '.next', 'node_modules', 'coverage', 'playwright-report', 'test-results', '__pycache__']); const excludedDirectories = new Set([
'.git',
'.next',
'node_modules',
'coverage',
'playwright-report',
'test-results',
'__pycache__',
]);
const excludedFiles = new Set([manifestName, 'package-lock.json', 'tsconfig.tsbuildinfo']); const excludedFiles = new Set([manifestName, 'package-lock.json', 'tsconfig.tsbuildinfo']);
async function walk(directory) { async function walk(directory) {
@@ -22,25 +30,41 @@ async function walk(directory) {
const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8')); const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8'));
const failures = []; const failures = [];
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16) failures.push('Phase 16 manifest identity is invalid.'); if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16)
if (manifest.release_status !== 'Launch aborted before deployment') failures.push('Manifest release status is not the recorded Phase 16 decision.'); failures.push('Phase 16 manifest identity is invalid.');
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length) failures.push('Manifest file count is invalid.'); if (manifest.release_status !== 'Launch aborted before deployment')
failures.push('Manifest release status is not the recorded Phase 16 decision.');
if (!Array.isArray(manifest.files) || manifest.file_count !== manifest.files.length)
failures.push('Manifest file count is invalid.');
const listed = new Set(); const listed = new Set();
for (const entry of manifest.files ?? []) { for (const entry of manifest.files ?? []) {
if (typeof entry.path !== 'string' || path.isAbsolute(entry.path) || entry.path.split('/').includes('..')) { if (
failures.push(`Unsafe manifest path: ${String(entry.path)}`); continue; typeof entry.path !== 'string' ||
path.isAbsolute(entry.path) ||
entry.path.split('/').includes('..')
) {
failures.push(`Unsafe manifest path: ${String(entry.path)}`);
continue;
}
if (listed.has(entry.path)) {
failures.push(`${entry.path}: duplicate manifest entry.`);
continue;
} }
if (listed.has(entry.path)) { failures.push(`${entry.path}: duplicate manifest entry.`); continue; }
listed.add(entry.path); listed.add(entry.path);
try { try {
const filePath = path.join(repositoryRoot, entry.path); const filePath = path.join(repositoryRoot, entry.path);
const details = await stat(filePath); const details = await stat(filePath);
if (!details.isFile() || details.size !== entry.bytes) failures.push(`${entry.path}: file metadata changed.`); if (!details.isFile() || details.size !== entry.bytes)
failures.push(`${entry.path}: file metadata changed.`);
if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`); if ((await sha256(filePath)) !== entry.sha256) failures.push(`${entry.path}: SHA-256 changed.`);
} catch { failures.push(`${entry.path}: file is missing.`); } } catch {
failures.push(`${entry.path}: file is missing.`);
}
} }
const actual = new Set(await walk(repositoryRoot)); const actual = new Set(await walk(repositoryRoot));
for (const file of actual) if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`); for (const file of actual)
for (const file of listed) if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`); if (!listed.has(file)) failures.push(`${file}: unlisted repository file.`);
for (const file of listed)
if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
if (failures.length) fail(failures); if (failures.length) fail(failures);
else pass(`Phase 16 package manifest verifies ${listed.size} repository files with no extras.`); else pass(`Phase 16 package manifest verifies ${listed.size} repository files with no extras.`);
+46 -11
View File
@@ -16,16 +16,51 @@ const required = [
]; ];
const failures = []; const failures = [];
for (const file of required) { for (const file of required) {
try { await access(path.join(repositoryRoot, file)); } try {
catch { failures.push(`Missing required Phase 16 artifact: ${file}`); } await access(path.join(repositoryRoot, file));
} catch {
failures.push(`Missing required Phase 16 artifact: ${file}`);
} }
const status = await readFile(path.join(repositoryRoot, 'docs/phase16/FINAL_LAUNCH_STATUS_v1.0.md'), 'utf8'); }
const auth = await readFile(path.join(repositoryRoot, 'docs/phase16/LAUNCH_AUTHORIZATION_RECORD_v1.0.md'), 'utf8'); const status = await readFile(
const flags = await readFile(path.join(repositoryRoot, 'docs/phase16/FEATURE_FLAG_RELEASE_STATE_v1.0.csv'), 'utf8'); path.join(repositoryRoot, 'docs/phase16/FINAL_LAUNCH_STATUS_v1.0.md'),
const secrets = await readFile(path.join(repositoryRoot, 'docs/phase16/SECRET_PROVISIONING_RECORD_v1.0.csv'), 'utf8'); 'utf8',
if (!status.includes('Launch aborted before deployment')) failures.push('Final launch status is missing or inconsistent.'); );
if (!auth.includes('**Rejected**')) failures.push('Launch authorization is not explicitly rejected.'); const auth = await readFile(
if (/\btrue\b/.test(flags.split('\n').slice(1).map((line) => line.split(',')[1]).join('\n'))) failures.push('A Phase 16 production feature flag is enabled.'); path.join(repositoryRoot, 'docs/phase16/LAUNCH_AUTHORIZATION_RECORD_v1.0.md'),
if (!secrets.includes('No production credentials exist') && !secrets.includes('No credential supplied')) failures.push('Secret provisioning record does not explicitly preserve missing credentials as blockers.'); 'utf8',
);
const flags = await readFile(
path.join(repositoryRoot, 'docs/phase16/FEATURE_FLAG_RELEASE_STATE_v1.0.csv'),
'utf8',
);
const secrets = await readFile(
path.join(repositoryRoot, 'docs/phase16/SECRET_PROVISIONING_RECORD_v1.0.csv'),
'utf8',
);
if (!status.includes('Launch aborted before deployment'))
failures.push('Final launch status is missing or inconsistent.');
if (!auth.includes('**Rejected**'))
failures.push('Launch authorization is not explicitly rejected.');
if (
/\btrue\b/.test(
flags
.split('\n')
.slice(1)
.map((line) => line.split(',')[1])
.join('\n'),
)
)
failures.push('A Phase 16 production feature flag is enabled.');
if (
!secrets.includes('No production credentials exist') &&
!secrets.includes('No credential supplied')
)
failures.push(
'Secret provisioning record does not explicitly preserve missing credentials as blockers.',
);
if (failures.length) fail(failures); if (failures.length) fail(failures);
else pass('Phase 16 no-go governance, disabled feature state, blocked approvals, and evidence package are explicit and internally consistent.'); else
pass(
'Phase 16 no-go governance, disabled feature state, blocked approvals, and evidence package are explicit and internally consistent.',
);
+15 -7
View File
@@ -66,14 +66,14 @@
--duration-standard: 160ms; --duration-standard: 160ms;
--duration-overlay: 220ms; --duration-overlay: 220ms;
--easing-standard: ease; --easing-standard: ease;
--easing-productive: cubic-bezier(.2,0,0,1); --easing-productive: cubic-bezier(0.2, 0, 0, 1);
--font-latin: Inter Variable, Inter, Arial, sans-serif; --font-latin: Inter Variable, Inter, Arial, sans-serif;
--font-arabic: Noto Sans Arabic, Arial, sans-serif; --font-arabic: Noto Sans Arabic, Arial, sans-serif;
--breakpoint-medium: 768px; --breakpoint-medium: 768px;
--breakpoint-wide: 1120px; --breakpoint-wide: 1120px;
} }
html[data-theme="light"] { html[data-theme='light'] {
--color-canvas: var(--color-gray-25); --color-canvas: var(--color-gray-25);
--color-surface: #ffffff; --color-surface: #ffffff;
--color-surface-raised: #ffffff; --color-surface-raised: #ffffff;
@@ -91,10 +91,10 @@ html[data-theme="light"] {
--color-on-inverse: #f8fafc; --color-on-inverse: #f8fafc;
--color-soft-blue: #eef5ff; --color-soft-blue: #eef5ff;
--color-soft-orange: #fff7ed; --color-soft-orange: #fff7ed;
--shadow-color: rgba(8,20,38,.12); --shadow-color: rgba(8, 20, 38, 0.12);
color-scheme: light; color-scheme: light;
} }
html[data-theme="dark"] { html[data-theme='dark'] {
--color-canvas: var(--color-navy-950); --color-canvas: var(--color-navy-950);
--color-surface: var(--color-navy-900); --color-surface: var(--color-navy-900);
--color-surface-raised: var(--color-navy-850); --color-surface-raised: var(--color-navy-850);
@@ -112,11 +112,19 @@ html[data-theme="dark"] {
--color-on-inverse: var(--color-navy-950); --color-on-inverse: var(--color-navy-950);
--color-soft-blue: #10284a; --color-soft-blue: #10284a;
--color-soft-orange: #3a2015; --color-soft-orange: #3a2015;
--shadow-color: rgba(0,0,0,.36); --shadow-color: rgba(0, 0, 0, 0.36);
color-scheme: dark; color-scheme: dark;
} }
@media (prefers-reduced-motion: reduce) { @media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; } html {
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; } scroll-behavior: auto;
}
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
} }
+5 -8
View File
@@ -10,13 +10,10 @@ describe('demo API content type validation', () => {
expect(isApplicationJson(value)).toBe(true); expect(isApplicationJson(value)).toBe(true);
}); });
it.each([ it.each([null, '', 'text/plain', 'application/jsonp', 'application/json-patch+json'])(
null, 'rejects %s',
'', (value) => {
'text/plain',
'application/jsonp',
'application/json-patch+json',
])('rejects %s', (value) => {
expect(isApplicationJson(value)).toBe(false); expect(isApplicationJson(value)).toBe(false);
}); },
);
}); });