Skip to content

feat(flows): author flows against OpenRegister's store, and stop calling them graphs - #149

Merged
rubenvdlinde merged 4 commits into
developmentfrom
feat/flow-engine-unification
Aug 4, 2026
Merged

feat(flows): author flows against OpenRegister's store, and stop calling them graphs#149
rubenvdlinde merged 4 commits into
developmentfrom
feat/flow-engine-unification

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

What

Hermiq's flow editor now reads and writes OpenRegister's native flow store and nothing else, and the word "graph" is retired.

Why

The editor read a duplicate hermiq/agentflow object mirror while the engine ran the native oc_openregister_flows rows. Two copies, free to drift — and they had: the Hydra sequencer's prose sat in the object's description while the native row's notes was null, which is exactly why the Notes tab rendered empty for a flow that documents itself at length.

flow-storage/spec.md forbids the mirror outright:

A flow definition SHALL NOT be stored as an OpenRegister object […] No other app SHALL own a flow store, a flow controller, or a flow execution service.

src/api/graph.js is deleted. It posted to /apps/hermiq/api/graph/run — a route never registered in appinfo/routes.php, so Run had been posting into a 404.

Four rendering defects, one dialect mismatch

A flow is a Petri net: a node is a place, an edge is the transition carrying type/config. The builder read edges[].source/.target and nodes[].type — keys a stored flow does not have. On the live sequencer (17 nodes, 16 edges) that drew zero edges and labelled every node with an em-dash. Silently, because a dropped edge and an absent key look identical.

Defect Fix
no edges drawn draw from from/to, fanning array endpoints out — 3 splits ⇒ 19 lines from 16 edges
every node blank label name || id, never a dash
card inside a card one frame, owned by the canvas wrapper. Both failure modes had shipped — a second frame, then none at all
port purple regardless of role success where a run starts, error where it ends; round again at 16×16 (NC's global button min-height had stretched it to 16×34)

Plus: the sidebar closes and reopens (its close event was unhandled, so the X did nothing), and the canvas zooms — CnGraphCanvas always emitted update:zoom, nothing bound it.

The rename

"Graph" was never a second entity, and the second name is what let the second store, the phantom endpoint and the blank canvas each look reasonable. Routes, page ids, components, store, CSS and strings all say flow. /graphs and /graphs/:id remain as legacy pages so links already pasted into Hydra issues and run logs keep working.

Verification

lint, stylelint, check:specs, test:l10n, webpack build — all pass. POST /api/flow/validate accepts the round-tripped sequencer. e2e 9/9, including a positive control: forcing the edge count to 18 fails reporting 19, so the suite is shown able to fail.

Two of those greens were earned rather than assumed — the port-colour test was failing against correct code (it pinned var()'s fallback literal, and --color-success resolves differently at the handle than at the root), and wrapping the sidebar toggle in the new zoom controls had dropped its class.

Follow-ups (specced, not in this PR)

or-flow-action-nodes · or-flow-connectivity-and-last-run · or-flow-migrate-definitions · hermiq-flow-canvas-ports · cn-flow-store-and-canvas-rename (moves this PR's src/api/flows.js into nc-vue as a shared createFlowStore).

🤖 Generated with Claude Code

…g to write them

resolveFk() nulls a dangling foreign key and says "row still migrated". That is
right for an optional reference and impossible for Conversation.agentId, which
the schema marks REQUIRED — a conversation that runs against no agent is not a
conversation, so the write could only ever fail validation.

It did, eight times per repair: "failed to write conversation <uuid>: Property
'agentId' should be type 'string' but is 'null'" — an error describing the
symptom, emitted by a step that had already decided to carry on.

These conversations reference agents that no longer exist. There is nothing to
migrate them onto, so they are now skipped and reported as unmigratable, which is
the true statement. The class docblock said the row is always migrated; it now
records the exception rather than leaving the next reader to discover it from a
stack trace.
…ing them graphs

Hermiq's flow editor read a duplicate `hermiq/agentflow` OBJECT mirror while
the engine ran the native `oc_openregister_flows` rows. Two copies, free to
drift, and they had: the Hydra sequencer's prose sat in the object's
`description` while the native row's `notes` was null, which is why the Notes
tab rendered empty for a flow that documents itself at length.

`flow-storage/spec.md` forbids the mirror outright — "A flow definition SHALL
NOT be stored as an OpenRegister object […] No other app SHALL own a flow
store, a flow controller, or a flow execution service." So the editor now goes
to `/apps/openregister/api/flows` for everything, and `src/api/graph.js` is
deleted: it posted to `/apps/hermiq/api/graph/run`, a route never registered in
`appinfo/routes.php`, so Run had been posting into a 404.

Four rendering defects, all one dialect mismatch. A flow is a Petri net: a node
is a PLACE and an edge is the TRANSITION carrying `type`/`config`. The builder
read `edges[].source`/`.target` and `nodes[].type` — keys a stored flow does
not have. On the live sequencer (17 nodes, 16 edges) that drew ZERO edges and
labelled every node with an em-dash, silently, because a dropped edge and an
absent key look the same. Now:

  - edges draw from `from`/`to`, with array endpoints fanned out, so the three
    splits render as 19 lines rather than 16;
  - places are labelled `name || id`, never a dash;
  - one card frame — the canvas wrapper owns it, the body adds none. Both
    failure modes had shipped: a body frame over the wrapper's (a card in a
    card), then no frame at all;
  - the connection port carries the place's role — success where a run starts,
    error where it ends — and is round again, 16x16. Nextcloud's global button
    min-height had been stretching it to 16x34.

Also: the sidebar closes and reopens (its `close` event was unhandled and the
X did nothing), and the canvas zooms. `CnGraphCanvas` always emitted
`update:zoom`; nothing bound it, so the wheel was inert.

"Graph" is retired. It was never a second entity, and the second name is what
let the second store, the phantom endpoint and the blank canvas each look
reasonable. Routes, page ids, components, store, CSS and strings all say flow;
`/graphs` and `/graphs/:id` stay as legacy pages so shared links keep working.

Verified: lint, stylelint, check:specs, test:l10n and the webpack build pass;
`POST /api/flow/validate` accepts the round-tripped sequencer; e2e is 9/9 with
a positive control confirming the suite can fail.
…ductionNL/hermiq into feat/flow-engine-unification

# Conflicts:
#	l10n/en.json
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ 6df1325

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright

Quality workflow — 2026-08-04 14:06 UTC

Download the full PDF report from the workflow artifacts.

… cannot survive

`MigrateAgentDataTest::testDanglingForeignKeyIsNulledAndCounted` pinned a
defect. It asserted that a conversation whose agent no longer exists is "still
migrated" with `agentId` nulled — but `agentId` is REQUIRED on the Conversation
schema, so that row can only ever fail validation. It did, eight times per
repair, as "Property 'agentId' should be type 'string' but is 'null'": an error
describing the symptom, emitted by a step that had already decided to carry on.

f3497ef fixed the step to skip and report those rows instead. The test kept
asserting the old behaviour, so the fix landed red.

The spec never asked for the nulled write. It asks the step to "log the skipped
record and continue processing remaining records" — which is exactly what
skipping does. So the test now asserts that:

  - the unmigratable conversation is NOT written;
  - it is reported BY UUID, because a silent drop is not a skip;
  - and a second, resolvable conversation still migrates — the "continue
    processing" half, which is the requirement actually worth defending, since
    a repair step that abandons the run on one bad row leaves an instance
    half-migrated.

`IOutput::warning` is captured for the first time; a test watching only `info`
cannot tell a reported skip from a silent one.
@github-actions

github-actions Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/hermiq @ f418129

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
composer ✅ 117/117
npm ✅ 744/744
PHPUnit
Newman
Playwright

Quality workflow — 2026-08-04 14:30 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit bb1bd4c into development Aug 4, 2026
36 checks passed
rubenvdlinde added a commit that referenced this pull request Aug 5, 2026
…failing CI (#157)

v1.0.1 is `f4d9756` (2026-08-03) and predates three gate fixes, so every
Hydra Gates run this repo has ever made executed a script in which 16
gates reported PASS when their helper never ran (ConductionNL/.github#147),
gate-33 had no axe report to read and never said so (#148), and gates 6
and 7 reported PASS on an empty scope (#149). The tick was identical
either way, which is why nothing in this repo's history shows it.

That pin is now also RED, and the mechanism is worth writing down.
quality.yml is referenced `@main` while this package is PINNED, so the
two can desync. #164 flipped `hydra-gates-require-full-coverage` to
default true in the shared workflow, and that flag requires a gate to
DECLARE itself not-applicable. v1.0.1 contains ZERO `_skip` calls; v1.3.0
has 36. v1.0.1 has no vocabulary to declare, so every absent prerequisite
became "DID NOT RUN" and failed the job — for gates the repo has no
subject matter for.

Measured on this branch, diff-scoped against origin/development exactly
as CI scopes it, in a private mount namespace with a private tmpfs (the
runner's ~50 /tmp/hydra-gate-*.log paths are shared state and two
concurrent runs corrupt each other's counts, .github#158 item 6):

  v1.0.1  exit 98  FAIL — "GATES THAT DID NOT RUN: 24 33"
  v1.3.0  exit 0   PASS — those gates named NOT APPLICABLE, with reasons

Independently confirmed end-to-end: doriath#160 changed this one line and
nothing else, and its Hydra Gates job went failure -> success.

v1.3.0 is `f7eaf2a` = .github@main at the time it was cut.

Refs ConductionNL/.github#159
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant