diff --git a/apps/mapgen-studio/.interface-design/system.md b/apps/mapgen-studio/.interface-design/system.md index b07c498fae..c649788355 100644 --- a/apps/mapgen-studio/.interface-design/system.md +++ b/apps/mapgen-studio/.interface-design/system.md @@ -369,12 +369,15 @@ the trailing-disclosure placement; keeps the icon-only contract + zoning): `@container`; the signal chip's seed suffix rides a `@max-3xl:hidden` span so the chip collapses to `Turn N` before the bar wraps. Full `Turn N · Seed S` stays in the tooltip/accessible name. -- **Water proof section (ExplorePanel).** The river/lake/floodplain - inspector renders as a collapsible section between Step and Layers: lane - eyebrow per truth class, claim status as status-dot + word (`ready` / - `inspect` / `fail` / `open` / `skip`; projection evidence is steel - "inspect", NEVER success-green — projection masks don't masquerade as - engine truth), module-owned data-color dots on the layer-jump chips. +- **Water stats section (ExplorePanel).** The river/lake/floodplain data + renders as a STATS section between Step and Layers — one compact line per + data family carrying its semantic counts (plan vs engine, e.g. `lake + intent 32 · engine lakes 31`) plus layer-jump chips with module-owned + data-color dots. Divergence counts (mismatch/rejected) wear the warning + tint when nonzero; the collapsed header reads `matches baseline` or `N + mismatched`. PROOF VOCABULARY IS BANNED FROM PRODUCT CHROME: no claim + words, no lane eyebrows, no acceptance/rendered bookkeeping rows — claim + semantics stay in the `riverLakeInspector` module and project docs. - **Saved-config precedence shows "Custom".** Selection applies the saved config file EXACTLY (full replace); ANY divergence flips the selector value itself to a warning-tinted `Custom` entry with a `Re-apply` diff --git a/apps/mapgen-studio/src/app/StudioShell.tsx b/apps/mapgen-studio/src/app/StudioShell.tsx index e83e198b45..50f402ab95 100644 --- a/apps/mapgen-studio/src/app/StudioShell.tsx +++ b/apps/mapgen-studio/src/app/StudioShell.tsx @@ -242,8 +242,8 @@ export function StudioShell(props: StudioShellProps) { const setExploreStepExpanded = useViewStore((s) => s.setExploreStepExpanded); const exploreLayersExpanded = useViewStore((s) => s.exploreLayersExpanded); const setExploreLayersExpanded = useViewStore((s) => s.setExploreLayersExpanded); - const exploreWaterProofExpanded = useViewStore((s) => s.exploreWaterProofExpanded); - const setExploreWaterProofExpanded = useViewStore((s) => s.setExploreWaterProofExpanded); + const exploreWaterStatsExpanded = useViewStore((s) => s.exploreWaterStatsExpanded); + const setExploreWaterStatsExpanded = useViewStore((s) => s.setExploreWaterStatsExpanded); const stageView = useViewStore((s) => s.stageView); const setStageView = useViewStore((s) => s.setStageView); const pipelineSelectedStageId = useViewStore((s) => s.pipelineSelectedStageId); @@ -2148,12 +2148,12 @@ export function StudioShell(props: StudioShellProps) { !exploreStageExpanded && !exploreStepExpanded && !exploreLayersExpanded && - !exploreWaterProofExpanded; + !exploreWaterStatsExpanded; const next = rightCollapsed; setExploreStageExpanded(next); setExploreStepExpanded(next); setExploreLayersExpanded(next); - setExploreWaterProofExpanded(next); + setExploreWaterStatsExpanded(next); }, toggleLeftPanel: () => { const leftCollapsed = recipeSectionCollapsed && configSectionCollapsed; @@ -2437,8 +2437,8 @@ export function StudioShell(props: StudioShellProps) { onLayersExpandedChange={setExploreLayersExpanded} riverLakeInspectorSummary={riverLakeInspectorSummary} onRiverLakeInspectorLayerSelect={handleRiverLakeInspectorLayerSelect} - waterProofExpanded={exploreWaterProofExpanded} - onWaterProofExpandedChange={setExploreWaterProofExpanded} + waterStatsExpanded={exploreWaterStatsExpanded} + onWaterStatsExpandedChange={setExploreWaterStatsExpanded} /> ); diff --git a/apps/mapgen-studio/src/stores/viewStore.ts b/apps/mapgen-studio/src/stores/viewStore.ts index 077b6dfeb7..5bde2a4e53 100644 --- a/apps/mapgen-studio/src/stores/viewStore.ts +++ b/apps/mapgen-studio/src/stores/viewStore.ts @@ -47,7 +47,7 @@ export type ViewState = { exploreStageExpanded: boolean; exploreStepExpanded: boolean; exploreLayersExpanded: boolean; - exploreWaterProofExpanded: boolean; + exploreWaterStatsExpanded: boolean; // Explore selection (single owner; App no longer mirrors these) selectedStageId: string; selectedStepId: string; @@ -71,7 +71,7 @@ export type ViewState = { setExploreStageExpanded: (next: Updater) => void; setExploreStepExpanded: (next: Updater) => void; setExploreLayersExpanded: (next: Updater) => void; - setExploreWaterProofExpanded: (next: Updater) => void; + setExploreWaterStatsExpanded: (next: Updater) => void; setSelectedStageId: (next: Updater) => void; setSelectedStepId: (next: Updater) => void; setStageView: (next: Updater) => void; @@ -92,7 +92,7 @@ export const useViewStore = create((set) => ({ exploreStageExpanded: true, exploreStepExpanded: true, exploreLayersExpanded: true, - exploreWaterProofExpanded: false, + exploreWaterStatsExpanded: false, selectedStageId: "", selectedStepId: "", stageView: "map", @@ -118,8 +118,8 @@ export const useViewStore = create((set) => ({ set((s) => ({ exploreStepExpanded: resolve(next, s.exploreStepExpanded) })), setExploreLayersExpanded: (next) => set((s) => ({ exploreLayersExpanded: resolve(next, s.exploreLayersExpanded) })), - setExploreWaterProofExpanded: (next) => - set((s) => ({ exploreWaterProofExpanded: resolve(next, s.exploreWaterProofExpanded) })), + setExploreWaterStatsExpanded: (next) => + set((s) => ({ exploreWaterStatsExpanded: resolve(next, s.exploreWaterStatsExpanded) })), setSelectedStageId: (next) => set((s) => ({ selectedStageId: resolve(next, s.selectedStageId) })), setSelectedStepId: (next) => set((s) => ({ selectedStepId: resolve(next, s.selectedStepId) })), setStageView: (next) => set((s) => ({ stageView: resolve(next, s.stageView) })), diff --git a/apps/mapgen-studio/src/ui/components/ExplorePanel.tsx b/apps/mapgen-studio/src/ui/components/ExplorePanel.tsx index 8800f0abcc..60e3b06468 100644 --- a/apps/mapgen-studio/src/ui/components/ExplorePanel.tsx +++ b/apps/mapgen-studio/src/ui/components/ExplorePanel.tsx @@ -19,7 +19,7 @@ import { Flame } from 'lucide-react'; import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/ui'; -import { WaterProofSection } from './WaterProofSection'; +import { WaterStatsSection } from './WaterStatsSection'; import type { RiverLakeFloodplainInspectorSummary, RiverLakeInspectorLayerRef } from @@ -125,9 +125,9 @@ export interface ExplorePanelProps { /** Callback when a water-proof evidence layer chip is clicked */ onRiverLakeInspectorLayerSelect?: (ref: RiverLakeInspectorLayerRef) => void; /** Whether the water-proof section is expanded (optional controlled mode) */ - waterProofExpanded?: boolean; - /** Callback when waterProofExpanded changes (optional controlled mode) */ - onWaterProofExpandedChange?: (expanded: boolean) => void; + waterStatsExpanded?: boolean; + /** Callback when waterStatsExpanded changes (optional controlled mode) */ + onWaterStatsExpandedChange?: (expanded: boolean) => void; } // ============================================================================ // Component @@ -176,13 +176,13 @@ export const ExplorePanel: React.FC = ({ onLayersExpandedChange, riverLakeInspectorSummary, onRiverLakeInspectorLayerSelect, - waterProofExpanded: waterProofExpandedProp, - onWaterProofExpandedChange + waterStatsExpanded: waterStatsExpandedProp, + onWaterStatsExpandedChange }) => { const [localStageExpanded, setLocalStageExpanded] = useState(true); const [localStepExpanded, setLocalStepExpanded] = useState(true); const [localLayersExpanded, setLocalLayersExpanded] = useState(true); - const [localWaterProofExpanded, setLocalWaterProofExpanded] = useState(false); + const [localWaterStatsExpanded, setLocalWaterStatsExpanded] = useState(false); const [groupOpen, setGroupOpen] = useState>({}); const isStageExpanded = stageExpandedProp ?? localStageExpanded; @@ -203,10 +203,10 @@ export const ExplorePanel: React.FC = ({ if (layersExpandedProp === undefined) setLocalLayersExpanded(next); }; - const isWaterProofExpanded = waterProofExpandedProp ?? localWaterProofExpanded; - const setIsWaterProofExpanded = (next: boolean) => { - onWaterProofExpandedChange?.(next); - if (waterProofExpandedProp === undefined) setLocalWaterProofExpanded(next); + const isWaterStatsExpanded = waterStatsExpandedProp ?? localWaterStatsExpanded; + const setIsWaterStatsExpanded = (next: boolean) => { + onWaterStatsExpandedChange?.(next); + if (waterStatsExpandedProp === undefined) setLocalWaterStatsExpanded(next); }; const currentStage = stages.find((s) => s.value === selectedStage); const currentStep = steps.find((s) => s.value === selectedStep); @@ -460,11 +460,11 @@ export const ExplorePanel: React.FC = ({ ) : null} {/* WATER PROOF SECTION — River/Lake/Floodplain inspector rows */} - {/* 3. LAYERS SECTION. The debug toggle lives on this header — it filters diff --git a/apps/mapgen-studio/src/ui/components/WaterProofSection.tsx b/apps/mapgen-studio/src/ui/components/WaterProofSection.tsx deleted file mode 100644 index a19a72b2cf..0000000000 --- a/apps/mapgen-studio/src/ui/components/WaterProofSection.tsx +++ /dev/null @@ -1,216 +0,0 @@ -import React from 'react'; -// ============================================================================ -// WATER PROOF SECTION -// ============================================================================ -// River/Lake/Floodplain inspector rows inside the ExplorePanel dock. -// Presentational only — the semantic summary is built by -// features/viz/riverLakeInspector and passed in fully formed. -// ============================================================================ -import { ChevronDown, Droplets } from 'lucide-react'; -import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/ui'; -import type { - RiverLakeFloodplainInspectorSummary, - RiverLakeInspectorClaimStatus, - RiverLakeInspectorLayerRef } from -'../../features/viz/riverLakeInspector'; - -// ============================================================================ -// Props -// ============================================================================ -export interface WaterProofSectionProps { - /** Inspector summary built from the current viz manifest (null = no run). */ - summary: RiverLakeFloodplainInspectorSummary | null | undefined; - /** Jump the explore selection to an evidence layer. */ - onLayerSelect?: (ref: RiverLakeInspectorLayerRef) => void; - /** Whether the row list is expanded (controlled). */ - expanded: boolean; - /** Callback when the disclosure toggles. */ - onExpandedChange: (expanded: boolean) => void; -} - -// ============================================================================ -// Claim-status presentation (status-dot idiom, GameConsole vocabulary) -// ============================================================================ -// HARD RULE (anti-masquerade): status comes ONLY from `row.claimStatus` — the -// semantic module's verdict. Never derive status from layer presence (a row -// can have plenty of layers and still be unresolved; a debug mask being -// present proves nothing). The dot is aria-hidden; the WORD is the accessible -// signal. -const CLAIM_STATUS_DOT: Readonly> = { - pass: 'bg-success', - available: 'bg-primary', - fail: 'bg-destructive', - unresolved: 'bg-warning', - 'out-of-scope': 'bg-muted-foreground' -}; -const CLAIM_STATUS_WORD: Readonly> = { - pass: 'ready', - available: 'inspect', - fail: 'fail', - unresolved: 'open', - 'out-of-scope': 'skip' -}; - -const formatCountLabel = (key: string): string => { - switch (key) { - case 'layers': - return 'layers'; - case 'default': - return 'shown'; - case 'debug': - return 'debug'; - default: - return key; - } -}; - -const formatLayerButtonLabel = (ref: RiverLakeInspectorLayerRef): string => { - if (ref.dataTypeKey.includes('projectedRiverMask')) return 'projected'; - if (ref.dataTypeKey.includes('plannedMinorRiverMask')) return 'minor'; - if (ref.dataTypeKey.includes('plannedMajorRiverMask')) return 'major'; - if (ref.dataTypeKey.includes('engineRiverMask')) return 'terrain'; - if (ref.dataTypeKey.includes('Metadata')) return 'metadata'; - if (ref.dataTypeKey.includes('engineMinorRiverMask')) return 'minor meta'; - if (ref.dataTypeKey.includes('riverMismatchMask')) return 'mismatch'; - if (ref.dataTypeKey.includes('lakePlan')) return 'lake plan'; - if (ref.dataTypeKey.includes('plannedLakeMask')) return 'planned'; - if (ref.dataTypeKey.includes('engineLakeMask')) return 'engine'; - if (ref.dataTypeKey.includes('rejectedLakeMask')) return 'rejected'; - if (ref.dataTypeKey.includes('featureType')) return 'features'; - if (ref.dataTypeKey.includes('rejectionMask')) return 'rejects'; - const parts = ref.dataTypeKey.split('.'); - return parts[parts.length - 1] ?? ref.dataTypeKey; -}; - -// ============================================================================ -// Component -// ============================================================================ -/** - * Water-proof inspector: one row per truth-class claim about rivers, lakes, - * and floodplains, grouped into 8 lanes (Hydrology, Projection, Terrain, - * Metadata, Lakes, Floodplains, Rendered, Acceptance). Each lane answers a - * DIFFERENT question — "did Hydrology compute rivers?" is not "did the engine - * accept them?" is not "does the player see them?" — so a green check on one - * lane must never masquerade as proof of another. - * - * That is why projection-class evidence never renders success-green: a - * projection plan being PRESENT only makes it `available` (inspectable, - * `bg-primary`); `pass`/`bg-success` is reserved for claims the semantic - * module actually verified. Rows whose proof cannot come from Studio manifest - * layers at all (rendered Civ visibility, product acceptance) stay - * `unresolved`/`open` until same-run external evidence closes them. - */ -export const WaterProofSection: React.FC = ({ - summary, - onLayerSelect, - expanded, - onExpandedChange -}) => { - const rows = summary?.rows ?? []; - if (!summary || rows.length === 0) return null; - - const textSecondary = 'text-muted-foreground'; - const textMuted = 'text-muted-foreground/70'; - const borderSubtle = 'border-border-subtle'; - const hoverBg = 'hover:bg-accent'; - - const evidenceCount = rows.filter( - (row) => row.claimStatus === 'available' || row.claimStatus === 'pass' - ).length; - const openCount = rows.filter( - (row) => row.claimStatus === 'unresolved' || row.claimStatus === 'fail' - ).length; - const collapsedSummary = `${evidenceCount} evidence · ${openCount} open`; - - return ( - <> -
- -
- {expanded ? ( -
- {rows.map((row) => ( -
-
-
-
- {row.laneLabel} -
-
- {row.label} -
-
- - -
-
- {Object.entries(row.counts).map(([key, value]) => ( - - {formatCountLabel(key)} {value} - - ))} - {row.layerRefs.slice(0, 4).map((ref) => { - const refTitle = `${ref.label} · ${ref.presentation.categoryLabel} · ${row.proofClass}`; - return ( - - - - - {refTitle} - - ); - })} -
-
- ))} -
- ) : null} - ); - -}; diff --git a/apps/mapgen-studio/src/ui/components/WaterStatsSection.tsx b/apps/mapgen-studio/src/ui/components/WaterStatsSection.tsx new file mode 100644 index 0000000000..910a86b8f4 --- /dev/null +++ b/apps/mapgen-studio/src/ui/components/WaterStatsSection.tsx @@ -0,0 +1,186 @@ +import React from 'react'; +// ============================================================================ +// WATER STATS SECTION +// ============================================================================ +// Hydrology/lake/floodplain run statistics inside the ExplorePanel dock. +// Presentational only — the numbers come from the riverLakeInspector summary +// (manifest-derived counts). This is a STATS surface, not a proof ledger: +// claim/acceptance bookkeeping lives in the semantic module and the project +// docs, never in product chrome. What earns a row here is information — how +// the run compares to its baseline (plan vs engine counts, mismatches) plus +// jump-to-layer chips for the underlying evidence. +// ============================================================================ +import { ChevronDown, Droplets } from 'lucide-react'; +import { Tooltip, TooltipContent, TooltipTrigger } from '../../components/ui'; +import type { + RiverLakeFloodplainInspectorSummary, + RiverLakeInspectorLayerRef } from +'../../features/viz/riverLakeInspector'; + +// ============================================================================ +// Props +// ============================================================================ +export interface WaterStatsSectionProps { + /** Inspector summary built from the current viz manifest (null = no run). */ + summary: RiverLakeFloodplainInspectorSummary | null | undefined; + /** Jump the explore selection to an evidence layer. */ + onLayerSelect?: (ref: RiverLakeInspectorLayerRef) => void; + /** Whether the row list is expanded (controlled). */ + expanded: boolean; + /** Callback when the disclosure toggles. */ + onExpandedChange: (expanded: boolean) => void; +} + +// Inventory keys ('how many layers exist / are shown / are debug') are +// manifest bookkeeping, not run information — only semantic counts render. +const INVENTORY_COUNT_KEYS = new Set(['layers', 'default', 'debug']); + +/** + * Baseline-matching emphasis: counts that MEASURE divergence from the plan + * (mismatches, rejections) are the signal this section exists for — nonzero + * means the run diverged from its baseline and warrants a look. + */ +const isDivergenceCount = (key: string): boolean => /mismatch|reject|drift/i.test(key); + +const formatLayerButtonLabel = (ref: RiverLakeInspectorLayerRef): string => { + if (ref.dataTypeKey.includes('projectedRiverMask')) return 'projected'; + if (ref.dataTypeKey.includes('plannedMinorRiverMask')) return 'minor'; + if (ref.dataTypeKey.includes('plannedMajorRiverMask')) return 'major'; + if (ref.dataTypeKey.includes('engineRiverMask')) return 'terrain'; + if (ref.dataTypeKey.includes('Metadata')) return 'metadata'; + if (ref.dataTypeKey.includes('engineMinorRiverMask')) return 'minor meta'; + if (ref.dataTypeKey.includes('riverMismatchMask')) return 'mismatch'; + if (ref.dataTypeKey.includes('lakePlan')) return 'lake plan'; + if (ref.dataTypeKey.includes('plannedLakeMask')) return 'planned'; + if (ref.dataTypeKey.includes('engineLakeMask')) return 'engine'; + if (ref.dataTypeKey.includes('rejectedLakeMask')) return 'rejected'; + if (ref.dataTypeKey.includes('featureType')) return 'features'; + if (ref.dataTypeKey.includes('rejectionMask')) return 'rejects'; + const parts = ref.dataTypeKey.split('.'); + return parts[parts.length - 1] ?? ref.dataTypeKey; +}; + +// ============================================================================ +// Component +// ============================================================================ +/** + * Water stats: one compact line per hydrology/lake/floodplain data family, + * showing the run's semantic counts (rivers planned, engine readbacks, lakes + * intent vs engine, floodplains applied/rejected) with divergence counts + * (mismatch/rejected) emphasized when nonzero — that is the at-a-glance + * "does this run match its baseline?" signal. Each line carries jump chips + * to the evidence layers (module-owned data colors). Rows with no counts and + * no layers (closure bookkeeping the manifest cannot inform) do not render. + */ +export const WaterStatsSection: React.FC = ({ + summary, + onLayerSelect, + expanded, + onExpandedChange +}) => { + const allRows = summary?.rows ?? []; + const rows = allRows + .map((row) => ({ + rowKey: row.rowKey, + label: row.label, + counts: Object.entries(row.counts).filter(([key]) => !INVENTORY_COUNT_KEYS.has(key)), + layerRefs: row.layerRefs + })) + .filter((row) => row.counts.length > 0 || row.layerRefs.length > 0); + if (rows.length === 0) return null; + + const textSecondary = 'text-muted-foreground'; + const textMuted = 'text-muted-foreground/70'; + const borderSubtle = 'border-border-subtle'; + const hoverBg = 'hover:bg-accent'; + + const divergenceTotal = rows.reduce( + (total, row) => + total + row.counts.reduce((sum, [key, value]) => (isDivergenceCount(key) ? sum + value : sum), 0), + 0 + ); + const collapsedSummary = divergenceTotal > 0 ? `${divergenceTotal} mismatched` : 'matches baseline'; + + return ( + <> +
+ +
+ {expanded ? ( +
+ {rows.map((row) => ( +
+ {row.label} + {row.counts.map(([key, value]) => { + const diverged = isDivergenceCount(key) && value > 0; + return ( + + {key} {value} + + ); + })} + {row.layerRefs.slice(0, 4).map((ref) => { + const refTitle = `${ref.label} · ${ref.presentation.categoryLabel}`; + return ( + + + + + {refTitle} + + ); + })} +
+ ))} +
+ ) : null} + ); + +}; diff --git a/apps/mapgen-studio/src/ui/components/index.ts b/apps/mapgen-studio/src/ui/components/index.ts index ada16d132c..349aa85b46 100644 --- a/apps/mapgen-studio/src/ui/components/index.ts +++ b/apps/mapgen-studio/src/ui/components/index.ts @@ -13,7 +13,7 @@ export { GameConsole, type GameConsoleProps, type GameConsoleLiveRuntime } from // Panel components export { RecipePanel, type RecipePanelProps } from './RecipePanel'; export { ExplorePanel, type ExplorePanelProps } from './ExplorePanel'; -export { WaterProofSection, type WaterProofSectionProps } from './WaterProofSection'; +export { WaterStatsSection, type WaterStatsSectionProps } from './WaterStatsSection'; // Form components export { ViewControls, type ViewControlsProps } from './ViewControls'; diff --git a/apps/mapgen-studio/test/viz/waterProofSection.test.tsx b/apps/mapgen-studio/test/viz/waterStatsSection.test.tsx similarity index 51% rename from apps/mapgen-studio/test/viz/waterProofSection.test.tsx rename to apps/mapgen-studio/test/viz/waterStatsSection.test.tsx index ca350009cb..410c8272c3 100644 --- a/apps/mapgen-studio/test/viz/waterProofSection.test.tsx +++ b/apps/mapgen-studio/test/viz/waterStatsSection.test.tsx @@ -1,20 +1,19 @@ import { renderToStaticMarkup } from "react-dom/server"; import { describe, expect, it, vi } from "vitest"; -import { WaterProofSection, type WaterProofSectionProps } from "../../src/ui/components/WaterProofSection"; +import { WaterStatsSection, type WaterStatsSectionProps } from "../../src/ui/components/WaterStatsSection"; import { TooltipProvider } from "../../src/components/ui/tooltip"; import type { RiverLakeFloodplainInspectorSummary, - RiverLakeInspectorClaimStatus, RiverLakeInspectorLayerRef, RiverLakeInspectorRow, } from "../../src/features/viz/riverLakeInspector"; -// The water-proof section is the redesigned ExplorePanel's home for the -// River/Lake/Floodplain inspector. These pins guard the anti-masquerade rule: -// the status WORD + dot derive ONLY from `row.claimStatus`, never from layer -// presence, and `available` (evidence present, unverified) must never wear -// success-green. +// Water stats pins: the ExplorePanel surface is a STATS section — semantic +// counts (plan vs engine, divergence emphasis) plus layer-jump chips. Proof +// vocabulary (claim words, lane eyebrows, acceptance rows) must NOT render: +// claim bookkeeping lives in the semantic module and project docs, not in +// product chrome. function makeRef(overrides: Partial = {}): RiverLakeInspectorLayerRef { return { @@ -66,10 +65,10 @@ function makeSummary(rows: readonly RiverLakeInspectorRow[]): RiverLakeFloodplai return { version: 1, rows }; } -function renderSection(overrides: Partial = {}) { +function renderSection(overrides: Partial = {}) { return renderToStaticMarkup( - = {}) { ); } -function singleStatusRow(claimStatus: RiverLakeInspectorClaimStatus) { - return renderSection({ summary: makeSummary([makeRow({ claimStatus })]) }); -} - -describe("WaterProofSection", () => { - it("renders the lane eyebrow and row label for each row", () => { +describe("WaterStatsSection (water stats)", () => { + it("renders one stats line per data family with its semantic counts", () => { const html = renderSection({ summary: makeSummary([ makeRow(), makeRow({ rowKey: "hydrology-truth", - lane: "hydrology", - laneLabel: "Hydrology", label: "Drainage truth", - proofClass: "hydrology-truth", + counts: { layers: 3, rivers: 562 }, + layerRefs: [], }), ]), }); - expect(html).toContain("Projection"); expect(html).toContain("Navigable river plan"); - expect(html).toContain("Hydrology"); + expect(html).toContain("projected 12"); expect(html).toContain("Drainage truth"); + expect(html).toContain("rivers 562"); }); - it("renders available as the word inspect and never masquerades as success", () => { - const html = singleStatusRow("available"); - expect(html).toContain("inspect"); - // Anti-masquerade pin: evidence being PRESENT is not a verified pass. - expect(html).not.toContain("bg-success"); + it("drops manifest-inventory counts (layers/shown/debug) from the chips", () => { + const html = renderSection(); + expect(html).not.toContain("layers 1"); }); - it("renders unresolved as open with the warning dot", () => { - const html = singleStatusRow("unresolved"); - expect(html).toContain("open"); - expect(html).toContain("bg-warning"); + it("renders no proof vocabulary — claim words, lane eyebrows, status dots are gone", () => { + const html = renderSection({ + summary: makeSummary([ + makeRow({ claimStatus: "available" }), + makeRow({ rowKey: "x", claimStatus: "unresolved", counts: { terrain: 9 } }), + ]), + }); + + expect(html).not.toContain("inspect"); + expect(html).not.toContain(">open<"); + expect(html).not.toContain(">ready<"); + expect(html).not.toContain("bg-success"); + expect(html).not.toContain("bg-destructive"); }); - it("renders fail with the destructive dot", () => { - const html = singleStatusRow("fail"); - expect(html).toContain(">fail<"); - expect(html).toContain("bg-destructive"); + it("hides bookkeeping rows that carry no counts and no layers", () => { + const html = renderSection({ + summary: makeSummary([ + makeRow(), + makeRow({ rowKey: "acceptance", label: "Product closure", counts: {}, layerRefs: [] }), + ]), + }); + + expect(html).not.toContain("Product closure"); }); - it("renders pass as ready with the success dot", () => { - const html = singleStatusRow("pass"); - expect(html).toContain("ready"); - expect(html).toContain("bg-success"); + it("emphasizes nonzero divergence counts and reports them in the collapsed summary", () => { + const diverged = renderSection({ + expanded: false, + summary: makeSummary([makeRow({ counts: { mismatch: 3, terrain: 9 } })]), + }); + expect(diverged).toContain("3 mismatched"); + expect(diverged).toContain("text-warning"); + + const clean = renderSection({ + expanded: false, + summary: makeSummary([makeRow({ counts: { mismatch: 0, terrain: 9 } })]), + }); + expect(clean).toContain("matches baseline"); }); - it("renders nothing when the summary is null or empty", () => { + it("renders nothing when the summary is null or has no informative rows", () => { expect(renderSection({ summary: null })).toBe(""); expect(renderSection({ summary: makeSummary([]) })).toBe(""); + expect( + renderSection({ summary: makeSummary([makeRow({ counts: {}, layerRefs: [] })]) }) + ).toBe(""); }); - it("hides the row list when collapsed but keeps the header inline summary", () => { - const html = renderSection({ - expanded: false, - summary: makeSummary([ - makeRow(), - makeRow({ rowKey: "civ-rendered", laneLabel: "Rendered", label: "In-game visible rivers", claimStatus: "unresolved" }), - ]), - }); + it("hides the stats list when collapsed", () => { + const html = renderSection({ expanded: false }); - expect(html).toContain("Water proof"); - expect(html).toContain("1 evidence · 1 open"); - // The header keeps aria-controls pointing at the list id; the list element - // itself (and its rows) must be gone when collapsed. - expect(html).not.toContain('id="explore-water-proof-list"'); + expect(html).toContain("Water stats"); + expect(html).not.toContain('id="explore-water-stats-list"'); expect(html).not.toContain("Navigable river plan"); }); it("gives layer chips an accessible name carrying the category label", () => { const html = renderSection(); - expect(html).toContain( - 'aria-label="Projected river mask · Projection plan · projection-plan"' - ); + expect(html).toContain('aria-label="Projected river mask · Projection plan"'); }); }); diff --git a/docs/projects/mapgen-studio-redesign/00-GOAL.md b/docs/projects/mapgen-studio-redesign/00-GOAL.md index 5b81f4d80a..c24a010040 100644 --- a/docs/projects/mapgen-studio-redesign/00-GOAL.md +++ b/docs/projects/mapgen-studio-redesign/00-GOAL.md @@ -595,3 +595,78 @@ mod-swooper-maps **569/0 fail** (= handoff baseline); civ7-direct-control rebuild proved it, main-worktree control run cross-checked); mapgen-core **103/0 fail**; studio-server **2** (main's handler pins). Deferred to the operator: draining the stack (standing never-submit rule vs handoff step 5). + +## P7 deep semantic review (2026-06-12, operator-mandated) — COMPLETE + +The post-restack review-and-fix wave: a 5-lane framed subagent review +(config-precedence evidence, dead-ends, semantic-loss-vs-main, +inspector design, idiom/conformance) over everything the restack merged, +plus mid-flight operator additions. Nine slices stacked on +`design/post-main-restack-reconcile`: + +- [x] **Reconcile amend — contract regressions (P1×2).** The restack had + kept pre-rivers studio-server contracts while the ported engines emit + rivers shapes: `mapConfigs` deploy details (nested + `{build,targetDir,modsDir,filesCopied}`) and `runInGame` content-proof + fields (`contentMarkerProof`/`fileContentProof` + local/deployed script + content) restored from main, amended into the reconcile commit. +- [x] **`design/p7-config-precedence-custom` — the "big big big" item.** + Root cause at the engine seam: `prepareCiv7SinglePlayerSetup` loads the + saved config file FIRST then re-applies every studio key on top, so any + unauthored studio key silently overrides the file. Selection is now + exact-replace (`studioSetupConfigFromSavedConfigFile`, previously + production-dead), drift is total-equality, and the selector shows + **Custom** whenever the launched state differs from the file + ("Re-apply" restores it). Proven live: launch payload carried EXACTLY + the file's options (vs the prior 22-key default wall captured in the + wild), persistence wipe included. +- [x] **`design/p7-water-proof-inspector`** — river/lake/floodplain + inspector summary surfaced in the Explore dock with layer-jump chips — + superseded in-stack by `design/p7-water-stats` after operator review. +- [x] **`design/p7-explore-wiring`** — the Explore button wired for real: + the mounted control-oRPC router reached through the typed + `LiveControlPort.display.explore` seam (the gap was client-side only); + three-state affordance + result toasts; live-proven (full-map reveal). +- [x] **`design/p7-idiom-jsdoc-polish`** — naming/JSDoc pass over the + restacked surface (why-comments on the precedence seam, port taxonomy, + setup-config type). +- [x] **`design/p7-responsive-status-chip`** — turn/seed chip drops the + seed suffix under a Tailwind v4 container query when the bar narrows. +- [x] **`design/p7-flat-config-explorer`** — config objects flattened + into a flush nested disclosure explorer: wells retired, scalar runs and + section rows divided by hairlines, depth read via compounding `pl-3` + indent + heading tiers; array items as hairline rows. +- [x] **`design/p7-orpc-native-errors` — categorical wrapper kill.** + Operator directive: any error mapping/extraction around oRPC is an + architectural flaw. Investigation (native docs + effect-orpc source) + then migration: per-procedure `.errors()` maps pin the legacy + non-uniform statuses on DECLARED codes; the host context maps engine + `RunInGameHttpError`s to matching raw `ORPCError`s that arrive as + DEFINED errors; clients read `safe()` + `isDefinedError()` and branch + on codes. Deleted outright: `orpcError`/`statusToCode`/ + `rethrowEngineError`/`orpcFailure`/`runInGameFailure`/ + `saveDeployFailure`/`readErrorData`/`StudioServerOrpcFailure`/ + `recipeDagSafeErrorMiddleware`. **Deferred follow-up:** migrate the + ~18 engine `RunInGameHttpError` throw sites to contract errors so the + host mapping shim can die too. +- [x] **`design/p7-water-stats`** — operator verdict on the proof UI: + "slop". Re-expressed as a stats section: semantic counts per data + family, divergence (mismatch/reject) warning-tinted, collapsed summary + "matches baseline"/"N mismatched"; proof vocabulary BANNED from product + chrome (system.md amendment) — claim semantics stay in the + riverLakeInspector module. + +Dead-ends sweep closed: Explore wired, the dead selection helper now the +production path, all legacy error wrappers deleted; no dead routes — the +control-oRPC mount serves canonical procedures only. + +Gates at tip (all green): mapgen-studio **239/50** (canonical +`bun run test`) + tsc + build; studio-server build/check/**5** tests; +mod-swooper-maps **569**; direct-control **433**; control-orpc **345**; +mapgen-core **103**. Live proofs: config-precedence launch payload, +Explore reveal, water stats over a real run (surfaced a genuine +32-intent vs 31-engine lake delta at a glance). + +Submission: operator LIFTED the never-submit rule for this phase — +`gt submit --stack --ai` + merge drain per the repo-local +graphite-stack-drain skill.