Skip to content
jonbaePublic

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

ps-flow

A PureScript port of @xyflow/react. Vocabulary — surfaces, gates, baselines, the dual-run net — is CONTEXT.md; translating upstream TypeScript into PureScript is PORTING.md; this file is what to run.

Build

npm run build:driver   # spago build + esbuild bundles the browser driver page (see its README)
npm run build:oracle   # regenerate oracle/index.js, the @psflow/oracle bundle

The five gates

Three of them are parity gates — one technique at three grains, comparing PSFlow against executing upstream. Two are test suites — collections of written tests. The difference is not coarseness, it is instrument: delete xyflow/ from the machine and the three parity gates become impossible, having nothing to compare against, while conformance and smoke still run and still mean something.

Gate Command What it compares
surface parity npm run parity:surface exported values, shapes and selected JS-barrel behavior, plus prop members, against vendored upstream
function parity spago test pure-function return values, against the @psflow/oracle bundle
conformance test suite npm run test:conformance upstream's own e2e specs, ported — upstream's asserted intent
smoke test suite npm run test:smoke liveness: that the driver page comes up, and that a session with it prints nothing
system parity npm run parity:system a mounted app's whole end-state trace, against vendored upstream mounted beside it

spago test is deliberately broader than function parity: it also runs the unit tests, the PSFlow-only modules that prove the internals are self-consistent. Those are outside the gate scheme — a different claim from "PSFlow matches xyflow". "Function parity green" collapsing to "spago test green" is stricter than needed, never weaker.

The smoke suite was ten tests until #61: two liveness ones and eight hand-authored parity assertions, the densest in the repo. The eight retired when the net began re-reporting each of them, recorded per test in parity/system/corpus/retirement-debt.mjs — which names, for each, what it proved, the trace section the re-report lands in, and the scenario that does it. Nothing retired for redundancy; per test rather than per file because one of the eight was the only callback assertion on either surface. Three things go red: a citation naming no scenario fails parity:system, a retired title still present as a test( call fails test:harness, and so does a cited scenario whose stored traces are empty in the section it retires into. The two tests that did not retire are recorded beside them, each with the title it had before, so every title in the split is accounted for.

One browser spec sits outside the scheme as well:

npx playwright test --config examples/react-smoke/playwright.config.ts --project=screenshot
# writes parity/driver/dist/smoke.png

node-props.spec.ts stood beside it until #61 — a PSFlow-only guard on the NodeProps record. Its two tests are re-reported by node-props-record-parented, which drives a fixture whose nodes render the record into the DOM on both sides. It did not retire on the condition written down for it, "when the net's props section is green": both of its claims are about the graph, and a probed run compares only its own observation level rather than the graph the probe replaced. That section is still red on three things the spec never saw — EdgeProps and ConnectionLineComponentProps reaching it from one side only, and NodeProps.width / height reading undefined against upstream's 0 on an unmeasured node (#22). The first two were blocked on edgeTypes and connectionLineComponent not having crossed, which boundary stage 4 (#62) has since done; what is left there is a fixture that sets one.

Ordering. Surface parity green is a hard precondition — its failures are interop-shaped and shadow every other gate at once. Everything else is interpretation order: while a cheaper gate is red, do not trust a system parity divergence, but nothing blocks. A red function parity may not touch the fixtures at all.

Surface parity needs spago build first, and hard-fails on missing compiled output rather than falling back. It imports index.js rather than reading it — a regex over the file proves a line of text exists, not that the binding resolves — which is also what lets it compare typeof, arity and React wrapper kind across every export. The cost, accepted in ticket 041: the cheapest gate is no longer standalone.

The gate also deep-compares the eight frozen enum objects and calls the seventeen pure functions once through index.js with the same inputs as the vendored upstream bundle. This catches JS-boundary failures that typeof and arity cannot, especially a labelled record where upstream returns a positional array. Known differences are keyed by export and difference class; every one names its retiring boundary stage and fails as stale when it stops matching.

