Skip to content

Slice 1: Flows on Components — schema, service, spec paste #34

@CuriouslyCory

Description

@CuriouslyCory

Parent

Tracker: #33
PRD: #2
Plan: docs/plans/flow-routed-connections.md

What 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).
  • 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.

  1. Spec UI lives in a Component-detail side panel (not inline on the Canvas).
  2. User-authored Flows allowed (sourceSpecId = null) — useful for one-off SSE channels or function calls without a formal spec.
  3. WebSocket / EVENT polarity defaults to OUTBOUND from the server end; manual override available.
  4. Re-paste is non-destructive: matching keys preserved, dropped keys soft-deleted with a fresh deletionId per re-parse batch.
  5. Edge.label stays — it names the pipe ("primary HTTPS"). Flow titles name per-call content. Complementary.
  6. WebSocket modeling: two Flows per WS (one INBOUND, one OUTBOUND). Cleanest given ADR-0009. No BIDIRECTIONAL polarity.
  7. Markdown export TBD in Slice 5 — not relevant to this slice.
  8. 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.
  9. 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 getCanvas Promise.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).

Blocked by

None — can start immediately. M3-independent.

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentFully specified, ready for an agent to implement AFK

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions