art: make the deck the unit, so nine frames read as one deck - #319
Conversation
…re a deck
The owner reported on 2026-09-16 that the carousel artwork does not flow together and
that each frame reads as text boxes thrown on a page. Four measurements under that:
- txpost.js, the film grade, was loaded by 1 shipped slide of 205. The sibling
product loads its own copy on 127 of 127. txcolor.js ran 1 of 205 against 118
of 127. The finishing pass was in this repo the whole time and was never called.
- No deck ever had a chassis. Every frame reached into a permanent bin of finished
parts, which gives uniform parts and no unity of world. The sibling writes one
chassis per run, named for that deck's world, and all nine frames draw from it.
- Six frames of nine put an opaque plate behind the headline.
- Median adjacent value jump across nineteen shipped decks was 21.0 L* against the
sibling's 2.6. Ninety percent of sibling decks carry at most one hard cut. Not
one deck here did.
assets/js/txdeck.js holds the three invariants a deck can't keep by good intentions:
one light, one material ramp, one way of seating type, plus finish(), the one line
that grades a frame with the deck's own grade.
scripts/carousel/deck_chassis.py reads the source and refuses a deck whose frames
don't share a chassis, skip the grade, grade themselves, redeclare the light, or put
a plate behind type. It finds 33 defects in the 2026-09-16 deck.
scripts/carousel/deck_coherence.py reads the pixels and refuses a deck that strobes.
Its thresholds are derived from a 58 deck reference corpus and recorded with their
derivation in config/carousel/deck_coherence.json, never typed.
ownership.yaml carves assets/js/deck/** to the daily lane so a run can build a world
while assets/** stays human, so a run still can't edit the workshop.
The rotation rule was mandating the defect. It required nine different layouts, at least five distinct, no archetype twice in a row, and a screen that varies with the layout. The machine was obeying it correctly. It was told the wrong thing, and the owner's report that the slides do not flow together is what that rule produces. ILLUSTRATION_SYSTEM.md gains THE DECK IS THE UNIT, which outranks every per frame rule under it: the chassis law, one light and one ramp and one grade and one screen for the whole deck, a continuity mandate of at least two devices named in the storyboard, and no plate ever. The rotation is rebalanced rather than deleted. At most two of the same archetype in a row (two is a beat, three is a rut), at least three distinct. The image law did not move, because the defect the old table was written for was never too few layouts, it was no image. The rubric splits the art question in two. artwork_craft 0.28 to 0.22 asks only about a frame and no longer asks for a different layout on every one. A new deck_coherence at 0.12 asks whether the nine are one deck and tells a judge that a spread is a property of a set while a deck is a sequence. Art carries 0.34 between them, up from 0.28, and claim_integrity did not move. The threshold goes 6.7 to 8.0. The last fourteen runs scored 6.56 to 7.58, so the bar sat below the mean of the thing it judged and never once asked for another round. The sibling holds 8.3 on the same agents and engine. max_rounds still bounds the search, so a run can still ship, honestly, under the bar. layout_check gains --prose, which refuses any surface still stating the superseded rule. It finds two critic definitions under .claude/ that no routine may write, the flow critic among them, so Phase 12 now hands the flow critic the current rule at spawn time and UPGRADE_BACKLOG.md carries the four line fix for a maintainer.
examples/lamp-deck/ is the 2026-09-16 story rebuilt in the new system, same claims and same copy, so the two contact sheets are a clean A/B on artwork alone. Measured, old deck to new: mean adjacent value jump 23.83 -> 5.39 L* (sibling median 2.6, its p75 6.45) value spread 59.9 -> 19.1 hard cuts (>25 L*) 4 -> 0 opaque plates behind type 6 of 9 frames -> 0 frames loading the grade 0 of 9 -> 9 of 9 machine QA failures 0 -> 0, via 34 at the first build The deck now holds one world, one lamp at az 64 el 26, one ramp, one screen and one grade across nine frames, and the camera moves through that world rather than the world changing per frame. Two continuity devices run: the camera returns to frame 1's exact position on frame 9, and the hint tick evolves one, two, two, three and then absent, so the close argues the claim by not drawing it. ILLUSTRATION_SYSTEM.md gains the eight things this build paid for, among them that a hole punched in a light layer is a plate with the sign flipped, that a frame's two kinds of type want opposite things from the light, that a mid ground is the worst ground and no ink fixes it, that canvas text can't be registered to DOM text in a variable font, and that a frame whose QA goes sideways across rounds wants recomposing rather than tuning.
…achability guards.yml gains five steps: both new gates' self-tests, the reference chassis deck against deck_chassis, its bespoke reading recorded rather than enforced, and the prose agreement check. The bespoke step is deliberately non-blocking and says why in the file. A chassis deck scores HIGHER on pairwise similarity than a deck of nine unrelated drawings, because nine frames sharing one world share a drawing vocabulary. The sibling's own chassis decks measure 0.36 and 0.45 with closest pairs at 0.93 and 0.76, this one sits inside that range, and the old incoherent decks scored LOWER at 0.30. That step exists so a future run reading the WARN does not close it by dismantling the chassis. The prose gate separates the surfaces this repo can write from the two agent definitions under .claude/, which ownership.yaml gives to upgrade and which the host lets nobody write unattended. Reachable surfaces FAIL. The blocked ones are reported loudly with the backlog item that closes them. Failing CI on a path nothing in this repo can fix would stop every run for a defect no run can close, which is the shape of the mistake CLAUDE.md spent six wedged days learning.
Raising the threshold to 8.0 turned three local steps red, and all three were right to go red. run_complete pins the bar in its own self-test on purpose, so it cannot drift a tenth at a time with nobody able to name the run that moved it. The pin did exactly its job: the rubric edit alone went red and the bar could not move without a second deliberate commit naming it. Pin updated, with the move recorded beside the 2026-09-13 one. panel's ship:false case used judges at 7.5, which cleared the old bar and does not clear the new one, so the case stopped being the one under test and became an ordinary threshold dissent. Scores moved to 8.6 and the comment says why, because the next person to move the bar will hit this again. shipped_check ran the current bar against every deck ever published and marked 2026-08-20, 2026-08-21 and 2026-08-22 as never having shipped, at 7.42, 7.42 and 7.09. All three cleared the 7.0 in force when they shipped. None of them changed. That file already settled this principle on 2026-09-13 and implemented half of it: a deck that RECORDED its threshold answers to that one. The decks that recorded none fell through to 'the current one', which was harmless while the bar only ever went down. This is the first time it went up. BAR_HISTORY gives those decks the bar that actually applied, read from the rubric's own git history and from run_complete's docstring for the pre-rubric 7.0, and anything after the last entry still answers to the rubric as it stands.
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
tests/txlayout.mjs asserted that two of the same archetype in a row FAILS, which is the rule that forbade the deck its strongest continuity move and which changed with the rotation rebalance. The Python copy and its self-test were retargeted with the rule and this one was missed, which is the third copy of the same numbers in this repo and exactly what layout_check --prose now exists to catch for prose surfaces. Two in a row is a beat, the same camera with the light moved. Three is a rut, and there is now a case asserting that.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0349735563
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| - name: Deck coherence self-test (a spread is a set, a deck is a sequence) | ||
| run: python3 scripts/carousel/deck_coherence.py --self-test |
There was a problem hiding this comment.
Run the new gates against the shipped deck
This workflow only self-tests deck_coherence.py; the chassis check exercises the fixed reference example, and neither module is registered in shipped_check.py. Consequently, if a daily run skips or misapplies the prompt's local commands, CI can still merge a newly shipped deck that strobes or lacks a valid chassis while the workflow reports green. Invoke both gates against the newest committed run, as the existing shipped-deck sweep does for other product gates.
AGENTS.md reference: AGENTS.md:L68-L69
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and this is gate_wiring.py's own finding in a new place: a gate nothing points at its real subject. Self-testing a checker proves it can go red; running it against a fixed example proves it agrees with a deck that cannot change. Neither asks it about the deck a run just shipped.
Fixed in 118cb84. g_deck_chassis and g_deck_coherence are registered in shipped_check.py's table as CURRENT, so the sweep that already runs every other product gate across all 26 published runs now runs these two as well.
Both carry DECK_SINCE = "2026-09-16", for the reason CONSTRUCTION_SINCE and LAYOUT_SINCE already carry theirs: no deck on or before today has a chassis because the system did not exist, and every one of them strobes by thresholds derived today. A gate does not judge the work that produced it. The sweep reports both as not-applicable with that reason printed, so the exemption is visible rather than silent, and it expires on its own with the next deck.
Generated by Claude Code
| if "TXDECK.finish(" not in html: | ||
| problems.append( | ||
| f"{name}: never calls TXDECK.finish, so it ships ungraded. This is the call that " | ||
| f"was missing from 204 of 205 shipped slides") |
There was a problem hiding this comment.
Verify that the finishing pass actually runs last
This substring test passes when TXDECK.finish( appears only in a comment, or when a slide calls it and then performs additional canvas drawing. In either case the gate reports success even though some or all artwork bypasses the deck grade, directly violating the new last-canvas-operation contract. Parse executable calls or otherwise verify that no canvas mutation follows the finish call.
AGENTS.md reference: AGENTS.md:L68-L69
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Right on both halves, and the second one is the worse of the two: a frame that grades and then keeps drawing reports itself graded while shipping artwork the grade never saw.
Fixed in 118cb84. finish_problems() now strips block comments, finds TXDECK.finish( calls that are not commented out, and refuses any canvas mutation after the last one (fillRect, drawImage, putImageData, beginPath, the gradient constructors and the rest). A mention that exists only in a comment gets its own message, because "you commented it out" and "you never wrote it" are different mistakes.
Three cases added: a line-commented call, a block-commented call, and TXDECK.finish(...); cx.fillRect(...). All three were passing before.
Generated by Claude Code
| - path: "assets/js/deck/**" | ||
| owner: daily |
There was a problem hiding this comment.
Prevent later runs from overwriting a deck chassis
The filename is derived only from a free-form world name, while this rule lets every daily run modify any existing chassis. If a later deck reuses a name such as lamp, it can overwrite assets/js/deck/lamp.js; archived slide HTML under runs/carousel/<date>/slides still references that shared path, so reproducing or checking the earlier run now executes the later deck's chassis. Make chassis paths run-unique or enforce create-only behavior so shipped artifacts retain their original dependency.
AGENTS.md reference: AGENTS.md:L37-L37
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
This is the most consequential of the eight and it was a real latent bug, not a style point. shipped_check.py re-runs the gates against every deck this project has published, so a collision would not have waited for somebody to reproduce an old run by hand — the next sweep would have re-checked an earlier deck against a later deck's light, ramp and grade.
Fixed in 118cb84 by making the path unique rather than by a create-only rule, because create-only is a rule somebody has to remember and a filename is not. Chassis files are assets/js/deck/<date>-<world>.js; CHASSIS_NAME_RE refuses anything else and says why; the reference chassis moved to 2026-09-16-lamp.js with its nine slides and the builder updated, and it re-renders clean. ownership.yaml's note carries the reasoning where the carve-out lives.
A self-test case now loads deck/caprock.js and is refused on the name.
Generated by Claude Code
| **TELL THE FLOW CRITIC THE ROTATION RULE CHANGED, IN THE SPAWN PROMPT, EVERY ROUND.** Its own | ||
| definition under `.claude/agents/` still says "no two frames in a row laid out the same way, at | ||
| least five layouts across nine" and "the print register varies with the layout". Those are the | ||
| SUPERSEDED rule as of 2026-09-16, no routine may edit a file under `.claude/`, and a critic | ||
| enforcing a superseded rule argues the deck back toward the defect it was changed to fix. So | ||
| hand it the current rule with the deck: |
There was a problem hiding this comment.
Override the treatment directors' stale rotation rule
The workaround updates only the flow critic, but layout_check.py --prose also identifies carousel-treatment-director.md as stale: it still requires at least five layouts and a screen that varies per frame. Those directors run earlier in Phase 9 and seed the dossiers, while the new Node check merely permits three layouts and therefore does not reject their old five-layout plans. Pass the same current-rule override when spawning treatment directors, or they will keep planning decks toward the exact incoherence this change is intended to remove.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and the ordering argument is the part I had missed. The directors run in Phase 9 and their pitches become the dossiers, so overriding only the flow critic left the plan aimed at the superseded rule and gave the critic something it could only complain about after the fact.
Fixed in 118cb84. Phase 9 now opens with the override, before any director is spawned, and the Phase 12 paragraph points at it so the two cannot drift. The directors are also told to name the deck's continuity devices, since layout_check --require refuses a storyboard declaring fewer than two and the directors are who decide them.
layout_check --prose reports zero findings on every surface this repo can write. The two remaining are the agent definitions themselves, which no routine may edit; that gate now separates reachable surfaces (fail) from those (reported), and UPGRADE_BACKLOG.md carries the four-line fix.
Generated by Claude Code
| // liked. A deck of nine FULL_BLEED frames with a real image on each and a spine running | ||
| // through them is excellent. A deck of nine different layouts with a headline over a small | ||
| // object is what the judges called clip art. | ||
| var ROTATION = {"max_consecutive": 2, "min_distinct": 3, "max_type_as_object": 1, "min_full_bleed_or_close_crop": 2, "min_primary_area": 0.30, "min_bleed_frames": 4, "min_continuity_devices": 2}; |
There was a problem hiding this comment.
Enforce the continuity-device minimum
The new rotation table declares min_continuity_devices: 2, but TXLAYOUT.check() accepts only an archetype sequence and never reads this key; the Python rotation check likewise ignores it. A nine-frame storyboard naming no spine, motif, camera move, or other continuity device therefore passes both executable rotation checks despite the new doctrine explicitly requiring two. Parse and count the storyboard declarations in a real gate rather than leaving this as an unused configuration value.
AGENTS.md reference: AGENTS.md:L68-L69
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, and this was the worst of the eight: min_continuity_devices went into the table and nothing read it, which is GATE_LESSONS' oldest shape committed inside the same change that added a gate against that shape.
Fixed in 118cb84. TXLAYOUT.check(seq, devices) now takes the device list and a DEVICES vocabulary of the five; layout_check.declared_devices() reads CONTINUITY: <DEVICE>, <DEVICE> off the storyboard and continuity_problems() binds under --require, alongside the layout keys and for the same reason (a deck that declares none has not planned its continuity). The devices argument is optional so a caller asking only about the sequence still can, and --require is what makes the mandate bind.
Five self-test cases now cover it: two devices passes, one is refused, none is refused and told where to write them, a name off the list is named as such, and without --require it does not bind. examples/lamp-deck/storyboard.md declares CAMERA_MOVE, MOTIF_EVOLUTION and documents how each one works across the nine frames.
Generated by Claude Code
| - name: deck_coherence | ||
| weight: 0.12 |
There was a problem hiding this comment.
Require the panel to score every rubric criterion
Adding deck_coherence does not make it mandatory in panel.py: combine() treats any common nonzero set of criteria as complete and trusts the names and weights supplied by the judges. If all three judges return the previous six-criterion schema, the panel silently computes the old weighted result and can ship without scoring this new criterion at all; the panel self-test itself still uses that legacy schema. Validate every card's criterion names and weights against scoring_rubric.yaml before accepting the per-criterion result.
AGENTS.md reference: AGENTS.md:L68-L69
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct, including the note that the self-test itself used the legacy schema. complete meant "the judges agree with each other", and three judges all returning the same stale card agree perfectly — so the panel weighted a six-criterion rubric and reported a clean per-criterion median with deck_coherence simply absent.
Fixed in 118cb84. rubric_criteria() reads the names and weights from scoring_rubric.yaml, combine() names any declared criterion no judge scored and any criterion the judges invented, and complete now requires the set to match. Weights are checked per card rather than on the merged value, because the merged weight is whatever the last judge said and a single dissenting judge is invisible in it — my first attempt checked the merged value and the reweight case did not fire.
On the historical replays: want is injectable for the same reason bar already is. Those cases assert real numbers from the real 2026-08-19 panel, which was judged under a six-criterion rubric, so they pass their own rubric and a 2026-08-19 assertion does not come to require a criterion that did not exist then. A live run passes neither and is held to the rubric as it stands.
Four cases added. Run against the 2026-09-16 judge cards it now says no judge scored deck_coherence and falls back honestly instead of quietly weighting six.
Generated by Claude Code
|
|
||
| version: 1 | ||
| threshold: 6.7 # below this the deck does not ship | ||
| threshold: 8.0 # below this the deck does not ship |
There was a problem hiding this comment.
Keep the email builder aligned with the raised threshold
Raising the rubric threshold to 8.0 leaves gmail_draft.py with a default threshold of 7.0, and that script's documented example omits --threshold. Any repair or manual invocation following the example will therefore describe a score such as 7.4 as "Shipped" even though the current rubric says it missed the bar; email_check.py validates the payload's shape but does not reconcile this value. Read the threshold from the rubric or make the argument mandatory instead of retaining a stale default.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct. Fixed in 118cb84: --threshold defaults to None and falls back to rubric_threshold(), which reads config/carousel/scoring_rubric.yaml and refuses to run if it declares no numeric threshold. A caller can still override it deliberately.
Same reasoning as run_complete, which this repo already states: the threshold comes from the rubric and nowhere else, because a run that could name its own bar would name a lower one on the day it mattered. The email is the owner's one look at a run, so a 7.4 captioned "Shipped" against an 8.0 bar is the worst place for a stale default to surface.
Generated by Claude Code
| # An opaque fill behind display type. Three ways it has been written here. | ||
| PLATE_CSS_RE = re.compile( | ||
| r"\.(?:plate|hookbox|headbox|titlebox|textbox|knockout)\b[^{}]*\{[^{}]*background", re.I) | ||
| PLATE_RGBA_RE = re.compile( | ||
| r"background\s*:\s*rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*(0?\.\d+|1(?:\.0+)?)\s*\)", re.I) |
There was a problem hiding this comment.
Detect opaque plates independently of class names
The new no-plate gate recognizes only six selector names, while its generic fallback recognizes only rgba(...) backgrounds. A display element such as .hook { background: #08060F; }, an inline hex background, or a canvas rectangle behind the headline passes this check despite creating the exact opaque text plate the gate claims to forbid. Inspect the computed background of actual display-type elements or rendered pixels rather than relying on optional class naming.
AGENTS.md reference: AGENTS.md:L68-L69
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Correct. Fixed in 118cb84, and the fix taught me something worth recording.
opaque_plates() now parses every CSS rule and judges the fill's opacity on any selector that reaches display type, plus inline style= backgrounds, with _alpha_of() handling rgba(), #rrggbbaa, #rgba, bare hex and named colours. A gradient or a var() returns 0 rather than a guess, because the source cannot answer that and the render gate can.
The first cut of the fix replaced the class names with the behaviour test and lost a real catch. .plate { background: rgba(9,10,15,0.97) }, the exact defect on the 2026-09-16 deck, stopped being found, because .plate carries no display type of its own — it sits behind some. So both lists run: DISPLAY_SEL for the behaviour and PLATE_SEL for classes whose name is the confession.
Verified against both decks: 7 plates across 5 frames of the shipped deck, 0 on the reference build. A 0.22 wash and a gradient are still allowed, and the furniture is excluded because TXLAYOUT owns its own treatment.
Generated by Claude Code
…eported clean Codex reviewed the deck-chassis change and every one of its eight findings held up. THE CONTINUITY MANDATE WAS DECORATION. min_continuity_devices went into the rotation table and nothing read it, which is GATE_LESSONS' oldest shape committed inside the same change that added a gate against that shape. TXLAYOUT.check now takes the device list, layout_check reads 'CONTINUITY: <DEVICE>, <DEVICE>' off the storyboard and binds under --require, and five self-test cases make the key mean something. THE FINISH CHECK WAS A SUBSTRING TEST. It passed on a commented-out call and on a frame that graded and then kept drawing, both of which ship artwork the grade never saw. It now finds executable calls and refuses any canvas mutation after the last one. THE PLATE CHECK KNEW SIX CLASS NAMES. A hex background on .hook, an inline style, or a short-hex alpha all walked past it. It now parses every rule and judges the fill's opacity on any selector that reaches display type, and keeps the names too, because dropping them lost the original .plate catch: that class carries no display type of its own, it sits behind some. THE NEW GATES WERE ONLY EVER POINTED AT A FIXTURE. guards.yml self-tested them and ran them against examples/, so a run that skipped its Phase 11 commands could ship a chassis-less strobing deck under a green board. Both are registered in shipped_check now, with a since-date because no deck before today has a chassis and a gate does not judge the work that produced it. A CHASSIS NAMED FOR ITS WORLD ALONE COLLIDES. A later deck picking the same world would overwrite it while the earlier run's archived slides still point at that path, so re-checking or reproducing that run would render a different light, ramp and grade. Chassis files carry their run's date and an undated name is refused. THE TREATMENT DIRECTORS WERE STALE TOO, and they run BEFORE the flow critic and seed the dossiers, so overriding only the critic left the plan itself aimed at the old rule. Phase 9 hands them the current rule and has them name the deck's devices. THE PANEL TRUSTED THE JUDGES' SCHEMA. Three judges all returning the previous six criterion card agree perfectly with each other, so the panel weighted a rubric with deck_coherence missing and reported a clean per-criterion median. It now validates names and weights against the rubric per card, and the historical replays pass the rubric of their own day so a 2026-08-19 assertion does not require a criterion that did not exist then. THE EMAIL KEPT A STALE BAR. gmail_draft defaulted to 7.0 with the rubric at 8.0 and its own usage example omits the flag, so a 7.4 would have been captioned Shipped. It reads the rubric now.
… from CI's gates job was red on port_audit's residue check and the failure was mine: config/carousel/deck_coherence.json names the sibling repo, and residue refuses Alaska names outside its allowlist. LOCALLY THIS LOOKED LIKE A DIFFERENT FAILURE, which is worth writing down. With the sibling checkout on disk, parity runs and fails on three pre-existing brand.yaml gaps, and residue was buried under it. In CI the sibling is absent, so parity SKIPS and residue was the only failure. Two environments, one script, different answers, and the local one hid the half that was actually mine. Same shape as the local suite and CI disagreeing about email_check on 2026-08-26. The file is allowlisted by exact path, alongside the two instrument design records and for a narrower reason than theirs: every ceiling in it was derived from a 58 deck corpus and the file states which corpus, how it was measured and what the subject measured against it, because the law this project publishes is that a figure can be recomputed from the same inputs. A derivation that will not name its inputs is a number with a story attached, and the next context that wants a looser ceiling talks itself into one in an afternoon. 'The sibling product' would not have done: this repo has four siblings and two of them are Texan. Only that one path is exempt, so the residue rule still protects what it is for, and the ledgers still may not name Alaska at all.
Correcting myself on
|
This is the same defect as the shipped_check one, in a worse place, and I fixed the first and missed the second inside one change. scripts/site/site_context.py decides which runs become ARTICLES by calling run_complete.threshold() ONCE and applying today's bar to every deck ever published. Raising the bar to 8.0 therefore dropped every deck between 7.09 and 7.58: fifteen article pages and about three hundred media files, the last three decks among them. IT SURFACED AS A REBUILD THAT WANTED TO DELETE THEM, and the first reading of that was that the site builder had a pre-existing bug. It did not. The deletions were this change's, and calling them pre-existing repeats the attribution mistake this run had already made once today on port_audit's residue check. BAR_HISTORY and bar_in_force move to run_complete, which both readers already import, because two copies of a rule about history is how history gets two versions. bar_for_run reads a deck's OWN recorded threshold first and falls back to the bar in force on its date, and site_context asks it per run rather than once per build. WHAT REMAINS IS NOT MINE AND IS LEGITIMATE. site_fresh_check still reports two MISSING article pages, 2026-09-03 and 2026-09-09, and the EXTRA list is now empty. Those two decks scored 6.762 and 6.714 against the 6.8 bar they were judged under, both hit rounds 5 against a cap of 5, and neither carries a hard fail, so run_complete's round cap path authorises them exactly as written: past the cap a deck with no hard fail is finished and ships at whatever it scored. They became publishable when they shipped and docs/ was never rebuilt. The cure is a rebuild that adds two pages and deletes nothing.
…ilt for site_fresh_check has been red on main for a while and this is the whole of it: the builder produces article pages for 2026-09-03 and 2026-09-09 and docs/ does not carry them. Both scored under the 6.8 bar they were judged against, 6.762 and 6.714, and both reached rounds 5 against a cap of 5 with no hard fail, so run_complete's round cap path authorises them exactly as written. They became publishable the day they shipped and the site was never rebuilt. ADD ONLY, AND THAT IS THE POINT. 6 new files and 34 modified index and sitemap pages, and ZERO deletions. Before the previous commit fixed the per-run bar this same rebuild wanted to delete 324 files, fifteen published article pages among them, and that is why it was reverted rather than committed the first time. The gate now reports the site fresh, 1580 files matching a rebuild byte for byte. docs/ is generated and any actor may trigger a rebuild. Nothing here was hand edited.
browser-render has been red on main for its last six guards runs on one assertion, "the horizon is warmer than the sky above it", reporting top r-b -7 and horizon -14. THE SKY WAS NEVER THE PROBLEM. Measured down the centre column at 1440x900, the warm band peaks at y 770 with r-b +6 against the top's -8, so the dusk horizon is doing exactly what theme.py's comments say it should. The pair sampled x 720, y 880, and the front page grew an ask box into that column: elementFromPoint at that point returns the box's own INPUT, which paints a cool fill, so the check was reading a form control and calling it the sky. This file's own header says these checks sample where content is NOT, and the other four points are gutters for that reason. The pair now samples the right gutter, the same x the spots table already uses, where the same band measures +3 against -8. AND IT ASSERTS THE POINT IS SKY BEFORE IT MEASURES IT, because moving a hardcoded coordinate to another hardcoded coordinate is the same bug waiting for the next layout change. If anything in main, the masthead, the footer or the ask box is ever under that point again, the check says so in those words rather than reporting the sky as cold. Nothing in the sky, the theme or the page was changed.
…h3ekki # Conflicts: # docs/index.html # tests/page_ground.mjs
Owner's report, 2026-09-16: the artwork is not good enough, the slides do not flow together, and each frame reads as text boxes thrown on a page.
What was actually wrong, measured rather than argued
Four facts read off shipped artifacts in this product and the sibling:
txpost.js)txcolor.js)The grade and the ramp builder were ported, working and documented, and essentially never called. That alone is most of the flat look.
The deeper cause is that there was no deck. Every frame reached into a permanent bin of finished parts (
txobjects,txfig), which gives uniform PARTS and no unity of WORLD. That is whytxobjects.js, written in September specifically to cure "clip art", did not cure it. The sibling writes a new chassis per run, named for that deck's world, and all nine frames draw from it.And the machine was being told to be incoherent.
txlayout.jsrequired nine different layouts, at least five distinct, no archetype twice running, and a print screen that varies per frame. The rubric's top artwork score required "no other frame in the deck laid out the same way". The 2026-09-16 craft judge praised frame 6 for being "the identical camera to frame 3 with the light inverted" and in the same breath gave the deck credit for "nine distinct layouts". The rule was pulling against the thing the judge liked.The scorer was also reading the value track backwards, calling a sawtooth "a genuine value arc" and rewarding its amplitude, because nothing measured adjacency. A spread is a property of a set. A deck is a sequence.
What changed
assets/js/txdeck.jsholds the three invariants a deck cannot keep by good intentions: one light, one material ramp, one way of seating type, plusfinish(), the one line that grades a frame with the deck's own grade. The chassis declares the deck exactly once, so nine frames cannot hold nine lights. Coherence is structural rather than checked.ownership.yamlcarvesassets/js/deck/**to thedailylane whileassets/**stayshuman. A run may build a world. It may not edit the workshop. Without this the pre-commit hook would have refused every chassis and the upgrade would have failed silently in production.Two new gates.
deck_chassis.pyreads the source and refuses a deck whose frames don't share a chassis, skip the grade, grade themselves, redeclare the light, or put a plate behind type; it finds 33 defects in the shipped 2026-09-16 deck.deck_coherence.pyreads the pixels and refuses a deck that strobes. Its thresholds are derived from a 58-deck reference corpus and recorded with their full derivation inconfig/carousel/deck_coherence.json, never typed.The doctrine inverts.
ILLUSTRATION_SYSTEM.mdgains "THE DECK IS THE UNIT", which outranks every per-frame rule under it: the chassis law, one light and ramp and screen and grade for the whole deck, a continuity mandate of at least two devices named in the storyboard, and no plate ever. The rotation is rebalanced rather than deleted: at most two of the same archetype in a row (two is a beat, three is a rut), at least three distinct. The image law did not move, because the defect the old table was written for was never "too few layouts", it was "no image".The rubric splits the art question.
artwork_craft0.28 → 0.22 asks only about a frame and no longer asks for a different layout on every one. A newdeck_coherenceat 0.12 asks whether the nine are one deck. Art carries 0.34 between them, up from 0.28, andclaim_integritydid not move. The threshold goes 6.7 → 8.0: the last fourteen runs scored 6.56 to 7.58, so the bar sat below the mean of the thing it judged and never once asked for another round.layout_check --proserefuses any surface still stating the superseded rule, and separates paths this repo can write (FAIL) from the two critic definitions under.claude/that the host lets nobody write unattended (reported, with the backlog item that closes them). Phase 12 now hands the flow critic the current rule at spawn time until a maintainer makes that edit.Proof
examples/lamp-deck/is the 2026-09-16 story rebuilt in the new system, same claims, same copy, so the contact sheets are a clean A/B on artwork alone.One world, one lamp at az 64 el 26, one ramp, one screen, one grade. The camera moves through that world rather than the world changing per frame. Two continuity devices run: the camera returns to frame 1's exact position on frame 9, and the hint tick evolves one, two, two, three, then absent, so the close argues the claim by not drawing it.
The eight things that build cost are written into
ILLUSTRATION_SYSTEM.md, among them that a hole punched in a light layer is a plate with the sign flipped, that a frame's two kinds of type want opposite things from the light, that a mid ground is the worst ground and no ink fixes it, that canvas text can't be registered to DOM text in a variable font, and that a frame whose QA goes sideways across rounds wants recomposing rather than tuning.Raising the bar broke three gates, and all three were right
run_completepins the threshold in its own self-test so it cannot drift with nobody able to name the run that moved it. The pin did its job: the rubric edit alone went red and the bar could not move without a second deliberate commit.panel's ship-false case used judges at 7.5, which cleared the old bar and not the new one. Andshipped_checkmarked three already-published decks as never having shipped, at 7.42, 7.42 and 7.09, all of which cleared the 7.0 in force when they shipped;BAR_HISTORYnow gives a deck that recorded no threshold the bar that actually applied.Known, and not this PR's
scripts/shared/port_audit.pyfails itsparitycheck on three missingconfig/brand.yamlkeys. This is red onmaintoo and is unrelated to artwork; verified by running the audit against a cleanorigin/mainworktree. Fixing it means real brand-config decisions, so it is reported rather than folded in here.The two critic definitions under
.claude/agents/still carry the superseded rotation rule. No routine may write that path, soknowledge/carousel/UPGRADE_BACKLOG.mdcarries the four-line fix for a maintainer, and the routine hands the critics the current rule at spawn time in the meantime.🤖 Generated with Claude Code
https://claude.ai/code/session_01QGXgJMCsDkwKBDTKYgWdU6
Generated by Claude Code