System parity is red, and that is where it is meant to be. It mounts upstream and ps-flow on the same unmodified fixtures, settles each on its own clock, captures each twice, and diffs all seven trace sections. Selective probe variants compare only their declared observation level, so a replacement probe's DOM cannot be mistaken for library behavior; its first run found nine classes of divergence, and the call log added its own — handlers ps-flow never fires, handlers it fires twice, and a call sequence it does not reproduce between its own two captures. Both lists live on the divergence backlog (#22) rather than being fixed. A difference is answered by a fix in the port or by a region in parity/system/regions.json — never by loosening what the net looks at. It builds both driver bundles itself, so it cannot measure code that is not in the tree, and needs spago build first for the ps-flow side. Its detail is parity/system/README.md; the report is parity/system/report.md and the traces behind it are committed under parity/system/traces/.

It also says what it reached. parity/system/coverage.md is derived from the traces that ran rather than declared: every export-bearing census entry carries a witness — a selector for dom, a name mapping for the other four sections — and counts as driven only if some captured trace holds it. What nothing drove is a hole with a written reason, and an undeclared hole fails the run. Today 124 of 156 are driven and 32 are declared holes, with no undeclared residue. That is what the corpus's termination condition is stated against. It regenerates from the committed traces in milliseconds, with no browser and no vendored checkout.

npm run parity:system                    # build both bundles, capture the corpus, diff, cover
node parity/system/net.mjs --compare-only  # re-diff the stored traces — seconds, no browser
node parity/system/net.mjs --scenario mount-baseline--nodes-general
npm run parity:coverage                  # regenerate coverage.md from the stored traces

Checks that are not gates in that sense

npm run parity:boundary   # boundary module — outbound drift + every crossing mounted
npm run parity:changelog  # 12.3.5→12.11.0 changelog audit; gates on unbucketed PRs
npm run test:audit        # node --test over the audit's bucket table and its rules
npm run test:surface      # node --test over surface parity's shape and allowlist logic
npm run test:boundary     # node --test over the PureScript record reader both boundary checks share
npm run test:census       # node --test over the standalone census generator and its staleness logic
npm run test:compare      # node --test over the system-parity comparison core
npm run test:coverage     # node --test over the witness language, the coverage join and its artifact
npm run test:harness      # node --test over its capture half and the driver's registries and sides
npm run test:harness:live # the net harness against a real page — a browser, but no parity claim
npm run test:ci           # node --test over the gates workflow and the baseline vendoring script

parity:boundary is two staleness checks over src/Boundary/. drift.mjs compares each crossed record's PureScript label set against its JS-shaped one, live from source, because the outbound direction has no compiler check and the repo's generators vary too few fields for a round-trip property to find an omission. mount.mjs enters through index.js — the only door the boundary module is gated on — and makes seven claims: that a fully converted prop set mounts and arrives JS-shaped, that every prop the boundary has not crossed yet is refused rather than ignored, that every callback prop that has crossed is accepted rather than refused and is converted from its own field by a handler type a JavaScript caller can satisfy in one call, that the graph utilities a driver calls are callable from JavaScript with their round trip closing and addEdge reporting an empty endpoint through options.onError, that three of the four chrome components mount with no props at all and the fourth names the prop upstream declares required, that the four components a consumer's own node component mounts do so — <Handle /> on upstream's defaults for the two props ps-flow's record makes required, <NodeToolbar />, <NodeResizer /> and <NodeResizeControl /> beside it — and that useNodesState / useEdgesState return upstream's 3-tuple with a setter that runs. It needs spago build first and hard-fails on missing compiled output.

The callback claims are the ones that cannot be fired here: every flow-level handler reaches the store through an effect, and a server render runs none, so this gate holds that a handler was accepted and the net's callbacks section holds that it was called.

parity:changelog measures what a baseline bump costs, not what detects a divergence.

test:ci is the gates workflow's self-test: it holds .github/workflows/gates.yml to the five-gate table above — the four cheap gates each run once, under the command this file documents, with surface parity first and system parity absent — and covers the baseline vendoring script CI runs. Editing either file without the other goes red.

Continuous integration

.github/workflows/gates.yml runs the four cheap gates on every pull request: surface parity, function parity, the conformance test suite and the smoke test suite, in that order, each failing the pull request check when it is red. spago test is the whole PureScript suite, so that step is stricter than function parity alone — never weaker.

Surface parity runs first and blocks. It is the only hard precondition, so when it is red the three gates after it do not run and the run's summary says why they were withheld rather than leaving them skipped in silence. Everything below it is interpretation order.

