From 86091cbf67bc42be868719b1f067731128c98c09 Mon Sep 17 00:00:00 2001 From: Travis Gilbert <1travisgilbert@gmail.com> Date: Tue, 4 Aug 2026 10:31:44 -0400 Subject: [PATCH] Make Data-model a full-bleed OWOX canvas with adapter chrome only. Retire plan-id BlockShell, inspector, and fields/records lenses from ModelView so /Data-model is ModelCanvasShell edge-to-edge with a floating OKF/Propose/Diff/Restore strip; align the e2e proportions oracle. --- apps/console/e2e/console-sidebar.spec.ts | 4 +- apps/console/src/app/Data-model/page.tsx | 4 +- apps/console/src/views/model/ModelView.tsx | 704 +++++------------- .../views/model/ObservedDeclaredLenses.tsx | 8 +- .../views/model/diagram/ForkDiagramCanvas.tsx | 101 +-- .../EXECUTE-REPORT.md | 2 +- 6 files changed, 252 insertions(+), 571 deletions(-) diff --git a/apps/console/e2e/console-sidebar.spec.ts b/apps/console/e2e/console-sidebar.spec.ts index 9e620fb9..df523482 100644 --- a/apps/console/e2e/console-sidebar.spec.ts +++ b/apps/console/e2e/console-sidebar.spec.ts @@ -123,7 +123,9 @@ test.describe('Console sidebar', () => { await expect(page.getByTestId('model-canvas-shell')).toBeVisible({ timeout: 120_000, }); - await expect(page.locator('[data-model-inspector]')).toHaveCSS('width', '320px'); + // Full-bleed OWOX body: no plan-id inspector rail; canvas fills the well. + await expect(page.locator('[data-model-inspector]')).toHaveCount(0); + await expect(page.locator('[data-model-canvas-page]')).toBeVisible(); }); test('Program route mounts the canonical Program Canvas surface', async ({ page }) => { diff --git a/apps/console/src/app/Data-model/page.tsx b/apps/console/src/app/Data-model/page.tsx index a73e21aa..e81d5137 100644 --- a/apps/console/src/app/Data-model/page.tsx +++ b/apps/console/src/app/Data-model/page.tsx @@ -1,6 +1,6 @@ // SOURCING: none. Canonical App Router segment for the Data model surface. -// Server-stamp the register impl so the cutover doctor can observe it without -// waiting for client hydration of ModelView / ForkDiagramCanvas. +// Server-stamp the register impl for the cutover doctor. The page body is the +// console surface with model.studio โ†’ ModelCanvasShell (OWOX) full-bleed. import ConsoleSurfacePage from '@/lib/console-surface-page'; diff --git a/apps/console/src/views/model/ModelView.tsx b/apps/console/src/views/model/ModelView.tsx index a9957142..2759633a 100644 --- a/apps/console/src/views/model/ModelView.tsx +++ b/apps/console/src/views/model/ModelView.tsx @@ -1,50 +1,36 @@ 'use client'; -// SOURCING: @commonplace/block-view for scope and mutation seams, -// @xyflow/react and tablecn structure through the registered lens components. +// SOURCING: @commonplace/model-canvas (OWOX hard fork) as the Data-model page +// body. Registry read/write stays in this adapter; plan-id BlockShell chrome +// is gone โ€” the canvas is the page (SPEC-COMMONPLACE-MODEL-CANVAS-FORK-1.0). import { useCallback, useEffect, useReducer, useRef, useState, type FormEvent } from 'react'; import type { ViewRenderProps } from '@commonplace/block-view/types'; import { emptyDeclaredModel, emptyObservedModel, - formatFieldType, - type DeclaredModel, - type FieldMetadata, - type FieldType, - type ObservedEdge, - type ObservedField, type PinKind, type SchemaProposalDraft, - type ScopeRef, } from '@commonplace/data-model-contracts'; import { DiffDialog, diffGraphs, type ModelGraph } from '@commonplace/model-canvas'; -import { BlockShell } from '@/components/block/BlockShell'; -import { degradationFor, withAction } from '@/lib/degradation'; +import '@commonplace/model-canvas/canvas.css'; import { exportOkfModel, fetchObservedModel, importOkfModel, postPin, - postSchemaDeclare, postSchemaProposal, postSchemaRestore, postUnpin, previewOkfModel, type OkfModelPreviewPayload, } from '@/lib/observed-model-client'; -import { WhyTrace } from '../harness-ux/WhyTracePanel'; -import { - DiagramLens, - FieldsTableLens, -} from './ObservedDeclaredLenses'; -import { RecordsLens } from './RecordsLens'; +import { DiagramLens } from './ObservedDeclaredLenses'; import type { LayoutPositions } from './diagram/layout'; import { createModelQueryState, modelScopeFromSet, reduceModelQuery, - type ModelLens, type ModelSelection, } from './modelQuery'; import { modelCanvasId } from '@/lib/canvas/store'; @@ -53,7 +39,6 @@ import { declaredToModelGraph, parseOkfBundle, } from './okfBridge'; -import { schemaDeclareInputForField } from './schemaDeclare'; import { UNKNOWN_REGISTRY_SIGNAL, registryMoved, @@ -100,21 +85,6 @@ function positionsFromLayoutDocument(document: JSONCanvas | null): LayoutPositio return positions; } -function selectedObservedEvidence( - selection: ModelSelection | null, - observed: ReturnType, -): ObservedField | ObservedEdge | null { - if (selection?.kind === 'observed-field') { - return observed.types.flatMap((type) => type.fields) - .find((field) => field.observedKey === selection.key) ?? null; - } - if (selection?.kind === 'observed-edge') { - return observed.types.flatMap((type) => type.edges) - .find((edge) => edge.observedKey === selection.key) ?? null; - } - return null; -} - function ProposalCard({ draft, busy, @@ -167,246 +137,6 @@ function ProposalCard({ ); } -function ModelInspector({ - selection, - observed, - declared, - fieldEditBusy, - fieldEditError, - onFieldEdit, -}: { - readonly selection: ModelSelection | null; - readonly observed: ReturnType; - readonly declared: DeclaredModel; - readonly fieldEditBusy: boolean; - readonly fieldEditError: string | null; - readonly onFieldEdit: (fieldId: string, replacement: FieldMetadata) => void; -}) { - const evidence = selectedObservedEvidence(selection, observed); - const declaredField = selection?.kind === 'declared-field' - ? declared.fields.find((field) => field.id === selection.key) ?? null - : null; - const observedOrigin = declaredField?.provenance - ? observed.types.flatMap((type) => type.fields) - .find((field) => field.observedKey === declaredField.provenance?.observedKey) - : null; - const whyNodeId = declaredField?.provenance?.nodeId - ?? observedOrigin?.provenanceNodeId - ?? observedOrigin?.eventIds?.[0]; - const evidenceSources = evidence?.sourceRefs?.length - ? evidence.sourceRefs - : observed.sources; - const [fieldKey, setFieldKey] = useState(declaredField?.key ?? ''); - const [fieldLabel, setFieldLabel] = useState(declaredField?.label ?? ''); - const [fieldKind, setFieldKind] = useState(declaredField?.fieldType.kind ?? 'text'); - const [fieldRequired, setFieldRequired] = useState(declaredField?.required ?? false); - - // The initialisers above run once, at mount, which normally happens with - // nothing selected. Without this reset the editor opened empty on the first - // selection and kept field A's key, label, type and required flag after - // moving to field B, so submitting could redeclare B with A's values. - // - // Adjusted during render rather than in an effect: an effect would paint the - // stale draft first and then cascade a second render. Guarded by the field - // identity so it never fights the reader's typing. - const editedFieldId = declaredField?.id ?? null; - const [editorFieldId, setEditorFieldId] = useState(editedFieldId); - if (editorFieldId !== editedFieldId) { - setEditorFieldId(editedFieldId); - setFieldKey(declaredField?.key ?? ''); - setFieldLabel(declaredField?.label ?? ''); - setFieldKind(declaredField?.fieldType.kind ?? 'text'); - setFieldRequired(declaredField?.required ?? false); - } - - function editedFieldType(current: FieldType, kind: string): FieldType { - if (current.kind === kind) return current; - switch (kind) { - case 'long_text': - return { kind: 'long_text' }; - case 'integer': - return { kind: 'integer' }; - case 'number': - return { kind: 'number' }; - case 'boolean': - return { kind: 'boolean' }; - case 'timestamp': - return { kind: 'timestamp' }; - case 'date': - return { kind: 'date' }; - case 'uuid': - return { kind: 'uuid' }; - case 'json': - return { kind: 'json' }; - case 'geometry': - return { kind: 'geometry' }; - default: - return { kind: 'text' }; - } - } - - return ( - - ); -} - -const LENSES: readonly ModelLens[] = ['diagram', 'fields', 'records']; const LAYOUT_PERSIST_MS = 400; /** Fallback heartbeat for registry changes made outside this client. */ const REGISTRY_SIGNAL_MS = 15_000; @@ -436,8 +166,6 @@ export function ModelView({ set, host }: ViewRenderProps) { const [okfPreview, setOkfPreview] = useState(null); const [diffVersionIds, setDiffVersionIds] = useState(['', '']); const [diffOpen, setDiffOpen] = useState(false); - const [fieldEditBusy, setFieldEditBusy] = useState(false); - const [fieldEditError, setFieldEditError] = useState(null); const setScope = modelScopeFromSet(set); const layoutHost = host as ModelLayoutHost; const setScopeTopicId = setScope?.kind === 'topic' ? setScope.topicId : ''; @@ -668,30 +396,6 @@ export function ModelView({ set, host }: ViewRenderProps) { } } - async function applyFieldEdit( - fieldId: string, - replacement: FieldMetadata, - ): Promise { - if (!topicId) return; - setFieldEditBusy(true); - setFieldEditError(null); - try { - const input = schemaDeclareInputForField(declared, fieldId, replacement); - const result = await postSchemaDeclare(topicId, input, host); - setDeclared(result.declared); - setNotice( - result.receipt.idempotentReplay - ? 'Declaration already matches the registry.' - : `Declared ${replacement.label}.`, - ); - setReloadToken((token) => token + 1); - } catch (editError) { - setFieldEditError(editError instanceof Error ? editError.message : String(editError)); - } finally { - setFieldEditBusy(false); - } - } - async function requestProposal(event: FormEvent): Promise { event.preventDefault(); const request = proposalRequest.trim(); @@ -836,7 +540,6 @@ export function ModelView({ set, host }: ViewRenderProps) { selection: queryState.selection, pendingPins: queryState.pendingPins, onSelect: (selection: ModelSelection | null) => { - setFieldEditError(null); dispatch({ type: 'select', selection }); }, onPin: (observedKey: string, kind: PinKind, parentObservedKey?: string) => { @@ -850,241 +553,210 @@ export function ModelView({ set, host }: ViewRenderProps) { scheduleLayoutPersist(positions); }, }; - const unavailable = !topicId - ? degradationFor('observed_model_scope_unavailable', 400) + const unavailableMessage = !topicId + ? 'Select a topic to load the observed model.' : error - ? withAction(degradationFor(error, 500), () => setReloadToken((token) => token + 1)) + ? error : null; return ( -
- topic:{topicId} : 'No topic selected'} - count={`${observed.eventCount} events`} - degradation={unavailable} - controlRow={( -
-
- {LENSES.map((lens) => ( - - ))} -
- - - {declared.versions.length >= 2 ? ( -
- - - - -
- ) : null} +
+
+ {loading && topicId ? ( +
+ Loading observed model.
+ ) : ( + )} - className="bg-transparent text-ij-ink" - > -
-
-
- {proposalComposerOpen ? ( -
void requestProposal(event)}> -