The dock now talks to a real LLM. On first load it asks Socrates for an
opening turn that's grounded in the project's actual SysML model + active
validation issues. User replies and option-picks send turns through the
same channel. Thread + every message persist in SQLite so refresh keeps
the conversation.
apps/web/lib/llm
- gateway.ts: LLMGateway interface (chat + ChatOptions). Two adapters:
- lmstudio: OpenAI SDK against LMSTUDIO_BASE_URL (default
http://localhost:1234/v1)
- anthropic: @anthropic-ai/sdk against claude-sonnet-4-6 (set
ANTHROPIC_API_KEY when LLM_PROVIDER=anthropic)
Provider chosen via LLM_PROVIDER env (default: lmstudio).
- chatJSON(): JSON-mode helper with parse-error repair-retry — the same
defensive pattern proven against gemma-4-e4b in Phase 0.
- prompts.ts: server-only loader that caches .md prompts.
- prompts/socrates/character.md + review.md: ported verbatim from
phase-0/src/prompts/ (Phase 0 corpus validated these 10/10).
- socrates.ts: sendUserTurn() — builds the system prompt (character +
review + project context with trimmed model + active issues), runs
chatJSON against the gateway, persists user + assistant turns,
returns the structured turn. SocratesTurn schema is { text, options? }
with up to 3 numbered options matching the prototype.
apps/web/prisma
- SocratesThread + SocratesMessage tables. Auto-create one open thread
per project on first load.
apps/web/app/api/projects/[projectId]/socrates
- GET: returns active thread + parsed messages.
- POST: body { text }. Empty text triggers an opening turn. Persists user
+ assistant turns, returns assistant turn + provider metadata.
apps/web/components/socrates/SocratesDock.tsx
- Replaces the static thread prop with a projectId. Loads from API on
mount, auto-triggers an opening turn if the thread is empty, sends
user replies via POST. Numbered options click-to-pick or 1–3 keyboard
shortcut (skipped when focus is in an input). Status line shows the
active provider + model. Optimistic-local: user message appears
instantly, "thinking…" placeholder shows while the LLM works, errors
surface inline.
apps/web/.env.example + .env.local
- LLM_PROVIDER, LMSTUDIO_BASE_URL/MODEL/API_KEY, ANTHROPIC_API_KEY/MODEL.
- .env.local committed only with the local default (no real secrets);
user supplies their own per-machine.
What's not yet here (next iterations):
- Streaming responses (currently waits for full response, ~5-15s)
- Multi-thread switcher (one auto-thread per project)
- Socrates-proposes-ops flow (M7)
State now survives a page refresh. Every applied op writes a fresh model
snapshot + a changelog row inside one transaction; the editor route
server-component loads the latest snapshot and seeds it as the initial
ModelStore state. Optimistic-local apply on the client + background POST
to /api/projects/[id]/apply gives the UI an instant feel without giving
up the server-as-truth contract.
apps/web/prisma
- schema.prisma: Project / ModelSnapshot / ChangelogEntry. SQLite for dev
(file:./dev.db). The same schema swaps to Postgres by changing the
provider line + DATABASE_URL.
apps/web/lib/db
- client.ts: PrismaClient singleton with hot-reload guard.
- repo.ts: loadProject() auto-seeds Aristotle from fromFixture();
applyOpsToProject() runs the pure applyOps() server-side inside a
$transaction, writes the snapshot + changelog atomically, returns
{ applied, model, version, idMapping, errors }. Optimistic-concurrency
via expectedVersion → returns the server model on mismatch so the
client can resync without losing its tab.
apps/web/app/api/projects/[projectId]
- route.ts (GET): returns latest { model, version }
- apply/route.ts (POST): body is { ops, expectedVersion?, reason? }
apps/web/lib/sync/ModelStore.tsx
- Now takes initialModel + initialVersion + projectId. apply() updates
local state immediately, then POSTs in the background. On response the
authoritative server model + version replace the optimistic state
(handles tempId resolution from the server). Network errors keep the
optimistic state; the next successful apply reconciles.
apps/web/app/editor/[projectId]/page.tsx
- Server component now: awaits loadProject(projectId), passes initialModel
+ initialVersion + projectId to EditorShell. EditorShell falls back to
the fixture path when those props are absent (legacy callers / tests).
package.json
- pnpm.onlyBuiltDependencies allowlists prisma + @prisma/client + @prisma/engines
- db:push / db:generate / db:reset scripts
.gitignore
- apps/web/prisma/dev.db + dev.db-journal excluded.
Pinned to prisma@6 (prisma@7 dropped url from schema in favor of the
new adapter pattern; not worth the churn for MVP).
Both canvases now share a canonical SysMLModel through React context.
Renames in the diagram inspector ripple to every chip in the narrative
referencing the same refId, and double-clicking a chip emits an update op
that re-renders the diagram block. Validation re-runs on every successful
apply. State is in-memory; M5.9 adds Postgres persistence.
apps/web/lib/sync (new)
- ops.ts: ModelOp alphabet (18 op kinds across block / property /
association / constraint / requirement / relation, add/update/remove for
each). tempId() helper + per-kind constructors.
- applyOps.ts: pure (model, ops) → { model, idMapping, errors, applied }
reducer. Atomic per batch. Cascades on remove-block (drops incident
associations + constraint applies-to + requirement satisfiers). tempId
resolution rewrites to canonical ids on duplicate-id collision.
- ModelStore.tsx: React provider exposing { model, apply, issues,
issuesByElement }. Validation memoized on every model change.
Editor refactor
- EditorShell wraps in ModelStoreProvider. Initial model derived from
fixture (+ optional ?break= corruptions). Validation now lives in the
store, not duplicated here.
- LeftRail consumes useModel(): Model section lists real blocks +
constraints (sorted by kind), Requirements section lists real
requirements with traced/untraced status from r.relations.
Diagram refactor (the tricky piece)
- React Flow now owns ephemeral state via useNodesState / useEdgesState.
Positions, drag-in-progress, selection are all RF-internal.
- Model → RF: a useEffect runs on model change, applies targeted setNodes
updates only for elements whose semantic data (label, kind, properties)
changed. Object identity preserved for unchanged nodes — fixes the
"re-render storm on drag" + RF measurement-cache loss.
- RF → Model: onNodesChange / onEdgesChange / onConnect / onDrop emit
ops via useApply(). Constraint-applies edges decompose into
updateConstraint ops. deleteKeyCode={[Backspace, Delete]}.
- onNodesChange now handles type:'remove' too (was missing — that's why
selecting a block + Del removed only the edges, leaving the block).
Chip refactor
- ChipView resolves displayed label from useModel() via refId lookup
(block / requirement / association / property). Double-click chip →
inline rename input → emit update-{block,requirement,association} op.
All other chips with the same refId update on the next render.
- Slash-menu inserted chips have refId=null and skip the rename
affordance (until M6 wires real model element resolution).
Removed obsolete components/diagram-canvas/fixtureToFlow.ts; replaced
with modelToFlow.ts. Bumped CSS for the chip-rename input.
apps/web/lib/sysml — pure engine (no UI deps)
- model.ts: canonical types ported from phase-0 (Block, Property,
Association, Constraint, Requirement, SysMLModel)
- validate.ts: 12 rules grouped Structural (S1–S5) / Semantic (M1–M5) /
Traceability (T1–T2). Pure function returning element-anchored issues.
Cycle detection via DFS with canonical-key dedup. T3 deferred to Phase 1.5
(depends on narrative chip references).
- depgraph.ts: directed graph over Blocks / Properties / Constraints /
Requirements with `dependentsOf()` and `neighborhood()` for impact analysis
(substrate for M7).
- fromFixture.ts: loose FixtureData → strict SysMLModel converter (interim
until M5 unifies state).
- breaks.ts: 8 named model corruptions (S1/S2/S3/M2/M3/M5/T1/T2) for
?break=... demonstrations.
UI integration in EditorShell + LeftRail + DiagramCanvas + IssuesPanel
- Floating IssuesPanel (bottom-right) with severity-grouped counts and
collapse. Click an issue to focus its anchor across the rail and diagram.
- LeftRail: severity dots on each block in the Model section and on each
requirement entry. Tooltips show full issue text.
- DiagramCanvas: red/blue/dotted ring around offending blocks via
issuesByElement → BlockNode.data.issueSeverity.
- ?break=S1-dangling,M2-cycle,M5-dup-property,... query param injects
corruptions for verification of the "done when" criterion.
- Suspense boundary added at the editor route for useSearchParams.
Two pre-existing TipTap typing fixes (editor.storage cast through unknown).