System parity is not in the per-PR workflow. Its per-run cost is not knowable until the corpus exists — a floor of ~60 scenarios, which at two sides by two captures is ~240 captures, each settling on its own clock — and multiplying the two is what answers the per-PR / nightly / bump-only question. Until that measurement exists, a schedule would be a guess; and while the divergence backlog is being worked, a per-PR run that is red on purpose would train everyone to ignore a red pull request check.

CI vendors the parity baseline itself, since xyflow/ is gitignored:

node .github/scripts/vendor-baseline.mjs   # resolves the tag from the exact pins below

It reads the exact-pinned @xyflow/* devDependencies, downloads that release tag's source tarball into xyflow/, and fails if the extracted tree disagrees with either pin — the tag names the react version only, so the system version is checked rather than assumed. It refuses to touch an existing xyflow/: a bump is a delete and re-vendor, never a merge.

CI then rebuilds oracle/index.js and parity/driver/dist/psflow.js before running anything. Both are committed so a fresh clone works without the vendored tree, and nothing fails when they are left stale — a bundle predating output/ leaves every spec green about code that is gone. CI has the vendored tree anyway, so rebuilding costs seconds and the gates measure the commit rather than the last artifact someone rebuilt by hand.

The parity baseline

The parity baseline is the vendored xyflow/ checkout — currently @xyflow/react 12.11.0 / @xyflow/system 0.0.77. All three parity gates read from it; no JavaScript in this repo imports @xyflow/* from node_modules, and parity/driver/build.mjs fails if a bundle picks any up. The two browser pages do link upstream's stylesheets from there, which is what the exact pin below makes safe.

The @xyflow/react / @xyflow/system devDependencies are exact-pinned to those same versions. They are a parity baseline pin, not build dependencies — they exist so the declared version can never silently disagree with what the gates actually measure. Do not add a caret: a floating range would let npm install drift the declared baseline away from the vendored one without any gate noticing.

Bumping the baseline is one atomic change:

  1. re-pin both devDependencies to the new versions,
  2. rm -rf xyflow && node .github/scripts/vendor-baseline.mjs — the checkout is resolved from the pin, which is what keeps the two halves of the baseline one change rather than two, and is how CI gets its copy,
  3. npm run build:oracle (commit the regenerated oracle/index.js),
  4. npm run build:driver (commit the regenerated parity/driver/dist/psflow.js — it inlines upstream's fixture files and the ColorMode example, so a bump changes it),
  5. npm run parity:surface and npm run parity:changelog — the audit will fail on every PR the bump introduced until each is bucketed in parity/changelog-audit/verdicts.json,
  6. npm run parity:fork — the conformance seed is a one-time fork of upstream's own end-to-end suite, and a bump that rewrites one of those specs leaves the scenario lifted from it driving perfectly while it no longer corresponds to anything upstream tests. Read each spec the register names, decide whether its scenario still drives what it should, then node parity/system/fork.mjs --affirm. A test the bump added stays unregistered until someone writes down what to do about it; affirming cannot create an entry. parity:system runs this first and will not capture until it is green,
  7. npm run parity:system (commit the re-captured parity/system/traces/, report.md and coverage.md). Diff the traces against their previous versions before you commit them: what upstream's own render changed under the bump is visible there and nowhere else. A bump that adds an export fails the census until it is classified and then fails coverage until it has a witness, and one that removes an export fails coverage as a stale witness — which is how a bump surfaces new surface instead of silently under-counting.

xyflow/ is gitignored, so parity:surface, parity:changelog, build:oracle, build:driver, parity:fork and parity:system all require it present and hard-fail on a clean clone. parity:coverage is the one that does not: it prefers the corpus, falls back to the scenarios the stored traces name, and says in both the console and the artifact which it used. The committed artifacts (parity/surface/report.md, parity/changelog-audit/report.md, oracle/index.js, parity/driver/dist/psflow.js, parity/system/report.md, parity/system/coverage.md and the traces under parity/system/traces/) are what survive without it.

The traces are committed for a reason beyond a clean clone: re-diffing a stored trace of one baseline against a trace of the next is what turns a bump into a behavioural changelog, and that comparison is impossible if the older traces only ever existed on the machine that captured them.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages