The Spiral Laboratory
The Spiral Laboratory is where a situation becomes a test. You replay a recorded moment, step to the point that interests you, try things, write down what should hold, reduce it to the smallest case that still demonstrates it, and keep it. The library that results is what a new Spiral release and every domain change are measured against.
It is also the tuning loop: adjust the cost model — delay costs, noSoln outsourcing costs, travel
factors, option times, quality settings — until Spiral’s decisions match the operation’s intent, then
replay the same test and compare.
Open it from the landing screen (Spiral Laboratory, with an admin login), or from the entity
editor. Prerequisites: a client dev or tuning environment — see
Develop a client extension — and one or more tests in the
client’s tests/ tree.
What the laboratory holds, and what it does not
Section titled “What the laboratory holds, and what it does not”1. The library and its stages
Section titled “1. The library and its stages”The laboratory browses the client’s tests/ tree (HELIX_TESTS_DIR, or <HELIX_CLIENT_PATH>/tests). Each
directory is a stage of a test’s life:
| Stage | What lives there |
|---|---|
development/ | the bench — new tests, from scratch, from the vault, from a snapshot, or reduced by the minimiser. Not shared. |
new/ | ready for work; a developer approves it into current |
current/ | under development |
regression/ | passing reliably — the suite a new Spiral or a changed domain must pass |
challenge/ | to be retired or modified; leaves only to retired or current |
future/ | aspired to, not yet believed passable |
retired/ | kept for the record, and where a stale test belongs |
Each card shows the test’s name, its comment, what a good run of it is — every test must pass without
timing out, and one ending in assertions must satisfy those too — and its recent history as a colour on the
left edge — green
for a clean window, aqua when it passes now but has failed recently, and a distinct colour per failure kind
(result check, crash, timeout, cycling, slow). What do the colours mean? opens the legend. The history comes from
<name>.audit.json, which the harness appends to on every run, wherever the run was started.
A directory lists its tests newest first, by the run file’s modification date. The harness touches a test whose result has changed since its last run, a pass that was failing included, as well as every failure. So what has just moved is at the top. A test whose result changed within its last 5 runs is highlighted: a yellow ring and a changed tag, and its hover says what it was and what it is now.
Three rules watch the library, and they are proposals, never moves:
- a test in
current/whose last 10 recorded runs all passed on this node’s Spiral is proposed forregression/; - a test in
regression/whose last run failed is proposed forcurrent/; - a stale test is proposed for
retired/, wherever it sits.
Stale tests
Section titled “Stale tests”A test is stale when Spiral refuses the test’s own text — Json: add:<queue> not understood, or the
<<HERE>> marker on a statement it cannot parse. Nothing has been learnt about the thing under test: the
test is written in a language the optimiser has moved past, usually because an entity or an attribute it
names has since been retired.
The harness reports it as its own result, Stale, and the laboratory treats it as one:
- the card’s edge takes the stale colour, so it is never mistaken for a finding;
- the lifecycle proposes retirement, not repair;
- the minimiser refuses it — there is no fault in it to isolate.
That last point matters when a library has been carried over from an older tuning system. A stale test describes a way of working that is being retired along with the system that generated it, and the honest thing to do with it is to let it go.
Cycling tests
Section titled “Cycling tests”Some tests never finish. Spiral gets within a few steps of a plan, then repeats the same output records many
times a second, assigning a stop to a resource and taking it off again, and the plan is never optimised. The
perf line on Spiral’s stderr gives it away. Its steps to do (todo — i + u + s) fall to a small number and
stay there, never reaching 0, at one gear, while the output keeps streaming. An optimisation that is getting
somewhere empties todo and climbs the gears. A test waiting out its own long pause for writes no output at
all.
The harness watches for exactly that. When steps to do have sat above 0 and below 10, at one gear below 10, for 10 seconds while Spiral writes at least 10 records a second, it ends the run and reports its own result:
regression/invalidrepairtime2 FAIL Cycle: 2-record cycle on project:234370546 resource:1049403759 from +34s for 10s — gear 0, todo 2 never 0, 512 records/sThat run used to occupy its whole 450-second budget and report Timed out. Left without a budget, it flapped
for 23 minutes before Spiral broke out by itself. Now it ends 45 seconds in.
- The report lands beside the run file as
<name>.cycle.json. It records when the stall began, the perf line it sat at, the output rate, and the records that were repeating, with every number masked except the ids and references, so the flap reads plainly. It also names the projects and resources those records write to. - A test that times out while cycling is reported as
Cycle … (at the timeout). A cycle that began just before the alarm is still named for what it is. - The card takes the cycling colour (indigo, next to the timeout’s purple, because it is that kind of failure), and
the suite tally counts
Cycle:separately. SPIRAL_CYCLE_SECONDSsets the window (default10).0turns the watcher off.- The watcher is
spiralCycle.js, run by the harness with node. Without node, or without the file beside the harness, the harness behaves exactly as before. In the laboratory it is always there.
To look back at a run that has already finished, ask the same question of its stream. It reports every cycle it finds, or none, and whether one was still going at the end:
node tests/spiralCycle.js scan tests/regression/invalidrepairtime2.err.json --tail 0 # pulled beside the harness# ended at @1773528331 after 1401s: 2-record cycle on resource:1049403759 project:234370546 from +36s for 1399s — gear 0, todo 2 never 0, 517 records/s--tail is how many megabytes of the end of the file to read (16 by default; 0 reads all of it).
The same detector runs on a live dev or tuning node, and on any node with a Spiral. When the optimiser stalls
like this, an amber CYCLING appears beside the Spiral icon. Hover it for what is repeating, on what, and since
when. The primary’s card in Mission Control says the same. It is reported, never acted on. A Restart Spiral
clears it, as does Spiral breaking out by itself.
After a batch run, and whenever you press Review moves, the proposals open in a dialog: one row per test with the reason, a destination list with the recommended stage starred, a place for a note, and two buttons — Move or Ignore. Ignoring leaves the test where it is and stays quiet until that test’s history changes. Every move is recorded with its reason in the library’s journal, so the shape of the library is always explained.
If you would rather the laboratory kept the directories itself, tick lab manages directories: the recommended moves are then applied after each batch run. It is off by default, because a client’s own categorisation is theirs.
2. Open a test — the transport
Section titled “2. Open a test — the transport”Press Open on a card (or double-click it). Every test opens parked at its first tag, and the dispatch panel’s header gains the transport:
| Button | What it does |
|---|---|
| ⏪ | rewind. Reloads the test from the beginning on an empty database. |
| 👣 / ⏸ | paused. Standing on a command the button is a step, and pressing it releases that command. Once released it becomes pause, because the step is now running towards the next tag or assertion. |
| ▶ | playing. Every tagged command is released as it arrives, until you pause or the test ends. |
| ⏩ | fast forward. Press it, type a tag, Enter: every tagged command below that tag is released without stopping. An assertion still parks — running to a tag never runs through the thing being asserted. |
| ● | recording. Playback stops and what you do next becomes part of the session. |
| ✓ | check. Arms one click on a stop, to write down what should hold. |
| 💾 | save. Writes the session, launch to now, as a test. |
The three states are exclusive: pressing one turns the other two off, and none of them is released by
pressing it again — to leave a state you choose another, so the transport is never in no state at all.
The line beside them carries one thing at a time: the command you are standing on while stepping, Spiral’s
own tag x of y progress while playing or fast-forwarding, and ● recording while recording. When the
test has played out an END sign replaces the position: rewind to run it again.
A paused Spiral does not stop you. A released command queues on its stdin and is processed when the pause resolves, so stepping works throughout — it is simply a longer step. The Spiral icon beside the transport is what says the optimiser is pausing, and hovering it gives the time it is paused until.
Because the application is never read-only here, you can open editors, change values and probe between steps. That is the point: bring the replay to the moment you care about — the project whose allocation looked wrong, the surge that triggered outsourcing — and examine the world before letting the next command land.
3. Record your own steps
Section titled “3. Record your own steps”Press ● and do the work: raise a job, move a resource, force an allocation, change a window. Every command the application sends Spiral joins the session, bracketed by comments that say when recording began and ended, so the saved test shows exactly where you took over. Press ⏸ or ▶ to close the recording — ● does not un-press, so you cannot leave the transport in no state at all.
This is how a test grows beyond what was recorded: replay to the interesting moment, then add the two or three commands that provoke the behaviour you want to capture.
Or start from here: Snapshot. ● builds a test from its beginning, so rewind before you record. A
recording begun part-way through has no starting position, and Save refuses it. Snapshot, in the Dispatch
header, takes the world as it stands now as the start of a new test and starts recording from it. The
transport comes up with ● pressed, whether or not a test was open. Do the work, press ⏸ or ▶ to close the
recording, and Save keeps it as development/snapshot-<date>-<time>. A test that was open stays where it
was: the session now belongs to the snapshot, so Save never writes a partial test over it. Saving also ends a
recording that is still running.
A snapshot is taken where Spiral is. Helix’s records change the moment a command is released, but a stop’s
type, state and times come from Spiral’s answer. So a snapshot first waits for the answers Helix has already
received. It is refused, naming the stops, if any are still unanswered, because Spiral has not read the
commands that made them: it sits in a pause, or is cycling, before them. A test written then could never replay
(All non [DONE] stops must have a type). Take the snapshot at a tag Spiral has reached, before releasing those
commands. If the commands that follow it need to be in the new test, ⏩ through them while recording.
4. Conclude — write down what should hold
Section titled “4. Conclude — write down what should hold”A test becomes a test when it ends with a conclusion: a settle, then one or more check or refute
assertions (input object). You write them. Nothing is derived
for you, because the point of the exercise is to state the requirement.
Open the stop you care about — double-click it, or press ✓ and click it once in the plan, which is the same thing in one click. In its editor’s Schedule card, on the row with Rebook and Hold, choose check or refute and tick the properties that matter: the resource, the desk, the state, the times, the type, the solution tier — only the ones Spiral actually gave the stop are offered. Press Add check. The assertion, and a settle before it, are recorded into the session.
The assert control needs Full or Developer mode, and a stop Spiral has scheduled. A dev or tuning
node opens in Full for exactly this reason; on any other node, switch mode from the burger menu.
- check — this must hold on replay.
- refute — this must not.
5. Save
Section titled “5. Save”Press 💾. You are asked for the name, and it defaults to the open test’s: keep it to overwrite that test,
or type another to create a new one in development/, which then becomes the open test. What is written is
the whole session, launch to now — the position, the commands played, the ones you recorded, and the
conclusion — as one freestanding test that replays through the harness with no Helix at all.
6. Minimise — the smallest test that still shows it
Section titled “6. Minimise — the smallest test that still shows it”A test captured from a live moment carries thousands of entities, of which a handful matter. Minimise reduces it to the least that still shows the fault, and proves every removal by replaying the candidate and asking Spiral.
There are two ways in, and one panel.
-
The dispatch header, beside Snapshot: Minimise is offered for the open test in two cases:
- its audit, the record of its runs, says the last one failed clearly;
- you have just watched it fail here: the open test’s run in your node has reached a clear failure, such as CYCLING or SPIRAL FAILED.
The second case needs the test saved. With an unsaved recording, the file on disk is not what failed, so a minimisation would work on something else, and the button says to save first. Either way the panel’s first run confirms the failure in the test harness, and that run is what the minimiser holds. A run in your node is never written into the test’s audit, which stays the harness’s record.
-
The laboratory’s Sandbox card: the same rule. Its Minimise offers the displayed tests whose last run failed clearly. A test that passes, is stale or only timed out is not among them. A plain timeout says nothing about what went wrong, so there is no failure to hold. A cycling Spiral is reported as
Cycle, which can be minimised.
It works on a copy, wherever the test lives. The panel copies the test into a work area under
tests/.minimise/, and everything the minimisation runs happens there: the first run, its output, the
candidates and the result. Nothing is written beside the test, so a regression test’s history is not touched
until you decide what to keep.
First it has to know what the reduction must preserve, and that is simply how the test fails now. The
panel runs the copy once, within the suite’s budget (SPIRAL_TEST_TIMEOUT), and says what it saw, in the
harness’s own vocabulary:
Resultwith the exactValidation check …text;Crashwith the last input record;Cyclewith the projects and resources its repeating records write to;Perf,LogicorAssertion.
A pass, a stale test or a plain timeout has nothing to hold, and the panel says so. A run the panel has seen before on the same text is remembered, so a second visit goes straight to the choice. The failure is the thing being isolated, so a reduction that cures it, or changes it into a different failure, is rejected and put back. There is nothing to author and nothing to edit.
Then press Minimise, holding that failure. The candidates run in the test harness, which is Spiral on its own, with no database or Helix behind it, several at once. They do not run in your node, so the open test stays open while it works. The minimiser:
- runs the reference twice — both must meet the objective, or there is nothing to anchor;
- pins every entity the conclusion, the objective and the played commands name, plus everything they reference (resources, desks, hubs);
- removes the rest of the starting position, collection by collection, coarsely first and then by binary search, keeping only what the objective survives without;
- does the same for the commands;
- confirms the result three times.
Then you keep the result, one of two ways:
- Overwrite the test. The test is replaced by its minimised version and its history starts fresh. Its run record, its outputs and any crash core go, because they describe the test it was.
- Save it as a new test. The name is relative to the test’s own directory, so
irt2-mingoes beside it and../development/irt2-mingoes on the bench. It cannot leave the tests tree or replace a test that exists.
The result carries its provenance on the first line: what it came from, how many lines became how many, how many runs it took, the Spiral it ran on and what it holds. A best-so-far result is kept after every successful reduction, so Interrupt — take the best so far leaves a valid, smaller test to keep, though not one confirmed three times. While it works, the panel shows the iterations so far, how many were successful reductions, and the stops taken out so far.
A timing target (Perf) is a measurement rather than a statement, so candidates run one at a
time — racing them on one machine changes the very thing being measured — and even then a reduction can slip
under the threshold by luck. The three-run confirmation is what catches that.
A cycle target is a state, not a stopwatch, so its candidates run in parallel as usual. Three rules keep it honest:
- The projects and resources it names are pinned, because a world without the flapping stop’s project is a different world, not a smaller one.
- A candidate holds the target if it cycles on at least one of them. The smaller world may flap the project against a different resource, and that is still the fault.
- A cycle can begin late, so each candidate is given at least one and a half times the gate run’s own time.
The reduction is worth the wait. A full-size AA extract of 8008 position entities came down to 225 in 123 runs and five minutes; a small recorded test typically ends at one project, one resource and no zones in under a minute.
7. Promote
Section titled “7. Promote”Promote moves a test along the lifecycle and records why. Reaching regression/ passes the reliability
gate — the last 10 recorded runs all passing on this node’s Spiral — and the dialog shows that window
before you commit to it. A human decision can force past the gate; the force is written into the journal
with the reason.
challenge/ is the exception: a test there leaves only to retired/ or current/, by hand.
The sandbox
Section titled “The sandbox”A test run and a minimisation both replay tests outside your node, in the sandbox: Spiral on its own, several at once. A minimisation’s first run, the one that finds out how the test fails, is its first stage. A node runs one sandbox activity at a time. There is no queue and no jumping in: the activity has to be stopped, or finish, before another can start. While the sandbox is busy, nothing offers to start another. Instead you see what is running and how to stop it.
You see it in three places, and only while it is busy:
- In the laboratory, the Sandbox card. It is always there, with the same shape, and its head says what
the sandbox is doing: available, or the activity.
-
Available, it offers its two options:
- Run: Standard, Full or Visible, with the Spiral to run them on.
- Minimise: a choice among the tests displayed whose last run failed clearly. It is not offered when none has. A minimised result waiting to be kept is noted here too, with Look in.
The last run’s outcome, its failures and the harness’s tally stay beneath the options.
-
Busy, the progress takes the card over:
- A test run shows its bar, its counts and its failures so far, with Stop. A stopped run keeps the
results it had recorded. Any test whose result differs from its previous run is listed first, under
Changed, as
from → to, passes included, and a failure that changed carries awas …tag. The same list heads the last run’s outcome once the run has finished. - A minimisation shows its first run, or the stage, iterations, successful reductions and the stops taken
out so far (
stops 12940 → 4120 (−68%)). It has Look in, which opens the Minimise panel, and Interrupt — take the best so far, which ends it at once and leaves its best result so far to keep.
Stopping it returns the card to available, so you can choose what to run next. Run on the test cards is unavailable meanwhile.
- A test run shows its bar, its counts and its failures so far, with Stop. A stopped run keeps the
results it had recorded. Any test whose result differs from its previous run is listed first, under
Changed, as
-
- On the dispatch screen, beside the tuning controls. Press it to look in.
- On the landing page, inside the Spiral Laboratory tile.
The activity belongs to the node, not to a page. Leaving the dispatch screen or the laboratory changes nothing, and it is there when you come back. Stopping the node stops it.
8. Test a new Spiral, or a changed domain
Section titled “8. Test a new Spiral, or a changed domain”This is what the library is for. A new Spiral release, or a changed domain, must pass all of
regression/.
- Three run scopes, as three exclusive buttons. Standard is
current/,regression/andnew/— the stages a release is judged on. Full is every stage, the bench and the retired included. Visible is exactly what the console is listing, which is how you re-run a handful without composing anything. The running scope stays depressed until it finishes; stopping is a separate action and asks first, because the results already recorded still count. - The filter is a search, and it goes through the directories. Type a word and the laboratory looks at
every test at or below the directory you are standing in, matching the path or the comment. The cards
are this directory’s matches; each stage below it reports a count instead of its contents, so
stbyat the top of the library draws no test cards — there are no tests at the top — but shows that it is 74 tests inregression/and 3 inchallenge/before you enter either. Step into one and you have its matches as cards. Visible runs the whole set, wherever it lives, which the node works out for itself. The stage cards are never filtered away, so a search can never strand you. And the filter is yours: the laboratory never clears it, it holds as you step through the directories, and it is still there when you come back. - Output, on a card, shows Spiral’s stderr from the last run — where a crash names what it was doing, and where the version banner sits. It is read-only and rewritten by every run: the run file is the thing you edit, through Edit. These files are large (the median in a mature library is around 190 KB), so what you get is the head and the tail with the middle elided and named, and the whole file’s path given — the banner is at the top and the failure at the bottom, which is what it is opened for.
- Candidate binaries. Point a run at a Spiral build that is not yet the one Helix runs: put the builds
in a directory, pass it as
HELIX_CANDIDATES_DIR_HOSTtohelixctl … devortuning, and pick one from the header. The laboratory probes each for its version. A candidate’s run never changes the engine the node itself executes, and never proposes a move — the gate is about the Spiral you actually run. - History. Every completed run records the Spiral version and commit, the core version, the harness revision, the executable, the seconds taken and a checksum of the domain file it ran against. History lists every version the library has seen and compares two of them: new failures, fixes, unchanged, and the verdict — accepted, or N regressions. A domain change shows on the same axis, because the domain is part of the record.
9. Review the costs — Full / Developer mode and auto-allocate
Section titled “9. Review the costs — Full / Developer mode and auto-allocate”Switch to Full or Developer mode from the top-bar settings menu (☰ → Mode: Full / Mode: Developer). Normal mode is the operator view; the tuning detail only shows in these.
Auto-allocate asks Spiral to re-place a stop freely and — crucially — to explain itself: right-click
a stop row (or Ctrl+right-click its map marker) and choose Auto Allocate. Helix sends a force coerce
with rid: 0 (recalculate the resource; did: 0 for desk assignment) and the tagged reply comes back as a
Spiral reply dialog:
- Warnings are translated through the domain’s
helix.warningtemplates — the operator-facing reasons a placement was refused or penalised. simpleSolns(Full/Developer only) is Spiral’s raw solution set — the candidate resources it considered with their solution data, as an expandable JSON tree. This is the primary cost-review surface: compare the candidates Spiral weighed and what separated them.
Per stop, the Schedule card shows what the current solution cost: dep / arv / fin, travel
(tvl/dst), the solnTier the solution landed in, cbt (Full/Developer), and Spiral’s info
messages. Developer mode additionally shows each tree node’s stop id.
10. Inspect the runtime domain in the JSON editor
Section titled “10. Inspect the runtime domain in the JSON editor”Open the JSON Editor (☰ → JSON Editor). The entity buttons include the Define set — Stop
Parameters, Resource Parameters, Customer Parameters, Contract Parameters, Topography — and Set
Parameters, each seeded from the live reference record of the running system. Entries appear as
defined, each with its clone still in place; double-click a stop or resource on the map to see the
parameters it resolves to. The Dispatch Stream Viewer below it dumps the whole live in-memory database.
Use it to confirm what the running system was actually given, to follow a clone chain (they can
surprise), and to copy the current stopParams block as the starting point for your tuned version.
The Define verb does not apply live. The editor lets you modify the JSON, but an interactive
definepost is rejected by the CRUD (define not available in crud mode): the domain reaches a running Spiral only through file replay or a restart. Validation therefore happens by replay — see below.
Look up an attribute’s documentation
Section titled “Look up an attribute’s documentation”Click any key in the JSON Editor or the Tune panel, and the editor’s footer shows where it sits
(stopParams › JOB › helix › icon) with a ? button. ? opens this site’s reference entry for that
attribute in a separate documentation window, which you can move and size beside Helix. While that window
is open it follows each key you select, so you can walk a block and read as you go.
If the key is a name you chose (a stop type, a template, a duty code, a skill domain), you land on the definition of the object that holds it. An attribute the reference does not describe yet lands on its containing definition too.
Every Helix link to this site (the user guide, the ? buttons, the tutorials) opens in that same window.
The Tune panel — edit the domain file in place
Section titled “The Tune panel — edit the domain file in place”In Full or Developer mode on a solo system, the auto-allocate Spiral reply dialog offers a
Tune… button (admin login required). It opens the client domain file itself — comments and all — as a
navigable tree, positioned at the current stop’s stopParams entry:
- Follow the clone chain: a
clonereference is a link — clicking it jumps to the referenced definition and pushes your position onto a stack (the breadcrumb shows the chain, e.g.booked-2 ▸ appt-2 ▸ immediate); ⬅ Back returns. This makes it quick to find where an inherited cost actually comes from. The jump stays inside the entry’s own catalog — a key is only unique there, and a file may well carry aDEFAULTinresourceParamsand another incontractParams— preferring the entry’s own array when a catalog is split over severaldefineblocks; a clone on a row inside an entry (a contract’s duty rows) resolves to a sibling row. - Edit values in place: click any value, type, Enter. Edits are applied surgically to the file text, so comments and layout are preserved — only the value you changed is touched.
- Look it up: click a key, then ? (see above). The panel’s header ? opens this section.
- Save and Reload saves the file and restarts Spiral: the domain is re-read from disk and the app’s
parameters refresh, so the next auto-allocate runs under the tuned values. (A one-off
.bakof the original is kept alongside.)
The panel also opens from the entity editors in Full/Developer mode, from a Definition row of
chips: the stop editor’s names the stop type and the customer (Tune RSS, Tune RSS-AA PERSONAL), and the
resource editor’s names the resource type and its contract (Tune MEDIUM_VAN, Tune PBC); the project
editor’s footer has Tune <type>. One tuning window exists at a time.
Structural changes (adding keys or entries) belong in your editor or the JSON editor’s define flow — the
Tune panel is for the tuning loop: costs, times, factors.
11. Validate and land the change
Section titled “11. Validate and land the change”Two equivalent validation routes; both end by replaying the same test and comparing decisions:
- Domain-file route (recommended): edit
<CLIENT>.domain.json— adelay.cost, anoSoln.costStop, an optiont. In dev and tuning the domain is read live from the mounted repo, so the change applies on the next load: reopen the same test and step to the same moment. - Test route: for a self-contained experiment, copy the tuned
defineblock into a copy of the test, ahead of the recorded commands — replay applies defines in-stream, so the recorded traffic runs under the modified domain without touching the real file.
Compare before and after: the same auto-allocate on the same stop — did the candidate ranking change as
intended? Did solnTier improve? Did the warnings disappear? Stepping makes the comparison precise,
because you can halt at the identical tag in both runs.
Then make it permanent:
- Dev: already done — the dev loop reads
<CLIENT>.domain.jsonlive from your repo (dev loop); commit it. - Production: rebuild and ship the client bundle —
helixctl release <version> --as bundlepacks the extension and the domain config, thenhelixctl import+start <type> <artefact>on the node (build & deploy).
Finally, run the library against the changed domain. The domain checksum is part of every result, so “passed on this domain” is on record.
12. Probe a vehicle’s track — Spiral and the travel calculator, fix by fix
Section titled “12. Probe a vehicle’s track — Spiral and the travel calculator, fix by fix”A resource heading to a stop reports its position every few seconds, and each fix makes Spiral re-estimate the drive. With the OSRM travel calculator on, that estimate has been seen to lengthen or wander in ways the vehicle’s progress does not explain, and a stop deassigned as a result. The probe makes that visible: the same fixes reach Spiral in the same order at the same clock, with a settle after each, on the bare engine with the calculator on and then off, and a table shows what the plan did after every fix.
The intent it tests. The calculator should correct future plans and have little effect on plans in progress — a stop that is deployed, heading, committed or arrived. So the figure that matters is the number of rid changes on in-progress stops with the calculator on, less the same number with it off. It should be zero.
Make the run file. The best source is a Spiral record stream — a node’s spiral.log, or any .err.json the
harness wrote — because it carries the clock on every command Spiral was given ({"@":T,"IN":…}, with debug
4 or more). A run file or a crud2spiral.log is untimed; it works, with --interval to space the fixes.
node helixCore/avlRun.js make --from spiral.log --resource 1854604495 --window 1705496000,1705498400 \ --out tests/development/avl-1854604495.run.jsonnode helixCore/avlRun.js synth --base tests/regression/avlupdate1.run.json \ --route 2089458407:148423236,2089464644:148422887 --every 60 --speed 50 --out tests/development/avl-two.run.jsonmake replays the world to the window as it happened, then per fix of the resource (or --resource R1,R2, or
all) writes a set time, the fix tagged from 1001, and a settle. synth takes a base world’s starting position
and moves two patrols along straight lines to their stops at one cadence, fixes interleaved — the shape in which
two similar journeys can be swapped between patrols — with --offset (metres to the right of travel: a wrong
carriageway) and --jitter (seeded noise) to perturb them. Both land in tests/development/, the bench.
Run it both ways.
helixCore/bin/avlProbe tests/development/avl-two.run.json # calculator from $SPIRAL_TRAVEL_CALCULATOR, then '{}'helixCore/bin/avlProbe tests/development/avl-two.run.json --threads 1 --map my-precision-0.0001.map.jsonhelixCore/bin/avlProbe tests/development/avl-two.run.json --exe ~/release/spiral.l64 --exe-b helixCore/spiral.l64The driver copies the experiment to <stem>.osrm and <stem>.flat, runs each through the core’s own testSpiral,
and records every byte Spiral exchanged with the calculator in <stem>.osrm.travel.in.json and .travel.out.json
through a shim it installs for the run. --threads 1 makes the order of requests and replies certain; --map is a
replacement map, for example one with precision 0.0001; --exe-b runs a second engine for a four-way comparison.
Read the table. One row per fix. imm is the drive time Spiral recomputed synchronously on the fix, on its
internal estimate — the coarse value; opt is the first settle, set the last; c→f is set − imm, the delta
between coarse and fine. Δtvl is the change in the settled drive since the previous fix of the same stop, exp is
what elapsed time alone would give (minus the seconds between fixes), and dev their difference. eng.t is the
calculator’s own answer for that fix to that stop, and req lists every request the calculator received from that
fix, as stop:seconds — which is how you see Spiral pricing a candidate assignment: a patrol’s fix asking for
the drive to the other patrol’s stop, with the fine value, while the patrol already heading there stays on the
coarse one. changes lists every in-progress stop whose rid changed in the step, stop:from→to, whoever ends
up with it.
| flag | meaning |
|---|---|
DEASSIGN | an in-progress stop changed rid in this step; changes says which |
SWAP | two in-progress stops exchanged rids in one step |
SWAP_BACK | a change that reverts one from the last three steps — the wobble, two changes a stop |
TVL_UP | the settled drive rose by more than a minute while the stop stayed with its patrol |
DEV | the drive moved by more than two minutes against what elapsed time explains |
SHAPE | drive time rose while distance fell, or distance rose while the fix moved toward the stop |
SPIKE | a TVL_UP that was back within a minute of the earlier value two fixes later |
IMM_NE_SET | coarse and fine differ by more than a minute |
OSC | the plan went un-optimised and optimised again more than once within the step |
CELL | a TVL_UP or DEV that coincided with the fix crossing a precision cell |
ARV_GT_LATEST | the settled arrival is after the stop’s latest |
The summary under each table gives the in-progress rid changes, the largest and mean rise in drive time, the
spread of dev, and the number of calculator requests; the comparison puts the on and off rows side by side and
prints the one figure the intent is judged by. Everything is also written beside the run file as .avl.json and
.avl.csv.
What it is not. Evidence, not a mitigation: nothing here changes what Spiral does with a fix. A HEAD stop’s first value is the input’s own — Spiral initially calculates non-deployed stops only — so the first row of a stop has no previous to compare with and raises no flag.
Coarse against fine over a country. The same tools measure the two estimators without any fixes at all:
pairs builds a world of N resources each standing at an origin with one stop at a destination, locked to it,
and the probe runs it on and off; the off run’s tvl on each stop is Spiral’s coarse drive and the tee capture
holds OSRM’s reply for the same two points, which the analyser divides by the calculator’s speedFactor to give
the fine value Spiral would put on an unstarted stop. Spiral asks the calculator for about a hundred drives in a
run, so a thousand pairs are ten batches, pooled with --out.
for k in $(seq 1 10); do node helixCore/avlRun.js pairs --base tests/regression/avlupdate1.run.json \ --resource 2089458407 --project 148423236 --positions stops.txt --n 100 --per-cell 4 --seed $k \ --earliest 0 --settle 0 --out tests/development/avl-pairs-b$k.run.json; donefor f in tests/development/avl-pairs-b*.run.json; do helixCore/bin/avlProbe "$f"; donenode helixCore/avlAnalyse.js pairs --out tests/development/avl-pairs-uk tests/development/avl-pairs-b*.run.json--positions is one [lat,lng] a line — a client’s own stop positions, say — and --per-cell caps the
destinations per 1° cell so one city does not fill the sample. The table gives the ratio coarse ÷ fine by
distance band, by latitude and by longitude, with the ratio to OSRM’s raw duration beside it: a systemic drift is
a ratio that moves with one of them; a flat ratio away from 1.0 is the speedFactor. On the AA’s map, Spiral’s
coarse drive matched OSRM’s raw duration to within 1% over 50 km and ran over it under 25 km, so at a
speedFactor of 0.75 every fine drive over 50 km came back a third longer than the coarse one.
One harness, one binary
Section titled “One harness, one binary”testSpiral is a helixCore component, shipped with the core and present in the image at
/app/helixCore/testSpiral — one place to maintain it. A client repo does not commit a copy. Pull the
harness the image actually carries:
./helix/runtime/helixctl harness # → tests/testSpiral + testSpiralRun + spiralRunDir + spiralCycle.js + the lifecycle directoriesBoth land in tests/, which every client .gitignore ignores, so they can never be committed and can
never drift from the core. That copy is yours, for running tests from a shell. It is the harness of the core you
are pinned to, so re-run helixctl harness after moving to a new one.
The laboratory never uses it. Run, the minimiser and its first run always use the harness of the core the node is
running, against that node’s own Spiral. The laboratory’s results, its cycle detection and how a stopped run is
recorded therefore always match the code reading them, whatever was pulled into tests/.
Running it
Section titled “Running it”cd <repo>/tests && ./testSpiral # the standard suite: current/ regression/ new/cd <repo>/tests && ./testSpiral -c # current/ only (-r for regression/)./testSpiral tests/current/t1.run.json # one test, named by path, from anywhere in the repotestSpiral is a bash script. Beside each Spiral it starts the optional cycle watcher, node spiralCycle.js watch (see Cycling tests), and stops it when the run ends. With no node on the machine,
nothing changes.
A run that is stopped part-way, by the sandbox’s Stop, a batch’s Stop or Ctrl-C, is closed in the audit as
stopped. It has no verdict, so it counts as neither a pass nor a failure, and its card stops showing it as running.
A run file may be named however you have it: a bare name (current/t1), the same with its .run.json suffix,
a relative path, or an absolute one. The run’s .err.json, .out.json and .audit.json always land beside
the run file, whichever directory you ran the harness from.
Spiral runs in a scratch directory beside the harness (tests/.work/) that links to your client’s
config directory — the one holding <CLIENT>.domain.json and <CLIENT>.map.json. A run file names them
bare and Spiral resolves a bare include against its own working directory, so the links make it resolve to
the real file — under its real name, so a stale include still fails as Stale — while nothing Spiral writes
lands in the repo: a crash core is moved beside the harness as tests/core.<test> and the result line
names it. The harness finds the config directory in this order, and refuses to run if it finds none,
rather than starting Spiral with no domain:
$SPIRAL_TEST_CONFIG_DIR | you name it |
$HELIX_CLIENT_PATH | inside a container, the unpacked client |
<repo>/helix/custom | the ordinary client repo — nothing to set up |
<repo> | older trees whose domain sits at the top |
| a client tar | $HELIX_CLIENT_TAR, or a *_helixConfig.tar beside the harness — unpacked once into a cache |
That last row is what lets a machine with no working tree at all run the library: a host that has only the installed artefact and a core image can still replay the whole corpus.
Which Spiral it runs
Section titled “Which Spiral it runs”$SPIRAL_EXE if it names an executable; else a native spiral.l64 beside the harness or in the repo’s
release/; else a spiralOnly drop-in, which runs the pinned core in a container. So a client repo with
no local Spiral build needs nothing extra — the tests run against exactly the engine the client is pinned to.
With none of the three it says so instead of running nothing.
The drop-in runs within a few seconds of native on a host whose image store is a kernel overlay. On a
rootless host whose store is fuse-overlayfs every file operation inside the container is a round trip to a
user-space process, and one thing Spiral does continuously is exactly that: with TZ unset, glibc stats
/etc/localtime on every time conversion, thousands a second, which halved the drop-in’s throughput on one
host. Spiral 2.5.4 pins TZ at start, so the check becomes a string compare and the file is read once;
helixctl check shows the storage driver it found. A drop-in that runs at half native speed on a
fuse-overlayfs host is on a Spiral older than 2.5.4.
Point both Helix and the harness at the same engine when you are tuning against a new Spiral build:
export SPIRAL_EXE=~/Honeywick/Helix/helixCore/spiral.l64Helix probes the binary it actually runs, so the About dialog reports the truth either way; and the laboratory’s own batch runs always use the engine this node runs, unless you pick a candidate.
Related
Section titled “Related”- Capture and extract — data logging, the vault, and building a test from a live system at a chosen moment
- Develop a client extension — the dev loop, domain-driven UI, deploy
- stopParams — the cost-model attributes (delay, noSoln, quality, appt …)
- input object —
check,refute,pause,tag - Scenarios — scripted demonstrations (as opposed to recorded replays)
- travelCalculator and perf
— the calculator’s settings and the
travelCachefigures the probe reads