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.
| 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 |
| 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 |
+1 -1
View File
@@ -4,7 +4,7 @@
**Phases:** 1 — Design Foundation through 9 — Developer Handoff
| 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-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 |
+39 -39
View File
@@ -8,13 +8,13 @@
"files": [
{
"path": "09_DEVELOPER_HANDOFF_v1.0.md",
"bytes": 8091,
"sha256": "e82d486f1e566d1bf0263f1baa7dd8035ae62da5c36bb6bfa417e61331ec8d51"
"bytes": 8378,
"sha256": "6cb5a76ee42e48caadd672e852520aeed0a974a9e4642be924c6b0b3176e8b1f"
},
{
"path": "DECISION_LOG_v9.0.md",
"bytes": 38775,
"sha256": "85569a7418f8cad57d094ee9ec1d83b7727b5ab91a405cbda26de0271a8d76a1"
"bytes": 63034,
"sha256": "8e00d2cfa72b2bad65d855827a0fdfd09cce3e30e9885e46d8f8c6c7feb5f09f"
},
{
"path": "DEFINITION_OF_DONE_v1.0.md",
@@ -108,8 +108,8 @@
},
{
"path": "components/COMPONENT_CONTRACTS_v1.0.md",
"bytes": 9800,
"sha256": "84b455fe725bff11b6ab54bb13373b1aa52991b062a36fad07db47a940187a53"
"bytes": 9698,
"sha256": "c36fe8a5221d0f76f0b74894d7227135adb7bae45eca0a7554a31b9433218052"
},
{
"path": "components/COMPONENT_STATE_MATRIX_v1.0.csv",
@@ -138,23 +138,23 @@
},
{
"path": "implementation/component-contracts_v1.0.ts",
"bytes": 2844,
"sha256": "177dd2420b804290cf1296e43f41a7e759c8859b3253073fbdc523a81f6f94c0"
"bytes": 3041,
"sha256": "45fb0222d99308b2f28014572316b13c2d7308bc16b1bd02f63b6e2257148399"
},
{
"path": "implementation/hreflang-generator_v1.0.ts",
"bytes": 751,
"sha256": "ab919a36a5d08a28c38ae51b4ddd0d84cef5d3ec3bed13649ebd26c3fb80ac8a"
"bytes": 799,
"sha256": "aa9b5e2eb602b2f651b1195a2d0ff7c2a0e6157bf702d3b27ab25b5b86249ba8"
},
{
"path": "implementation/localization-config_v1.0.ts",
"bytes": 772,
"sha256": "bd8ca09f121bfe516ece817948a81d58136e9205fbd2c05dad2ce87f2d7a1782"
"bytes": 806,
"sha256": "fd884ce4cd807ccdd196bd4c09e34128b3a26df73e0f267c120f270179c852d0"
},
{
"path": "implementation/theme-config_v1.0.ts",
"bytes": 725,
"sha256": "f56d37da4129d5830ba9ebd77b18cd6084a144e8514bfd8a8152387ecf76607c"
"bytes": 752,
"sha256": "5ae9687c6ffa832ae12b70fa8272a61853960eea8a3cc18e3a68ef1f120046f8"
},
{
"path": "interaction/ANALYTICS_EVENT_SPEC_v1.0.csv",
@@ -168,28 +168,28 @@
},
{
"path": "interaction/analytics-contract_v1.0.ts",
"bytes": 832,
"sha256": "2716eb69cc98032005e068ab7bdc265a4c896a8e1a1bed682c7fdd4f45368123"
"bytes": 918,
"sha256": "d79b32503a5f8346816c647b8a7f45a62468b96f4fc9525d6d5147b5d2f2ec45"
},
{
"path": "interaction/demo-form-contract_v1.0.ts",
"bytes": 632,
"sha256": "194db77f7cfca417dbe35e6f2c2ea94cdd02806b83b1d0f1993fe509d8c10154"
"bytes": 699,
"sha256": "073cf70224f93312d177f77abd7987704205628209e30c44824ae45f929ee59e"
},
{
"path": "interaction/theme-bootstrap_v1.0.js",
"bytes": 753,
"sha256": "7b89a849bdcd72e1049714ebf76f0c0decb39e44261480f0cb08bb7dfe734e4a"
"bytes": 757,
"sha256": "a90b31cc2b91fd085b8cd4442b85fce56127501602b9b7f90af75fd3c8f4c3b0"
},
{
"path": "localization/LOCALIZATION_IMPLEMENTATION_v1.0.md",
"bytes": 2062,
"sha256": "87a494d7ef7ed9f5012ed1699edf90e38995aa89a0efc3800db854e1d57e1024"
"bytes": 2069,
"sha256": "63773e2f4777a6a322570d07d8badac8867dc4d6eef2d71bf05cbf81dfd2c85b"
},
{
"path": "localization/ROUTE_MANIFEST_v1.0.json",
"bytes": 1573,
"sha256": "fa29b8d31da1f36f87d216a1228fd1bbe1cde337636950b40b0024a10efaf49a"
"bytes": 1433,
"sha256": "e08ac94ba1f3899bb02dfa934695f530d1089f9f03b0fadaa0feabd466ad8ec4"
},
{
"path": "localization/TRANSLATION_KEY_INVENTORY_v1.0.csv",
@@ -198,18 +198,18 @@
},
{
"path": "localization/locales/ar/homepage.json",
"bytes": 18615,
"sha256": "fe08aabd60203dbdebc718117f5e497b52a453a8a6fb671fe0bca28651bf0d47"
"bytes": 17419,
"sha256": "19c1b9d0c90c017da1679b2cde4f123a98625ab6dae8002f58354839309f0474"
},
{
"path": "localization/locales/en/homepage.json",
"bytes": 13869,
"sha256": "a5ed956c7f07fad016ca39b40b770c25e8beeae2799ff0fece82dbf087f18e51"
"bytes": 12769,
"sha256": "ba88daa108e3df63285327d0f7d7be6c0df3d971d800d48617e1e62c3414acfb"
},
{
"path": "localization/locales/fr/homepage.json",
"bytes": 15512,
"sha256": "24079356bd47431ffeb66b7eb2981e108c03080205861b1bb42514b03b945a0a"
"bytes": 14564,
"sha256": "5a3dfe1dc43c99a30cfc275309b5084b5d9f583a33ea136d388babb0714de719"
},
{
"path": "qa/ACCESSIBILITY_CHECKLIST_v1.0.csv",
@@ -243,18 +243,18 @@
},
{
"path": "qa/validate-phase9-package_v1.0.mjs",
"bytes": 2534,
"sha256": "631892318cf95c0530a605e5bf08def9b06c93af79b555205f3aeb8a2bcd87ed"
"bytes": 2766,
"sha256": "8bd42770f659b51e63d73b282d7d101d6d817650f8c9b8875cad1b0eb3eece4e"
},
{
"path": "qa/visual-regression-scenarios_v1.0.json",
"bytes": 1401,
"sha256": "08a04f676cbb93e73b4c6bc112a55ebf287cb97be3dd3176ab80161e6de743c2"
"bytes": 1209,
"sha256": "27d961c33c32775485475f7037b8d3bbd13f634b65fe2f59d6eea898860edf5f"
},
{
"path": "reference/FINDINGS_REPORT_v1.0.md",
"bytes": 3062,
"sha256": "ba34afd535856d0eaa8ed7e174c23a7b5f0eae87344c8fcee5daafe00c068ce4"
"bytes": 4135,
"sha256": "20eaba0c9fafd166220a64b2f0ede3c36b3bd9fc6f32982c1f6c7745f9311965"
},
{
"path": "reference/PHASE_8_HANDOFF_v1.0.md",
@@ -288,13 +288,13 @@
},
{
"path": "tokens/semantic-tokens_v1.0.css",
"bytes": 3553,
"sha256": "99912762e078bfc78b4878fc46b8c7d03d90c073e9c236942a58d83f6585a1b9"
"bytes": 3589,
"sha256": "f28b2f03139a6aa89b85d00574f4fe56aa90d4ca1e03e80a1fb6eb8e5d9fa621"
},
{
"path": "tokens/tokens_v1.0.ts",
"bytes": 442,
"sha256": "ec265270251f8dd01d9166aeca3fcec9be2b5e534d825c9058fb6ed3f51e3224"
"sha256": "3a1a2bb1bd29e55bf024a468a7c95bf2ec29d3cdc479f580050e8503c19c40a7"
},
{
"path": "validation/CONTENT_AND_STRUCTURE_VALIDATION_v1.0.json",
@@ -1,10 +1,7 @@
# 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.
## SiteHeader
**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.
## MobileDrawer
**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.
## LocaleSelector
**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.
## ThemeSelector
**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.
## HeroSection
**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.
## ProductPreview
**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.
## LifecycleProof
**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.
## ComparisonSection
**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.
## WorkflowSteps
**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.
## RoleCards
**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.
## ModuleGrid
**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.
## MeasurementPlan
**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.
## IntegrationCategories
**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.
## SecurityChecklist
**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.
## PricingPreview
**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.
## FAQAccordion
**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.
## FinalCTA
**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.
## SiteFooter
**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.
## DemoDialog
**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.
## ProductTourDialog
**Purpose:** Four-step guided overview
@@ -1,26 +1,136 @@
export type Locale = "en" | "fr" | "ar";
export type Direction = "ltr" | "rtl";
export type EvidenceStatus = "approved" | "illustrative" | "pending" | "expired" | "rejected";
export type ThemePreference = "light" | "dark" | "system";
export type Locale = 'en' | 'fr' | 'ar';
export type Direction = 'ltr' | 'rtl';
export type EvidenceStatus = 'approved' | 'illustrative' | 'pending' | 'expired' | 'rejected';
export type ThemePreference = 'light' | 'dark' | 'system';
export interface LinkModel { id: string; label: string; href: string; external?: boolean; }
export interface SiteHeaderProps { locale: Locale; direction: Direction; links: LinkModel[]; login?: LinkModel; onDemo(): void; }
export interface DrawerProps extends SiteHeaderProps { open: boolean; onOpenChange(open: boolean): void; returnFocusRef?: HTMLElement | null; }
export interface LocaleSelectorProps { locale: Locale; 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; }
export interface LinkModel {
id: string;
label: string;
href: string;
external?: boolean;
}
export interface SiteHeaderProps {
locale: Locale;
direction: Direction;
links: LinkModel[];
login?: LinkModel;
onDemo(): void;
}
export interface DrawerProps extends SiteHeaderProps {
open: boolean;
onOpenChange(open: boolean): void;
returnFocusRef?: HTMLElement | null;
}
export interface LocaleSelectorProps {
locale: Locale;
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";
type Locale = "en"|"fr"|"ar";
import manifest from '../localization/ROUTE_MANIFEST_v1.0.json';
type Locale = 'en' | 'fr' | 'ar';
export function localizedPath(routeId: string, locale: Locale): string {
const route = manifest.routes.find((item) => item.id === 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}`;
}
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 }));
alternates.push({ rel:"alternate", hreflang:"x-default", href:new URL(localizedPath(routeId,"en"),origin).href });
const alternates = manifest.locales.map((locale) => ({
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;
}
@@ -1,10 +1,20 @@
export const locales = ["en", "fr", "ar"] as const;
export type Locale = typeof locales[number];
export const defaultLocale: Locale = "en";
export const directionByLocale: Record<Locale,"ltr"|"rtl"> = { en:"ltr", fr:"ltr", ar:"rtl" };
export const localeCookie = "hpc-locale";
export const localeStorageKey = "hpc.locale";
export const campaignQueryAllowlist = ["utm_source","utm_medium","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 }; }
export const locales = ['en', 'fr', 'ar'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'en';
export const directionByLocale: Record<Locale, 'ltr' | 'rtl'> = { en: 'ltr', fr: 'ltr', ar: 'rtl' };
export const localeCookie = 'hpc-locale';
export const localeStorageKey = 'hpc.locale';
export const campaignQueryAllowlist = [
'utm_source',
'utm_medium',
'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 themeStorageKey = "hpc.theme.preference";
export const themePreferences = ["light","dark","system"] as const;
export type ThemePreference = typeof themePreferences[number];
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 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 };
export const themeCookie = 'hpc-theme';
export const themeStorageKey = 'hpc.theme.preference';
export const themePreferences = ['light', 'dark', 'system'] as const;
export type ThemePreference = (typeof themePreferences)[number];
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 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 =
| "page_view" | "section_view" | "navigation_click"
| "locale_change" | "theme_change" | "mobile_nav_open" | "mobile_nav_close"
| "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;
| 'page_view'
| 'section_view'
| 'navigation_click'
| 'locale_change'
| 'theme_change'
| 'mobile_nav_open'
| 'mobile_nav_close'
| '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 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 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>; }
export const fleetSizeValues = ['1-9', '10-24', '25-49', '50-99', '100-249', '250+'] as const;
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 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 cookieValue = cookieMatch ? decodeURIComponent(cookieMatch[1]) : null;
var stored = null;
try { stored = localStorage.getItem("hpc.theme.preference"); } catch (_) {}
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;
try {
stored = localStorage.getItem('hpc.theme.preference');
} catch (_) {}
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;
root.dataset.themePreference = preference;
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:
```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.
@@ -1,11 +1,7 @@
{
"version": "1.0",
"defaultLocale": "en",
"locales": [
"en",
"fr",
"ar"
],
"locales": ["en", "fr", "ar"],
"rootDetection": true,
"routes": [
{
@@ -16,15 +12,7 @@
"ar": ""
},
"indexable": true,
"hashes": [
"top",
"product",
"workflow",
"modules",
"pricing",
"faq",
"contact"
]
"hashes": ["top", "product", "workflow", "modules", "pricing", "faq", "contact"]
},
{
"id": "login",
@@ -68,22 +56,12 @@
}
],
"switching": {
"preserveQueryAllowlist": [
"utm_source",
"utm_medium",
"utm_campaign",
"utm_content",
"gclid"
],
"preserveQueryAllowlist": ["utm_source", "utm_medium", "utm_campaign", "utm_content", "gclid"],
"preserveValidHash": true,
"dropUnknownQuery": true
},
"hreflang": {
"values": [
"en",
"fr",
"ar"
],
"values": ["en", "fr", "ar"],
"xDefaultLocale": "en",
"selfCanonical": true
}
@@ -7,26 +7,11 @@
},
"header": {
"nav": [
[
"product",
"المنتج"
],
[
"workflow",
"سير العمل"
],
[
"modules",
"الوحدات"
],
[
"pricing",
"التسعير"
],
[
"faq",
"الأسئلة الشائعة"
]
["product", "المنتج"],
["workflow", "سير العمل"],
["modules", "الوحدات"],
["pricing", "التسعير"],
["faq", "الأسئلة الشائعة"]
],
"login": "تسجيل الدخول",
"demo": "احجز عرضًا توضيحيًا",
@@ -50,22 +35,10 @@
"title": "سير عمل واحد يغطي دورة التأجير كاملة.",
"body": "حافظ على ترابط سياق العميل والمركبة والعقد والمهام مع تقدم عملية التأجير.",
"items": [
[
"الحجز",
"سجّل الطلب والبيانات المطلوبة."
],
[
"المركبة",
"تابع التوافر والجاهزية."
],
[
"العقد",
"اجمع مهام التسليم والسجلات في موضع واحد."
],
[
"الإرجاع",
"سجّل الحالة وأغلق أعمال المتابعة."
]
["الحجز", "سجّل الطلب والبيانات المطلوبة."],
["المركبة", "تابع التوافر والجاهزية."],
["العقد", "اجمع مهام التسليم والسجلات في موضع واحد."],
["الإرجاع", "سجّل الحالة وأغلق أعمال المتابعة."]
]
},
"comparison": {
@@ -92,48 +65,20 @@
"title": "حرّك عملية التأجير إلى الأمام من دون فقدان السياق.",
"body": "توضح كل خطوة القرار والمسؤول والإجراء التالي بدل دفن التشغيل داخل قائمة عامة من الميزات.",
"steps": [
[
"01",
"تسجيل الحجز",
"سجّل التواريخ وبيانات السائق والمتطلبات والمصدر."
],
[
"02",
"تخصيص مركبة جاهزة",
"طابق التوافر مع الفئة والموقع وحالة الجاهزية."
],
[
"03",
"إكمال التسليم",
"تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."
],
[
"04",
"معالجة الإرجاع",
"سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."
]
["01", "تسجيل الحجز", "سجّل التواريخ وبيانات السائق والمتطلبات والمصدر."],
["02", "تخصيص مركبة جاهزة", "طابق التوافر مع الفئة والموقع وحالة الجاهزية."],
["03", "إكمال التسليم", "تحقق من المستندات والفحص والعقد ومهام تسليم المركبة."],
["04", "معالجة الإرجاع", "سجّل الحالة وأغلق الأعمال المفتوحة وأعد المركبة إلى التوافر."]
]
},
"roles": {
"eyebrow": "مصمم لفريق التشغيل",
"title": "امنح كل دور السياق الذي يحتاجه من دون إنشاء نسخ متعددة من الحقيقة.",
"roles": [
[
"المالك أو المدير",
"اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط."
],
[
"فريق الحجوزات",
"حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."
],
[
"الأسطول والتشغيل",
"اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."
],
[
"مكتب الاستقبال",
"حضّر مهام التسليم والإرجاع قبل انتظار العميل."
]
["المالك أو المدير", "اطّلع على عبء العمل والاستثناءات والمؤشرات التشغيلية عبر النشاط."],
["فريق الحجوزات", "حوّل الطلبات إلى حجوزات مؤكدة مع عدد أقل من عمليات التسليم."],
["الأسطول والتشغيل", "اعرف المركبات المتاحة أو المحجوبة أو المستحقة أو التي تنتظر إجراءً."],
["مكتب الاستقبال", "حضّر مهام التسليم والإرجاع قبل انتظار العميل."]
]
},
"modules": {
@@ -141,30 +86,12 @@
"title": "نواة مترابطة منظّمة حول أعمال التأجير الفعلية.",
"body": "توضح حدود الوحدات المسؤوليات، بينما تمنع السجلات المشتركة فوضى النسخ واللصق المعتادة.",
"items": [
[
"الحجوزات",
"التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد."
],
[
"جاهزية الأسطول",
"توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."
],
[
"عقود الإيجار",
"متطلبات التسليم وحالة العقد والسجلات التشغيلية."
],
[
"فحص المركبة",
"فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."
],
[
"سجلات العملاء",
"سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."
],
[
"التقارير التشغيلية",
"تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."
]
["الحجوزات", "التوافر والتواريخ ومتطلبات العميل والحالة ضمن سير واحد."],
["جاهزية الأسطول", "توافر المركبات والحجوزات التشغيلية وحالة الخدمة وسياق الموقع."],
["عقود الإيجار", "متطلبات التسليم وحالة العقد والسجلات التشغيلية."],
["فحص المركبة", "فحوص منظمة عند التسليم والإرجاع مع مهام متابعة."],
["سجلات العملاء", "سجل التأجير وبيانات الاتصال المرتبطة بالعمل الجاري."],
["التقارير التشغيلية", "تعريفات وعروض تشغيلية يمكن التحقق منها مقابل النشاط المصدر."]
]
},
"results": {
@@ -172,22 +99,10 @@
"title": "قِس التغيير، لا الخطاب التسويقي.",
"body": "لا ينبغي نشر أي نتيجة قبل اعتماد خط أساس حقيقي لعميل وفترة مقارنة وتعريف واضح للبيانات. لذلك تعرض هذه النسخة منهجية القياس بدل نسب مئوية مختلقة.",
"metrics": [
[
"زمن معالجة الحجز",
"من استلام طلب صالح للعمل حتى تأكيد الحجز."
],
[
"زمن إعادة المركبة إلى الجاهزية",
"من تسجيل الإرجاع حتى حالة جاهز للتأجير."
],
[
"معدل استخدام الأسطول",
"مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."
],
[
"متابعة حالات التأخير",
"عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."
]
["زمن معالجة الحجز", "من استلام طلب صالح للعمل حتى تأكيد الحجز."],
["زمن إعادة المركبة إلى الجاهزية", "من تسجيل الإرجاع حتى حالة جاهز للتأجير."],
["معدل استخدام الأسطول", "مقارنة أيام التوافر بأيام التأجير وفق تعريف واحد معتمد."],
["متابعة حالات التأخير", "عمليات التأجير المتأخرة المفتوحة مع مسؤول وإجراء تالٍ."]
],
"methodTitle": "المنهجية المطلوبة قبل نشر أي نتيجة",
"method": [
@@ -215,22 +130,10 @@
"title": "قيّم المنصة بالأدلة، لا بالشعارات.",
"body": "لا تُنشر شهادات أو وعود بمستوى الخدمة قبل وجود وثائق معتمدة. أثناء التقييم، اطلب إجابات محددة حول الضوابط المهمة لنشاطك.",
"checks": [
[
"التحكم في الوصول",
"الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم."
],
[
"قابلية التدقيق",
"التغييرات المسجلة والقابلة للبحث والتصدير."
],
[
"معالجة البيانات",
"الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."
],
[
"استمرارية العمل",
"النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."
]
["التحكم في الوصول", "الأدوار والصلاحيات وإجراءات المسؤول وإنهاء وصول المستخدم."],
["قابلية التدقيق", "التغييرات المسجلة والقابلة للبحث والتصدير."],
["معالجة البيانات", "الملكية والاحتفاظ والتصدير والحذف والجهات الفرعية."],
["استمرارية العمل", "النسخ الاحتياطي والاستعادة والتواصل عند الحوادث والدعم."]
]
},
"pricing": {
@@ -289,65 +192,32 @@
[
"المنتج",
[
[
"سير العمل",
"#workflow"
],
[
"الوحدات",
"#modules"
],
[
"التسعير",
"#pricing"
]
["سير العمل", "#workflow"],
["الوحدات", "#modules"],
["التسعير", "#pricing"]
]
],
[
"الشركة",
[
[
"تواصل مع المبيعات",
"#contact"
],
[
"احجز عرضًا توضيحيًا",
"#demo"
]
["تواصل مع المبيعات", "#contact"],
["احجز عرضًا توضيحيًا", "#demo"]
]
],
[
"الحساب",
[
[
"تسجيل الدخول",
"#login"
],
[
"اللغة",
"#language"
],
[
"المظهر",
"#theme"
]
["تسجيل الدخول", "#login"],
["اللغة", "#language"],
["المظهر", "#theme"]
]
],
[
"قانوني",
[
[
"الخصوصية",
"#!"
],
[
"الشروط",
"#!"
],
[
"إمكانية الوصول",
"#!"
]
["الخصوصية", "#!"],
["الشروط", "#!"],
["إمكانية الوصول", "#!"]
]
]
],
@@ -377,11 +247,7 @@
"أكثر من 500 مركبة",
"غير متأكد بعد"
],
"langOptions": [
"English",
"Français",
"العربية"
],
"langOptions": ["English", "Français", "العربية"],
"successTitle": "تم تسجيل طلب العرض",
"successBody": "اكتمل مسار الحجز في هذا النموذج الأولي. لم يُرسل أي طلب إلى نظام مبيعات فعلي.",
"submitAnother": "إرسال طلب آخر",
@@ -7,26 +7,11 @@
},
"header": {
"nav": [
[
"product",
"Product"
],
[
"workflow",
"Workflow"
],
[
"modules",
"Modules"
],
[
"pricing",
"Pricing"
],
[
"faq",
"FAQ"
]
["product", "Product"],
["workflow", "Workflow"],
["modules", "Modules"],
["pricing", "Pricing"],
["faq", "FAQ"]
],
"login": "Log in",
"demo": "Book a demo",
@@ -50,22 +35,10 @@
"title": "One workflow across the rental lifecycle.",
"body": "Keep the customer, vehicle, agreement, and task context connected as the rental moves forward.",
"items": [
[
"Reservation",
"Capture the request and required details."
],
[
"Vehicle",
"Track availability and readiness."
],
[
"Agreement",
"Keep pickup tasks and records together."
],
[
"Return",
"Record condition and close follow-up work."
]
["Reservation", "Capture the request and required details."],
["Vehicle", "Track availability and readiness."],
["Agreement", "Keep pickup tasks and records together."],
["Return", "Record condition and close follow-up work."]
]
},
"comparison": {
@@ -92,48 +65,23 @@
"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.",
"steps": [
[
"01",
"Capture the reservation",
"Record dates, driver details, requirements, and source."
],
[
"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."
]
["01", "Capture the reservation", "Record dates, driver details, requirements, and source."],
["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": {
"eyebrow": "Built for the operating team",
"title": "Give each role the context it needs, without creating separate versions of the truth.",
"roles": [
[
"Owner or manager",
"See workload, exceptions, and operating signals across the business."
],
[
"Reservations team",
"Move requests from inquiry to confirmed rental with fewer handoffs."
],
["Owner or manager", "See workload, exceptions, and operating signals across the business."],
["Reservations team", "Move requests from inquiry to confirmed rental with fewer handoffs."],
[
"Fleet and operations",
"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": {
@@ -141,26 +89,14 @@
"title": "A connected core, organized around real rental work.",
"body": "Module boundaries clarify responsibility while shared records prevent the usual copy-and-paste archaeology.",
"items": [
[
"Reservations",
"Availability, dates, customer requirements, and status in one flow."
],
[
"Fleet readiness",
"Vehicle availability, blocks, service state, and location context."
],
[
"Rental agreements",
"Pickup requirements, agreement status, and operational records."
],
["Reservations", "Availability, dates, customer requirements, and status in one flow."],
["Fleet readiness", "Vehicle availability, blocks, service state, and location context."],
["Rental agreements", "Pickup requirements, agreement status, and operational records."],
[
"Vehicle inspections",
"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",
"Definitions and operational views that can be validated against source activity."
@@ -172,22 +108,13 @@
"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.",
"metrics": [
[
"Reservation handling time",
"From usable request to confirmed reservation."
],
[
"Vehicle turnaround time",
"From return recorded to ready-for-rental status."
],
["Reservation handling time", "From usable request to confirmed reservation."],
["Vehicle turnaround time", "From return recorded to ready-for-rental status."],
[
"Fleet utilization",
"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",
"method": [
@@ -215,22 +142,10 @@
"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.",
"checks": [
[
"Access control",
"Roles, permissions, administrator actions, and offboarding."
],
[
"Auditability",
"Which changes are recorded, searchable, and exportable."
],
[
"Data handling",
"Ownership, retention, export, deletion, and subprocessors."
],
[
"Continuity",
"Backup, recovery, incident communication, and support process."
]
["Access control", "Roles, permissions, administrator actions, and offboarding."],
["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": {
@@ -289,65 +204,32 @@
[
"Product",
[
[
"Workflow",
"#workflow"
],
[
"Modules",
"#modules"
],
[
"Pricing",
"#pricing"
]
["Workflow", "#workflow"],
["Modules", "#modules"],
["Pricing", "#pricing"]
]
],
[
"Company",
[
[
"Contact sales",
"#contact"
],
[
"Book a demo",
"#demo"
]
["Contact sales", "#contact"],
["Book a demo", "#demo"]
]
],
[
"Account",
[
[
"Log in",
"#login"
],
[
"Language",
"#language"
],
[
"Theme",
"#theme"
]
["Log in", "#login"],
["Language", "#language"],
["Theme", "#theme"]
]
],
[
"Legal",
[
[
"Privacy",
"#!"
],
[
"Terms",
"#!"
],
[
"Accessibility",
"#!"
]
["Privacy", "#!"],
["Terms", "#!"],
["Accessibility", "#!"]
]
]
],
@@ -377,11 +259,7 @@
"More than 500 vehicles",
"Not sure yet"
],
"langOptions": [
"English",
"Français",
"العربية"
],
"langOptions": ["English", "Français", "العربية"],
"successTitle": "Demo request recorded",
"successBody": "This prototype has completed the booking flow. No request was sent to a real sales system.",
"submitAnother": "Submit another request",
@@ -7,26 +7,11 @@
},
"header": {
"nav": [
[
"product",
"Produit"
],
[
"workflow",
"Parcours"
],
[
"modules",
"Modules"
],
[
"pricing",
"Tarifs"
],
[
"faq",
"FAQ"
]
["product", "Produit"],
["workflow", "Parcours"],
["modules", "Modules"],
["pricing", "Tarifs"],
["faq", "FAQ"]
],
"login": "Se connecter",
"demo": "Réserver une démo",
@@ -50,22 +35,10 @@
"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.",
"items": [
[
"Réservation",
"Saisir la demande et les informations nécessaires."
],
[
"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."
]
["Réservation", "Saisir la demande et les informations nécessaires."],
["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": {
@@ -118,10 +91,7 @@
"eyebrow": "Conçu pour l’équipe opérationnelle",
"title": "Donnez à chaque rôle le contexte utile, sans multiplier les versions de la vérité.",
"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",
"Faire passer les demandes vers une location confirmée avec moins de transmissions."
@@ -130,10 +100,7 @@
"Parc et opérations",
"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": {
@@ -141,30 +108,15 @@
"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.",
"items": [
[
"Réservations",
"Disponibilités, dates, besoins du client et statut dans un même flux."
],
[
"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."
],
["Réservations", "Disponibilités, dates, besoins du client et statut dans un même flux."],
["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",
"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."
],
[
"Rapports opérationnels",
"Définitions et vues vérifiables par rapport à lactivité source."
]
["Dossiers clients", "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": {
@@ -176,18 +128,12 @@
"Temps de traitement des réservations",
"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",
"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",
"method": [
@@ -215,22 +161,10 @@
"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é.",
"checks": [
[
"Contrôle des accès",
"Rôles, autorisations, actions administrateur et départs."
],
[
"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."
]
["Contrôle des accès", "Rôles, autorisations, actions administrateur et départs."],
["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": {
@@ -289,65 +223,32 @@
[
"Produit",
[
[
"Parcours",
"#workflow"
],
[
"Modules",
"#modules"
],
[
"Tarifs",
"#pricing"
]
["Parcours", "#workflow"],
["Modules", "#modules"],
["Tarifs", "#pricing"]
]
],
[
"Entreprise",
[
[
"Contacter le service commercial",
"#contact"
],
[
"Réserver une démo",
"#demo"
]
["Contacter le service commercial", "#contact"],
["Réserver une démo", "#demo"]
]
],
[
"Compte",
[
[
"Se connecter",
"#login"
],
[
"Langue",
"#language"
],
[
"Thème",
"#theme"
]
["Se connecter", "#login"],
["Langue", "#language"],
["Thème", "#theme"]
]
],
[
"Mentions légales",
[
[
"Confidentialité",
"#!"
],
[
"Conditions",
"#!"
],
[
"Accessibilité",
"#!"
]
["Confidentialité", "#!"],
["Conditions", "#!"],
["Accessibilité", "#!"]
]
]
],
@@ -377,11 +278,7 @@
"Plus de 500 véhicules",
"Je ne sais pas encore"
],
"langOptions": [
"English",
"Français",
"العربية"
],
"langOptions": ["English", "Français", "العربية"],
"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.",
"submitAnother": "Envoyer une autre demande",
@@ -3,22 +3,77 @@ import path from 'node:path';
import crypto from 'node:crypto';
const root = path.resolve(path.dirname(new URL(import.meta.url).pathname), '..');
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',
'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'
'09_DEVELOPER_HANDOFF_v1.0.md',
'DECISION_LOG_v9.0.md',
'DEFINITION_OF_DONE_v1.0.md',
'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;
for (const file of required){ checks++; if(!fs.existsSync(path.join(root,file))) errors.push(`Missing ${file}`); }
const errors = [];
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'));
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}`);} }
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`));
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}`);
}
}
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]));
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');
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}`); } }
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));
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}`);
}
}
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",
"route": "/en",
"viewport": [
1440,
1000
],
"viewport": [1440, 1000],
"theme": "light",
"state": "homepage-top"
},
{
"id": "VR-002",
"route": "/fr",
"viewport": [
1024,
900
],
"viewport": [1024, 900],
"theme": "dark",
"state": "homepage-top"
},
{
"id": "VR-003",
"route": "/ar",
"viewport": [
390,
844
],
"viewport": [390, 844],
"theme": "light",
"state": "homepage-top"
},
{
"id": "VR-004",
"route": "/ar",
"viewport": [
390,
844
],
"viewport": [390, 844],
"theme": "dark",
"state": "drawer-open"
},
{
"id": "VR-005",
"route": "/en",
"viewport": [
390,
844
],
"viewport": [390, 844],
"theme": "light",
"state": "demo-invalid"
},
{
"id": "VR-006",
"route": "/fr",
"viewport": [
768,
1024
],
"viewport": [768, 1024],
"theme": "dark",
"state": "tour-step-3"
},
{
"id": "VR-007",
"route": "/ar",
"viewport": [
320,
800
],
"viewport": [320, 800],
"theme": "dark",
"state": "reflow-gate"
},
{
"id": "VR-008",
"route": "/en",
"viewport": [
1440,
1000
],
"viewport": [1440, 1000],
"theme": "dark",
"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
| 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-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 |
@@ -66,14 +66,14 @@
--duration-standard: 160ms;
--duration-overlay: 220ms;
--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-arabic: Noto Sans Arabic, Arial, sans-serif;
--breakpoint-medium: 768px;
--breakpoint-wide: 1120px;
}
html[data-theme="light"] {
html[data-theme='light'] {
--color-canvas: var(--color-gray-25);
--color-surface: #ffffff;
--color-surface-raised: #ffffff;
@@ -91,10 +91,10 @@ html[data-theme="light"] {
--color-on-inverse: #f8fafc;
--color-soft-blue: #eef5ff;
--color-soft-orange: #fff7ed;
--shadow-color: rgba(8,20,38,.12);
--shadow-color: rgba(8, 20, 38, 0.12);
color-scheme: light;
}
html[data-theme="dark"] {
html[data-theme='dark'] {
--color-canvas: var(--color-navy-950);
--color-surface: var(--color-navy-900);
--color-surface-raised: var(--color-navy-850);
@@ -112,11 +112,19 @@ html[data-theme="dark"] {
--color-on-inverse: var(--color-navy-950);
--color-soft-blue: #10284a;
--color-soft-orange: #3a2015;
--shadow-color: rgba(0,0,0,.36);
--shadow-color: rgba(0, 0, 0, 0.36);
color-scheme: dark;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
html {
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 themePreferences = ["light", "dark", "system"] as const;
export const themePreferences = ['light', 'dark', 'system'] as const;
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 motion = { fastMs: 120, standardMs: 160, overlayMs: 220 } as const;
@@ -10,7 +10,7 @@
## Findings
| Severity | Code | Path | Line | Finding |
|---|---|---|---:|---|
| -------- | ------------------------------------ | -------------------------------------------------- | ---: | --------------------------------------------------------------- |
| 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 | `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/image-types/global" />
import "./.next/dev/types/routes.d.ts";
import './.next/types/routes.d.ts';
// NOTE: This file should not be edited
// 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 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 excludedFiles = new Set(['PHASE_16_PACKAGE_MANIFEST_v1.0.json', 'package-lock.json', 'tsconfig.tsbuildinfo']);
const excludedDirectories = new Set([
'.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) {
const files = [];
@@ -26,7 +38,11 @@ const entries = [];
for (const file of files) {
const contents = await readFile(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 = {
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'),
'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.');
}
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';
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']);
async function walk(directory) {
@@ -22,25 +30,41 @@ async function walk(directory) {
const manifest = JSON.parse(await readFile(path.join(repositoryRoot, manifestName), 'utf8'));
const failures = [];
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16) failures.push('Phase 16 manifest identity 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.');
if (manifest.project !== 'RentalDriveGo' || manifest.phase !== 16)
failures.push('Phase 16 manifest identity 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();
for (const entry of manifest.files ?? []) {
if (typeof entry.path !== 'string' || path.isAbsolute(entry.path) || entry.path.split('/').includes('..')) {
failures.push(`Unsafe manifest path: ${String(entry.path)}`); continue;
if (
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);
try {
const filePath = path.join(repositoryRoot, entry.path);
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.`);
} catch { failures.push(`${entry.path}: file is missing.`); }
} catch {
failures.push(`${entry.path}: file is missing.`);
}
}
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 listed) if (!actual.has(file)) failures.push(`${file}: listed but excluded or absent.`);
for (const file of actual)
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);
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 = [];
for (const file of required) {
try { await access(path.join(repositoryRoot, file)); }
catch { failures.push(`Missing required Phase 16 artifact: ${file}`); }
try {
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 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.');
}
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 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);
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-overlay: 220ms;
--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-arabic: Noto Sans Arabic, Arial, sans-serif;
--breakpoint-medium: 768px;
--breakpoint-wide: 1120px;
}
html[data-theme="light"] {
html[data-theme='light'] {
--color-canvas: var(--color-gray-25);
--color-surface: #ffffff;
--color-surface-raised: #ffffff;
@@ -91,10 +91,10 @@ html[data-theme="light"] {
--color-on-inverse: #f8fafc;
--color-soft-blue: #eef5ff;
--color-soft-orange: #fff7ed;
--shadow-color: rgba(8,20,38,.12);
--shadow-color: rgba(8, 20, 38, 0.12);
color-scheme: light;
}
html[data-theme="dark"] {
html[data-theme='dark'] {
--color-canvas: var(--color-navy-950);
--color-surface: var(--color-navy-900);
--color-surface-raised: var(--color-navy-850);
@@ -112,11 +112,19 @@ html[data-theme="dark"] {
--color-on-inverse: var(--color-navy-950);
--color-soft-blue: #10284a;
--color-soft-orange: #3a2015;
--shadow-color: rgba(0,0,0,.36);
--shadow-color: rgba(0, 0, 0, 0.36);
color-scheme: dark;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
*, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
html {
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);
});
it.each([
null,
'',
'text/plain',
'application/jsonp',
'application/json-patch+json',
])('rejects %s', (value) => {
it.each([null, '', 'text/plain', 'application/jsonp', 'application/json-patch+json'])(
'rejects %s',
(value) => {
expect(isApplicationJson(value)).toBe(false);
});
},
);
});