You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The data model and service surface that lets a Component own a contract — paste OpenAPI / AsyncAPI / TS-signature text and have it materialize into addressable Flow rows. M3-independent: no boundary proxies, no refinement routing, no canvas-edge changes. This slice is the foundation every later slice composes on.
Three new entities and one cascade arm:
FlowSpec (1:1 with a Node) holds the pasted source text, parse status, and kind (OPENAPI / ASYNCAPI / TS_SIGNATURE / GRAPHQL / CUSTOM).
Flow is a first-class row owned by a Node (ownerNodeId): kind, key, title, polarity (INBOUND | OUTBOUND), signature (Json), sourceSpecId?. De-dupe on (ownerNodeId, key) among active rows (ADR-0005 style — service-enforced, with a partial unique index backstop per ADR-0010 / ADR-0011).
FlowPolarity enum: a Flow's directional relationship to its owner. INBOUND for GET /pets (the owner consumes), OUTBOUND for SSE / events (the owner emits). This is the per-flow direction encoder; ADR-0009 stays untouched (no stored direction on Edges).
deleteNode cascade-sweep extended to also stamp owned Flows + owned FlowSpec with the same deletionId so undo restores them in lockstep (ADR-0008).
Services exposed: attachFlowSpec, addFlow, updateFlow, deleteFlow, plus an internal parseFlowSpec invoked by attachFlowSpec. tRPC adapters thin. UI: a paste field in a Component-detail panel; a read-only Flow palette listing in that panel; a "N flows" pill on the Component node body when it owns ≥1 Flow. No canvas-edge changes in this slice.
Spec parsing is server-side, parse-on-write into Flow rows. Use a bounded loader (size + depth caps) so a hostile spec cannot OOM. Treat the raw source as untrusted (prompt-injection standing note in CONTEXT.md, parse-time clause): store verbatim, never interpolate.
MCP tools are split off to a follow-up issue (attach-flow-spec, add-flow, list-flows) so this slice can land its schema + service + UI without also gating on the MCP route. The follow-up reuses the services this slice exposes (no service-layer change required when MCP lands). The follow-up is blocked by #18.
Open design questions — defaults locked unless overridden
Recommended defaults from the plan, folded in here. Override on this issue before an agent picks it up if you disagree.
Spec UI lives in a Component-detail side panel (not inline on the Canvas).
User-authored Flows allowed (sourceSpecId = null) — useful for one-off SSE channels or function calls without a formal spec.
WebSocket / EVENT polarity defaults to OUTBOUND from the server end; manual override available.
Re-paste is non-destructive: matching keys preserved, dropped keys soft-deleted with a fresh deletionId per re-parse batch.
Edge.label stays — it names the pipe ("primary HTTPS"). Flow titles name per-call content. Complementary.
WebSocket modeling: two Flows per WS (one INBOUND, one OUTBOUND). Cleanest given ADR-0009. No BIDIRECTIONAL polarity.
Markdown export TBD in Slice 5 — not relevant to this slice.
OpenAPI is the only realized parser in Slice 1. Other FlowSpecKind values persist source verbatim with parseError = "Parser for <kind> is not implemented yet." and zero derived Flows. The enum stays complete so the API contract is stable for the MCP follow-up.
updateFlow rejects edits on spec-derived Flows (sourceSpecId != null). Re-paste the spec to change derived Flows. Hand-authored Flows are freely editable (title / signature only — narrow per "prefer narrow required inputs").
Acceptance criteria
Prisma schema additions for FlowSpec, Flow, FlowKind, FlowSpecKind, FlowPolarity authored via pnpm db:author <name> (scaffolds the migration directory and seeds it with the live-DB-to-schema diff — hand-edit for raw SQL such as the idx_flow_dedup partial unique index) and applied via pnpm db:migrate (ADR-0010 codified the workflow; the long-form prisma migrate diff invocation, db push, and migrate dev are retired — see CLAUDE.md "Commands" and commit b8305c6).
idx_flow_dedup partial unique index ON "Flow" ("ownerNodeId", "key") WHERE "deletedAt" IS NULL, with the DO $$ pre-existing-duplicates guard, modeled on idx_edge_dedup. NULLS NOT DISTINCT deliberately omitted (both columns NOT NULL — unlike Edge's nullable canvasNodeId).
isFlowDedupCollision helper in prisma-errors.ts mirroring isEdgeDedupCollision; ConflictErrorDetails widened additively with conflictingFlowIds?: string[].
Compile-time parity guard between Prisma enums and the Zod mirrors in ~/lib/schemas.ts (same Record<> map pattern as NodeKind) — six maps total for the three new enums × two directions.
attachFlowSpec, addFlow, updateFlow, deleteFlow, getFlowsForNode services exposed with the (db, actor, input) => result signature (ADR-0001). Writes owner-only via access.assertCanWrite; getFlowsForNode slug-readable per ADR-0002. Identity from the actor, never input.
attachFlowSpec parses the source with a bounded loader (MAX_FLOW_SPEC_SOURCE_BYTES = 1_000_000, MAX_DEPTH = 32, MAX_OPERATIONS = 500), persists FlowSpec, upserts one Flow per OpenAPI operation with polarity = INBOUND, and stamps re-parse drops with a fresh deletionId per batch.
Malformed spec stores FlowSpec.parseError, creates zero Flows, never throws to the caller.
deleteNode cascade-sweep extended: descendants + incident Edges + owned Flows + owned FlowSpec share one deletionId; restoreNode brings them all back with (ownerNodeId, key) pre-checks parallel to the existing Edge pre-check.
tRPC procedures wired and the Component-detail panel ships a paste field, parse-status indicator, and read-only Flow palette listing.
"N flows" pill renders on the Component node body when it owns ≥1 Flow (sourced from a _count.flows join into the existing getCanvasPromise.all — one round trip preserved per ADR-0001).
MCP tools split off to a follow-up issue; this slice does not register them. The follow-up reuses the services this slice exposes (no service-layer change required when MCP lands).
Vitest cases at the service seam (ADR-0003 isolated-Postgres harness):
happy-path attach + re-parse no-op
re-parse that drops keys soft-deletes them with a fresh deletionId
malformed spec creates zero Flows and records parseError
user-authored Flow (no sourceSpecId) round-trips
updateFlow rejects spec-derived edits with ValidationError
concurrency regression for idx_flow_dedup (mirror edge.service.test.ts:184-256)
deleteNode cascade stamps Flows + FlowSpec with the same deletionId; restoreNode revives them in lockstep
cascade does NOT re-stamp a Flow already soft-deleted by a lone deleteFlow
non-owner write paths denied; capability-slug never grants writes
CONTEXT.md updated with the new Flow / FlowSpec / Polarity / Flow palette / Flow kind / Flow spec kind vocabulary entries, plus the one-paragraph exception note inside the Component/Node split section ("Flow has no user/code split" — same word in both surfaces, no overload pressure). Standing prompt-injection note extended with the parse-time clause.
docs/adr/0011-flows-as-first-class-component-owned.md committed alongside the implementation (per the project's "docs travel with code slices" convention — the first-class-Flow decision is what this slice makes, so the ADR ships with it rather than being deferred to Slice 3 as originally sketched in the master plan).
Parent
Tracker: #33
PRD: #2
Plan:
docs/plans/flow-routed-connections.mdWhat to build
The data model and service surface that lets a Component own a contract — paste OpenAPI / AsyncAPI / TS-signature text and have it materialize into addressable Flow rows. M3-independent: no boundary proxies, no refinement routing, no canvas-edge changes. This slice is the foundation every later slice composes on.
Three new entities and one cascade arm:
FlowSpec(1:1 with a Node) holds the pasted source text, parse status, and kind (OPENAPI / ASYNCAPI / TS_SIGNATURE / GRAPHQL / CUSTOM).Flowis a first-class row owned by a Node (ownerNodeId):kind,key,title,polarity(INBOUND | OUTBOUND),signature(Json),sourceSpecId?. De-dupe on(ownerNodeId, key)among active rows (ADR-0005 style — service-enforced, with a partial unique index backstop per ADR-0010 / ADR-0011).FlowPolarityenum: a Flow's directional relationship to its owner. INBOUND forGET /pets(the owner consumes), OUTBOUND for SSE / events (the owner emits). This is the per-flow direction encoder; ADR-0009 stays untouched (no storeddirectionon Edges).deleteNodecascade-sweep extended to also stamp owned Flows + owned FlowSpec with the samedeletionIdso undo restores them in lockstep (ADR-0008).Services exposed:
attachFlowSpec,addFlow,updateFlow,deleteFlow, plus an internalparseFlowSpecinvoked byattachFlowSpec. tRPC adapters thin. UI: a paste field in a Component-detail panel; a read-only Flow palette listing in that panel; a "N flows" pill on the Component node body when it owns ≥1 Flow. No canvas-edge changes in this slice.Spec parsing is server-side, parse-on-write into Flow rows. Use a bounded loader (size + depth caps) so a hostile spec cannot OOM. Treat the raw source as untrusted (prompt-injection standing note in CONTEXT.md, parse-time clause): store verbatim, never interpolate.
MCP tools are split off to a follow-up issue (
attach-flow-spec,add-flow,list-flows) so this slice can land its schema + service + UI without also gating on the MCP route. The follow-up reuses the services this slice exposes (no service-layer change required when MCP lands). The follow-up is blocked by #18.Open design questions — defaults locked unless overridden
Recommended defaults from the plan, folded in here. Override on this issue before an agent picks it up if you disagree.
sourceSpecId = null) — useful for one-off SSE channels or function calls without a formal spec.Edge.labelstays — it names the pipe ("primary HTTPS"). Flow titles name per-call content. Complementary.BIDIRECTIONALpolarity.FlowSpecKindvalues persist source verbatim withparseError = "Parser for <kind> is not implemented yet."and zero derived Flows. The enum stays complete so the API contract is stable for the MCP follow-up.updateFlowrejects edits on spec-derived Flows (sourceSpecId != null). Re-paste the spec to change derived Flows. Hand-authored Flows are freely editable (title / signature only — narrow per "prefer narrow required inputs").Acceptance criteria
FlowSpec,Flow,FlowKind,FlowSpecKind,FlowPolarityauthored viapnpm db:author <name>(scaffolds the migration directory and seeds it with the live-DB-to-schema diff — hand-edit for raw SQL such as theidx_flow_deduppartial unique index) and applied viapnpm db:migrate(ADR-0010 codified the workflow; the long-formprisma migrate diffinvocation,db push, andmigrate devare retired — see CLAUDE.md "Commands" and commitb8305c6).idx_flow_deduppartial unique indexON "Flow" ("ownerNodeId", "key") WHERE "deletedAt" IS NULL, with theDO $$pre-existing-duplicates guard, modeled onidx_edge_dedup.NULLS NOT DISTINCTdeliberately omitted (both columns NOT NULL — unlike Edge's nullablecanvasNodeId).isFlowDedupCollisionhelper inprisma-errors.tsmirroringisEdgeDedupCollision;ConflictErrorDetailswidened additively withconflictingFlowIds?: string[].~/lib/schemas.ts(sameRecord<>map pattern asNodeKind) — six maps total for the three new enums × two directions.attachFlowSpec,addFlow,updateFlow,deleteFlow,getFlowsForNodeservices exposed with the(db, actor, input) => resultsignature (ADR-0001). Writes owner-only viaaccess.assertCanWrite;getFlowsForNodeslug-readable per ADR-0002. Identity from the actor, never input.attachFlowSpecparses the source with a bounded loader (MAX_FLOW_SPEC_SOURCE_BYTES = 1_000_000,MAX_DEPTH = 32,MAX_OPERATIONS = 500), persistsFlowSpec, upserts one Flow per OpenAPI operation withpolarity = INBOUND, and stamps re-parse drops with a freshdeletionIdper batch.FlowSpec.parseError, creates zero Flows, never throws to the caller.deleteNodecascade-sweep extended: descendants + incident Edges + owned Flows + owned FlowSpec share onedeletionId;restoreNodebrings them all back with(ownerNodeId, key)pre-checks parallel to the existing Edge pre-check._count.flowsjoin into the existinggetCanvasPromise.all— one round trip preserved per ADR-0001).deletionIdparseErrorsourceSpecId) round-tripsupdateFlowrejects spec-derived edits withValidationErroridx_flow_dedup(mirroredge.service.test.ts:184-256)deleteNodecascade stamps Flows + FlowSpec with the samedeletionId;restoreNoderevives them in lockstepdeleteFlowdocs/adr/0011-flows-as-first-class-component-owned.mdcommitted alongside the implementation (per the project's "docs travel with code slices" convention — the first-class-Flow decision is what this slice makes, so the ADR ships with it rather than being deferred to Slice 3 as originally sketched in the master plan).Blocked by
None — can start immediately. M3-independent.