Three marker kinds, a trailing row marker, and a body that is regenerated around its row blocks
Decision record 0009
Amended by 0120 and 0121: the root marker gains the optional keys
runs-failed,runs-failed-newestandruns-failed-at, last, and the row marker gainsdrift-check, last,failedon a row whose drift check failed. Both are display facts; nothing is decided from them but whether a push checks a stack’s drift again.Amended by 0117: the row marker gains the optional key
busy, last,trueon a row of the statepreview-failedwhose stack another update held the lock of when the scan ran. A display cache likefailed.Amended by 0110: the row marker gains six optional keys.
creates,updates,replacesandtrackingafterpolicy, the counts of a pending row’s first line, each left out at 0;changedright aftergone, how many resources a drifted row’s drift check found changed; andbehindlast, the stack ids a queued row waits behind, the display cache this record foresaw. All documented. The version stays 1.Amended by 0108: the root marker gains the optional keys
scan-runningandscan-running-since, after therun-waitingkeys, for a scan that is running. The scan writes them first and takes them away last, and every other writer carries them. Left out of the documented keys on purpose, like therun-waitingkeys.Amended by 0096: the markers are documented for readers outside Sluiceway in
docs/what-sluiceway-writes.md, with the version rule said for them. Every key is documented exceptshortenedon a row, therun-waitingkeys on the root marker, and the notes and confirm keys of a bulk box, which may change without a new version.Amended by 0086: the root marker gains the optional keys
run-waiting,run-waiting-sinceandrun-waiting-more, after the full scan keys, for a run of the workflow that waits for a runner. A scan writes them and every other writer carries them.Amended by 0083: a marker of the kind
sluiceway:bulksits on its own line outside the row blocks, under the pending or the drifted rows: the bulk box of that section, or its confirm box with the stacks and hashes it names.Amended by 0075: the row marker gains the optional key
deletesright afterdestroys, how many of the destroys are deletes, written wheneverdestroysis. A drifted row’s marker gainsgoneafterdrift, how many resources its drift check found gone outside the code. Both are display caches for the header and the destroy alert, likedestroys.Amended by 0059: the row marker gains the optional key
depends-onafterdrift, the stack ids a preview of a stack withdependsOn: autoread from its stack references.resolvereads it for such a stack, a second exception that, like the first, can only hold a deploy back or order it.Amended by 0054: a marker of the kind
sluiceway:mergesits on its own line outside the row blocks, one per update waiting to merge, with the keyspr,stackandhead.Amended by 0055: the state
driftand the keydrift="true"arrived. The precedence is deploying, preview failed, pending, drift, in sync.
Record 0004 made the body a cache of row blocks and said each block’s marker carries stack id, hash and row state. This record fixes the format. It has to let drift rows and “queued behind X” arrive later without a breaking change.
Amended by 0027: the row marker gains two optional display cache keys after
hash,destroysandfailed.Amended by 0028 (settled while building slice 1.8): a third optional display cache key follows them,
shortened, the level of a shortened row.Amended by 0035 (settled while building slice 2.6):
settledoes not write the body. It starts a full scan, which writes the rows of the stacks whose records it ended.Amended by 0056: the
queuedstate is used, placed and counted with deploying rows.resolvereads a dependency’s row state to refuse a tick while it is pending, the one exception to “no mode decides anything from it”, and it can only hold a deploy back.
Markers are HTML comments in one namespace, sluiceway:<kind>, with key="value" pairs. There are three kinds in v1:
<!-- sluiceway:dashboard v="1" scan-sha="294bbc0..." scan-run="1234567890" scan-at="2026-09-20T06:00:12Z" -->
- [ ] **apps/grafana:prod** · `+2 ~1 -0` · [preview](run-url) <!-- sluiceway:row stack="apps/grafana:prod" state="pending" hash="3fa9c1e2aabbccdd" --> (attribution, details, failure line) <!-- /sluiceway:row -->- **apps/loki:prod** · deploying · [run](run-url) <!-- sluiceway:row stack="apps/loki:prod" state="deploying" --> <!-- /sluiceway:row -->
- [ ] Rescan all stacks <!-- sluiceway:rescan -->The open row marker sits at the end of the row’s first line. Renovate’s placement, directly after the checkbox, was rejected after a render test: on a row with no checkbox the line would start with the comment, GitHub reads that as an HTML block, and the rest of the line shows as literal **apps/loki:prod**. A marker on its own line under the row also renders, but then a tick needs two lines to parse and a block starts one line before its own marker. At the end of the first line, one placement serves every row and a tick is one regex on one line, anchored on the box at the start and the marker at the end. The visible text between them is never parsed.
JSON and base64 payloads were rejected. JSON needs the same escaping plus nested quotes, and base64 is opaque in the raw body and a third larger. With pairs, an id such as apps/grafana:prod reads as itself.
Consequences
- A row block is every line from the one holding the open marker through the one holding
<!-- /sluiceway:row -->, which is the row’s last line, indented, with no payload. “Until the next marker” was rejected as the end of a block, because moving the last row of a collapsed section would take the closing</details>with it. The cost is about 25 characters per row. - Values are percent-encoded as UTF-8 bytes with upper case hex:
%,",<,>, and every byte up to0x20plus0x7F. Nothing else. A value can then never close the quote or the comment. A render test showed--inside a comment is harmless. - Row keys are written in the order
stack,state,hash(thendestroys,failedandshortened, see the notes at the top), and root keys in the orderv,scan-sha,scan-run,scan-at, so output stays byte-identical. Parsers do not depend on order. Line endings are normalized to\nbefore parsing. - The row state is a cache for placing and counting rows. No mode decides anything from it.
resolveacts on a row when its box is ticked, discovery knows the stack id, the stack has no open deployment and the ticker is authorized.applydeploys only if a fresh preview still gives the hash on the row. A wrong state can at worst misplace a row until the next scan. - The v1 states are
pending,deploying,in-syncandpreview-failed, in this precedence: an open deployment givesdeployingwhatever the preview says (0003), else a failed preview givespreview-failed, else a diff that is not empty givespending, elsein-sync. - One rule makes additions safe. Every writer ignores marker kinds and keys it does not know, carries a row whose state it does not know through byte for byte, never acts on a tick on such a row, and leaves it out of the counts it knows. Such a row is placed in a plain list at the end of the body. This can only happen after a downgrade.
- Drift later adds the state
driftfor a row with drift only, and a key on the row that says the hash includes drift, soapplychecks drift again before it compares (0008). A stack never has two rows. “Queued behind X” later adds the statequeued. The fact itself is a deploy fact: a deployment record with statusqueuedand the blocking stack ids in its payload (0003). Abehindkey on the row would only be a display cache. None of this changes the version. - The body is a function of the root facts, the row blocks and the deployment records.
resolve,applyandsettleparse only the root marker and the row blocks, swap their own blocks, and regenerate everything around the blocks with the renderer the scan uses: header, counts, mascot state, section headings, empty section copy, recently deployed, footer. Nothing outside a row block is patched or carried through. - There are no section markers. A section is the set of row blocks whose state maps to it, sorted by stack id. Sections can be reordered, renamed or restyled without touching this format.
- The root marker is the first line of the body. It carries the format version and the scan facts the header shows, because writers other than
scanhave no scan of their own to take them from. New header facts are new keys. - The version is one integer, on the root marker only. Rows carry none, since every scan rewrites all of them. It changes only when an older parser would misread the body, never for a new key, state or kind. A
resolve,applyorsettlethat meets a version other than its own does not touch the body.resolvedispatches a scan, which regenerates the body in its own version and clears the outstanding ticks with the note from 0005. Deploy safety is not involved, because it never rested on the body. - The dashboard is found by label, root marker and author together.
- A bare issue or pull request reference never sits on a row’s first line, because GitHub ticks a task that references an issue when that issue closes. Attribution goes on a line inside the block.
Research: