Skip to content

v0.17.0 — the QA engine, plus mid-drag docking with shake-to-disconnect - #22

Merged
Coldbari merged 2 commits into
mainfrom
feat/qa-engine
Sep 2, 2026
Merged

v0.17.0 — the QA engine, plus mid-drag docking with shake-to-disconnect#22
Coldbari merged 2 commits into
mainfrom
feat/qa-engine

Conversation

@Coldbari

@Coldbari Coldbari commented Sep 2, 2026

Copy link
Copy Markdown
Owner

Two commits from a shared working tree, kept separate because they are unrelated changes by different sessions.

efcccef — v0.17.0, the QA engine

Validation becomes a rule engine instead of two lists split by an implicit flag.

  • Severity is real. It used to be encoded as the absence of a field (severity?: 'suggestion', where undefined meant error), so there was no middle. 21 rules now declare critical / warning / information and a discipline, and the report groups by severity because that is how an engineer triages.
  • Accept a finding, with a reason. Not every finding is a mistake. The reason is recorded and stays visible in its own section — a hidden acceptance rots. Because findings are keyed by rule + engineering identity and never by node id, an acceptance survives deleting and redrawing the symbol.
  • Fixes are data, not callbacks. A rule returns a FixSpec, so a repair can be named, previewed with its blast radius, and replayed in a test. applyFix reports whether it landed instead of failing silently.
  • One index per document. runChecks alone used to build four maps; the advisor rebuilt a neighbour list per node per rule.

Three bugs it found

  • P-101 on a pump was reported as an invalid ISA tag. invalid-letters ran the ISA-5.1 instrument letter tables over equipment. Found by running the new rules against this project's own bundled templates — the false positive existed in the old engine too.
  • The same finding was emitted twice when two symbols wore one tag, so accepting one silently accepted both.
  • qaFor's cache never hit — doc.qa?.ignored ?? {} allocated a fresh object per call, re-running all 21 rules on every render.

Calibrated against real drawings

Run over the five bundled examples, the rule set initially reported criticals on three of them and 11–13 identical warnings. That teaches people to ignore the report. So no-relief and no-fail-position are warnings rather than blockers by default (a company standard promotes them in v0.18), and required-field-empty only fires on a record someone has started. All five bundled drawings now report zero criticals.

Retires validate/checks.ts, suggest.ts, issues.ts and the two panels that read them; coverage moved to rule tests. locateCell moves to canvas/locate.ts.

659ff7d — mid-drag docking with shake-to-disconnect

Authored by a concurrent session in the same checkout. Docking no longer waits for the mouse button: the line is drawn the moment two connection points meet and stretches as the drag continues. Symbols stand off 24px along the way the port faces (coinciding points read as nothing having happened); magnets only join ports that face each other; shaking the symbol mid-drag cuts the line that drag made. One undo step. Also fixes a pointer-event ordering bug where pointerup on window lands before JointJS's compatibility mouseup.

Verification

  • npm run build — clean
  • npm test746 passing
  • npx playwright test --workers=168 passing

Run serially on purpose: the e2e suite flaps against the shared dev server when multiple sessions are editing, because HMR reloads kill tests mid-run.

tsc -b initially reported clean because the check was piped through head, which masked its exit code; an isolated worktree at the commit surfaced two real TS6133 errors, since fixed.

🤖 Generated with Claude Code

Coldbari and others added 2 commits September 2, 2026 13:52
Validation becomes a rule engine an engineer can work through, instead of two
lists split by an implicit flag.

Severity is now a real thing

The old model encoded it as the ABSENCE of a field — `severity?: 'suggestion'`,
where undefined meant error — so there was no middle and no way to say how much
a finding mattered. Rules now declare critical / warning / information and a
discipline (tagging, topology, process, instrumentation, data), and the report
groups by severity first because that is how an engineer triages: what blocks
issue, then what to look at.

Accepting a finding, with a reason

Not every finding is a mistake. Accepting one records why, and keeps it visible
in its own section rather than hiding it — a hidden acceptance rots, and the
reason someone accepted it is exactly what the next reviewer needs. Because a
finding is keyed by rule + ENGINEERING identity and never by node id, the
acceptance survives deleting and redrawing the symbol.

Fixes are data, not callbacks

A rule returns a FixSpec, so a repair can be named, previewed with its blast
radius, and replayed in a test before anything happens. applyFix reports whether
it actually landed instead of failing silently, and describeFix gives one shared
description so a preview and the action cannot drift. (Requested by another
session building on this; it was the right call regardless, and converting the
closures is what exposed the duplicate-key bug below.)

One index per document

Every rule reads a single prepared walk of the drawing. runChecks alone used to
build four maps, and the advisor rebuilt a neighbour list per node per rule.

Three bugs this found

- Equipment tags were reported as ISA errors. invalid-letters ran the ISA-5.1
  INSTRUMENT letter tables over equipment, so P-101 on a pump — a completely
  standard tag — was flagged. Found by running the new rules over the project's
  own bundled templates. The false positive existed in the old engine too.
- The same finding was emitted twice when two symbols wore one tag, so accepting
  one would silently accept both. Findings are now one per rule and entity.
- qaFor's cache never hit, because `doc.qa?.ignored ?? {}` allocated a fresh
  object every call, so all 21 rules re-ran on every render.

Calibrated against real drawings, not intuition

Running the rule set over the five bundled examples first reported criticals on
three of them and 11-13 identical warnings. That teaches people to ignore the
report, so: no-relief and no-fail-position are warnings rather than blockers by
default (both are real, both are legitimately unstated on early drawings; a
company standard promotes them in v0.18), and required-field-empty only fires on
a record someone has STARTED — an object nobody has begun specifying is not yet
an omission. All five bundled drawings now report zero criticals.

Retires validate/checks.ts, validate/suggest.ts, validate/issues.ts and the two
panels that read them; their coverage moved to the rule tests. locateCell — the
primitive behind every jump in the app — moves to canvas/locate.ts, where it
belongs. Removes the dead Finding type.

Note on a shared working tree: this commit was made from a checkout in which
another session was concurrently building the mid-drag docking work. Its own
files are committed separately, but a few of its lines ride along here in files
both changes touch — the .pid-dock-cut styles in app.css, dockNode returning the
new edge id in store.ts, one README bullet, and its own [Unreleased] CHANGELOG
section (left under its own heading, deliberately not folded into 0.17.0).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Docking used to wait for the mouse button: you dropped a symbol on a
connection point, let go, then picked it back up to pull it into place.
The line is now drawn the moment the two points meet, with the button
still down, and the pipe stretches as the drag carries on.

- Symbols stand off 24px from the point they dock onto, along the way
  that port faces. Landing the points on top of each other read as
  nothing having happened: the symbols butted together and hid the line
  behind themselves.
- A magnet only joins ports that FACE each other. Two ports pointing the
  same way stood a symbol on the wrong side of the nozzle it had just
  connected to, with its inlet pointing away.
- Shake the symbol mid-drag and the line that drag made is cut, with a
  red flash where it landed. The symbol stays in hand, won't snap back
  onto the point just rejected, and letting go doesn't sneak the
  connection back on.
- The whole gesture is ONE undo step, catch and carry together.

Fixes a pointer-event ordering bug found while testing: 'pointerup' on
window lands BEFORE the compatibility 'mouseup' JointJS listens on, so
the global teardown was closing the drag's undo group early and letting
a release re-connect a shaken-off pair.

Authored by a concurrent session working in the same checkout; committed
here so it ships with v0.17.0 rather than sitting uncommitted. Its edits
to files this branch also touches (app.css, store.ts, README, CHANGELOG)
landed in the preceding commit — see the note there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Coldbari
Coldbari merged commit 659ff7d into main Sep 2, 2026
4 checks passed
@Coldbari
Coldbari deleted the feat/qa-engine branch September 2, 2026 08:29
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