Skip to content

v2.0.0

Latest

Choose a tag to compare

@github-actions github-actions released this 13 Aug 07:51
· 6 commits to main since this release
8aa124d

Breaking. data-audience is retired rather than aliased and now blocks, so a post-cutoff artifact still carrying the pre-rename summary marker fails until it is changed to data-summary="true". Artifacts dated before 2026-05-25 are grandfathered and unaffected. The summary itself does not change, only the attribute.

Ownership attaches to the claim rather than the artifact, every summary says what would change its own conclusion, and no surface an author reads or copies names a job title any more. Length gains the ceiling required-section never had, read-time is checked against the prose instead of trusted, and the review state has three names. Four rounds of independent review are folded in, each of which found real defects in the previous round's fixes.

  • House rule: the prompt lives in the provenance footer, never in a card at the top (references/patterns.md). A visible "the prompt that made this" block with a copy button looks like transparency and costs the most valuable space on the page on something the reader did not come for; they opened a plan to find out whether the plan is sound. promptHash in the footer already makes the artifact auditable. The rule is about placement, not concealment: put the full prompt in the footer whenever it is non-sensitive, which is more transparent than a card and costs the reader nothing.

  • Files read (references/patterns.md, optional, best in understanding / research / review). An artifact explaining code makes claims about a codebase that keeps moving, and six weeks later nobody can tell which claims still hold. A collapsed <details> block lists the files consulted and the revision they were read at, which converts the question into a command: git diff --stat <rev> -- <paths>, and empty output means nothing the artifact read has changed since, in history or in the reader's working copy. Deliberately not <rev>..HEAD, which compares two commits and so reports an uncommitted edit as clean; both forms were run against a planted uncommitted edit to confirm which one catches it. The revision is the whole pattern, so check warns when a sourceFiles block omits readAtRevision or ships an empty path list, and stays silent when there is no block at all. The pathspec may be wider than the list and must never be narrower, which is the one way this pattern fails while looking fine: a trimmed pathspec still prints nothing while a file a claim rests on has moved. Mirrored into the provenance JSON-LD as sourceFiles so it is machine-checkable, and demonstrated in two shipped examples: understanding-canonical.html (three files it traces a request through) and dynamic-port-correspondence.html (five files across a port, all read at one branch tip that carries both implementations, with a pathspec widened on purpose because a correspondence map is also wrong when a sixth file appears that nobody mapped, said in a clause so the mismatch does not read as a slip). Two tests run over every example carrying the block rather than one named file, so a new one is covered on arrival: the visible list, the <summary> count and the JSON-LD must agree, and the pathspec must contain every path listed. Deliberately NOT added to review, research, plan, status, incident or prototype: those examples cite no file paths at all, and the pattern's own rule is to list what was read rather than what exists. Needs no CSS; the scaffold already styles details.

  • The files-read guards could pass while the block was wrong, and the snippet people copy broke a rule printed beside it. Three findings from the Codex second eye, all reproduced before being accepted and all reproduced again as failures afterwards. First, the revision was matched anywhere in the block, so the <summary> could drift to a different revision while the command still carried the declared one; the summary is the revision the reader actually reads, so it and the command are now checked separately. Second, the path regex demanded exactly <li><code>path</code></li>, so a visible entry the JSON-LD never declared could hide behind an attribute; attributes are tolerated now, and a count assertion fails when any <li> is not read as a path, which also catches a path written without <code>. Both guards were sharing nothing, which is how they drifted, so they now share one parser. Third, the patterns.md snippet listed routes/*.ts and diffed the wider routes/, costing every copier a false alarm whenever an undeclared routes/README.md changed. The snippet is now held tighter than a shipped artifact, its pathspec having to equal its declared paths rather than merely contain them, because it is the copy source and a bare widening there teaches the exception as the default. Widening stays legal for an artifact, demonstrated with its explaining clause in dynamic-port-correspondence.html. A new test holds the snippet to this, since nothing checked it at all before.

  • A second reviewer pass found four more ways the guards were wrong, two of them in the fixes for the first pass. Each reproduced before being accepted. A revision was matched by substring, so a declared main was satisfied by a summary reading domain and by a command touching domain/file.ts; readAtRevision is any non-empty string and so may be a branch or tag rather than a hex SHA, which makes that collision reachable rather than theoretical. Both revisions are now compared as exact tokens. Only the first files-read block in a file was ever parsed, so a second would go unchecked, which the guards now assert against rather than quietly inspecting one block out of several. The rule holding the patterns.md snippet to its declared paths compared ordered lists, so the same paths in a different order failed while covering the identical scope; order is no longer checked, and a genuinely trimmed pathspec still fails. And the claim that empty output proves nothing changed in history was too strong in the other direction: git diff <rev> -- <paths> compares content at the two ends, so a file changed and later reverted also reads as clean. That is the right answer for staleness and the wrong claim about history, so both the pattern and the example now say "still matches what was read".

  • Same-data control block (references/patterns.md). A multi-option comparison has a failure mode that is invisible from outside: each option gets measured against whichever input flatters it, and the table at the end looks like a fair scoreboard while being four anecdotes in a grid. One block before the first option names the single input every option is judged against, says which way it is biased and confirms the bias falls evenly, states the properties that actually separate the designs rather than just the volume, and asserts that every number below comes from that input. dynamic-design-directions.html shipped as the worked example a release earlier; this documents it so it can be reached deliberately. The snippet carries the two CSS rules it needs, both drawn from scaffold variables, because .control and .fixed are the example's own and copying the markup alone would render unstyled.

  • Ownership attaches to the claim, not to the artifact (Rule 11, claim-owner, WARN). A recommendation nobody holds is the artifact arguing on its own behalf, and a reader has nothing to push back against. The rule is forced by the two kinds that carry both description and judgment: an architecture reports Before/After then recommends, a review reports Concerns then issues a Verdict, and both split down the same seam. Attaching ownership below the level where they split dissolves the problem rather than assigning it by fiat. data-owner on the section, plus the holder named in prose beside one sentence of their own doubt, because a name alone is bureaucracy and a name with a stated doubt tells a reviewer where to spend attention. No kind list: a heading regex selects the judgment section exactly as comparison-visual selects a comparison, so kind sensitivity emerges instead of being declared. research and understanding carry no such heading and never trip it; decision always does. In force in dynamic mode too, since who holds a call does not depend on which sections exist. status is the deliberate gap, its accountability being artifact-wide and already carried by the ribbon's Owner. The search window is the subtle part and is what the tests target: an owner sits on the section's opening tag, before the heading, so the window cannot start where comparison-visual's does, and it is floored at the previous heading so a neighbour's owner cannot satisfy an unowned section. A first cut of the marker regex accepted data-owner="", because \S matched the closing quote; the test written for that case is what caught it.

  • The reviewer field starts empty and says so. The scaffold shipped reviewer: "<human reviewer or role>" and rendered "reviewed by <reviewer>", which reads as a claim that review happened. It now ships "pending" and renders not yet reviewed by a human. Measured on a live lane of 197 artifacts before the change: 89 carried a human name, 51 carried no reviewer field at all, 29 carried an agent name, 18 carried some spelling of "pending" across nine distinct spellings, and 10 shipped the unfilled placeholder, which is an author hand-rolling a state the schema never offered. An agent name there is honest reporting rather than a mistake; the schema simply has no word for it yet. The shipped examples keep their reviewer names on purpose: they show the end of the lifecycle, the scaffold shows the start.

  • Every summary states what would change its own conclusion (Rule 1, fourth bullet, deliberately not validated). The first three bullets orient and none of them concludes, so a reader who cannot tell whether the opening is complete has one safe move, which is to audit to the bottom. Every navigation aid in this skill fails to prevent that, because a table of contents says where things are and never says what may be skipped. Measured cause: across 197 artifacts a reading guide was present on every recent one while the median length grew from 1,982 words in May to 4,359 in August. Naming the load-bearing assumption asserts closure, locates the one place a surprise can live, ranks the rest as support, and grants permission to stop. It is the mechanic the same-data control block already uses ("every number below comes from this file"), moved to the summary. No rule for it on purpose: a check would be satisfied by "nothing below changes this", written to clear it and testing nothing, which is worse than an absent bullet because it asserts a closure nobody looked for. It lives in the ship checklist instead, and all twelve examples now carry a real one anchored to a section that exists.

  • A unified diff is a first-class comparison visual (Rule 2, references/diagram-types.md). Narrow trigger, and the wording is deliberately narrow: the two states share most of their shape and the reader's job is to find the part that does not. That is the one case where side-by-side panels actively hurt, because they render the unchanged nine tenths twice and leave the reader diffing two columns by eye. Works over a call tree, a component tree, a file layout, or pseudocode control flow, not only over source, which is the notation taken from HumanLayer's show-me skill. Needs no validator change, since <pre class="diagram"> already passes. Documented with the two notation traps found while writing it up (the rail column shifting on a file-layout diff, and a node that only gained children needing remove-then-re-add) and three limits that fall out of rules already in force: not a general alternative to grid-cols-2, not a pasted git diff, and the +/- glyphs carry the meaning because colour never can.

  • A .keycard with three children collapsed into an unreadable ribbon. Found in a real artifact, not in review. The component was grid-template-columns:auto 1fr and depended entirely on having exactly two children: auto-placement put the hero in column 1 and the paragraph in column 2, which looked deliberate and was luck. A third child (a second paragraph, or a .kind label used instead of .big) wrapped back into column 1, whose auto width then grew to fit a full paragraph of prose and starved the 1fr column down to roughly one word per line, with most of the card left blank. The hero is now pinned to column 1 for the full height and every other child stacks in column 2, so the layout no longer depends on how many children an author wrote. The mobile single-column override releases that placement explicitly, since leaving grid-column:2 in a one-column grid conjures an implicit second column and undoes the collapse. No rule was added: the failure mode is gone rather than detectable, which is the better end state.

  • Embedded diagrams silently ate the last line of any multi-line node label. embed-svg renders with htmlLabels: true, so mermaid writes each label as HTML inside a <foreignObject> carrying a height measured on the machine doing the embedding, and the reader's browser re-flows that HTML in their own font. _DIAG_FONT led with ui-sans-serif, system-ui, which resolve to a different physical font on every operating system, so the mismatch was guaranteed rather than unlucky: a label measured at seven lines wrapped to eight for the reader, and foreignObject clips its overflow by default, so the eighth line was cut with no trace in the HTML source and nothing in check to catch it. Two fixes, both narrow. _DIAG_FONT now names real families ("Segoe UI", Roboto, "Helvetica Neue", Helvetica, Arial, sans-serif) so measurement and display land on the same metrics almost everywhere. And overflow: visible is set on the embedded foreignObject, turning any residual mismatch from a silent truncation into a visible overhang the author can see in render. The base scaffold had carried that overflow rule for live .mermaid blocks from the start; embed-svg rewrites the wrapper to .diagram-light / .diagram-dark and the rule never followed it across, which is the whole bug in one sentence. Documented as a mermaid trap, with the authoring consequence that a label of four or more lines is near the edge whatever you do.

  • No job titles anywhere an author reads or copies (Rule 12, audience-segmentation). The spine's ban on naming, segmenting, flattering or excluding a reader is absolute, and the skill was quietly breaking it in its own surfaces: the opening paragraph asked you to picture a tech lead or a product manager, the ship checklist told you to read each heading "as a PM", the reading-guide guidance named three job titles while banning them, architecture-canonical.html labelled three per-section leads "PM lead:", two other examples addressed themselves to stakeholders and new hires, and the scaffold carried _capture_pm_lead internally. All of it is now reader-relationship language, which also makes the skill fit organisations that have no such roles. The words survive in exactly three places, each on purpose: artifact-spine.md stating the ban, the detector regex that has to contain what it detects, and _RULE_ID_ALIASES.

  • data-audience is retired rather than aliased, and now blocks. It was the pre-rename answer-first marker, kept as an accepted alias so already-shipped artifacts would keep validating. Measured on a live lane of 197 artifacts, that kindness did the opposite of its purpose: 158 carried data-audience, 0 carried data-summary, 0 carried both, and the newest of the 158 was written the same week the count was taken. The alias was not easing a migration, it was the reason none started, because nothing ever told an author (or the model copying the previous artifact) that the marker names a job title in the markup. Both errors now fire together on purpose: summary-first alone would tell an author to add a summary block to an artifact that already has one, and audience-segmentation is the one that names the actual fix. A role-labelled reading guide warns rather than blocks, since that regex has judgment surface around it. Note the deliberate asymmetry: a retired rule ID keeps answering to a suppression comment forever, because that is an author's decision that must not silently invert; a retired content marker gets no such protection, because keeping it alive perpetuates the thing the rule exists to remove.

  • required-section has a ceiling now, measured in words (Rule 13, prose-budget, WARN past 4,000). It was a floor with nothing opposing it, so sections accrete: across 197 artifacts the median carries 10 <h2> sections against a skeleton of about 5, only 18 of 197 sit at or under their skeleton, and the median grew from 1,982 words in May to 4,359 in August. Calibrated so every shipped example passes. It flags 45 of 197 artifacts on the measured corpus and 9 of 16 on the most recent month; the second number is the point, not a miscalibration, since the recent trend is what the rule opposes. Markup, script, style and inline SVG are excluded, because the unit is what a person reads. Distinct from size-budget, which stays as it is: that one measures 512 KiB of payload, which inline SVG and CSS dominate, so it fired once in two hundred artifacts and stayed silent on an eight-thousand-word wall of text. Bytes are a transport concern and words are the reader's concern.

  • artifact-read-time is checked against the prose (Rule 14, read-time, WARN). It is the one field a reader uses to decide whether to commit, and it was tracking nothing: on the same lane, 194 words claimed 5 minutes, 4,701 words claimed 5 minutes, and the longest artifact gave up and wrote browse. check now computes the figure at 230 wpm and warns past 2.5x, or when the field carries no number at all. The tolerance is wide because diagrams, tables and code legitimately slow a reader down. Two deliberate limits. It is not auto-filled: new scaffolds placeholder prose so computing it there is meaningless, and having index rewrite artifacts as a side effect of building the gallery would mutate an author's file unasked, so the warning prints the computed number instead and fixing it is a copy. And an absent read-time only warns past 500 words, because the first version of this rule made every fresh scaffold warn on creation, which is how you train an author to ignore a rule before they have written anything. The scaffold now ships the field empty with <fill before publishing> in the ribbon.

  • The review state has three names (reviewState in the provenance JSON-LD): unreviewed, agent-reviewed, human-reviewed, with reviewer still saying who or what. Free text is what produced nine distinct spellings of "pending" in one lane, so a fourth value warns. agent-reviewed exists because an agent name in reviewer was honest reporting the schema had no word for. The scaffold ships unreviewed; the examples ship human-reviewed, since they show the end of the lifecycle and the scaffold shows the start.

  • Two independent reviews found the same class of bug in claim-owner, and it was the substring search. The rule shipped as a heading regex plus a backwards rfind for the enclosing tag, which has no idea what an element is. A <div> closed before the heading was mistaken for its container, so a genuinely owned section still warned; a preceding closed sibling's unrelated data-owner silently satisfied an unowned judgment; an owner inside an HTML comment or shown as escaped text in a <pre> counted; \bdata-owner matched the tail of data-source-data-owner, because - is not a word character; data-owner="&nbsp;" passed while a valid unquoted data-owner=Ana failed. Replaced with a real HTMLParser pass that tracks open ancestors and reads attributes as attributes. The rule is now: a heading is owned when the heading itself, or an element enclosing it below <main>, carries a non-empty data-owner. Ancestors count, so a section owner covers a nested <h3> and wrapping a heading in a <header> changes nothing; <body> and <main> are excluded so one attribute cannot own the whole page; a sibling no longer counts at all, which is a deliberate tightening on the first cut. Nine regression tests, one per reproduced case.

  • data-judgment="true", the escape hatch a well-written artifact needs more than a lazy one. The heading regex misses exactly the headings worth writing: one that states its judgment rather than naming it ("Lead with the narrow first stage", "The pick, and what it costs") matches no keyword. Both shipped examples that do this now carry the marker, and dynamic-design-directions.html gained an owner on a judgment section that had none and was invisible to the rule.

  • The keycard fix reached the generator and none of the artifacts. Every example embeds its own copy of the scaffold CSS, so all twelve shipped examples and the generated decision artifact still carried the two-track grid, and the three-child collapse stayed reproducible in the exact files an author copies from. Patched into all thirteen. This is the same failure the repo already has a memory about: the example is what gets copied, so a fix that only lands in the generator has not shipped.

  • Rule 11 asked for a holder-and-doubt sentence that four owned sections and the incident scaffold did not have, so a minimally completed scaffold could pass claim-owner while breaking the rule that documents it. Added to all five.

  • Rule 1 contradicted itself, saying every summary carries four bullets and then that three is the default. The BLUF alternative is reconciled: it may compress, and it may not drop the stopping claim.

  • The quoted measurements were drawn from two different populations and one distribution summed to 116 of 197. Every count is now taken from one stated population (197 files matching YYYY-MM-DD-*.html in a single lane, recursive, temp files excluded), both distributions sum to 197, and the definition is written down where the counts first appear so a reader can audit them. The corrected figures are more damning than the ones they replace: 158 artifacts carry data-audience and 0 carry data-summary, and "pending" was hand-rolled in nine distinct spellings rather than four.

  • A second reviewer pass on the rewrite found three more blockers, and all three were the same mistake: reading source text instead of parsed markup. <p data-owner="Ana"><h2>Decision</h2> closes the <p> implicitly in HTML, so the heading is a sibling and unowned, but the stack kept p open and called it owned; the same false pass hit an omitted </li> and </td>. Duplicate attributes took the last value where a browser takes the first, so data-owner="" data-owner="Ana" read as owned and the reverse invented a warning. And audience-segmentation grepped the raw file, so <!-- <section data-audience="pm"> --> and an escaped example in a <pre> both raised a blocking error: an artifact explaining the retired marker could not pass the rule explaining it. All three now go through parsed markup, with HTML's optional-end-tag rules applied before an element is pushed and first-value-wins attribute maps.

  • Three quieter ones from the same pass. prose_words stripped tags with <[^>]+>, which ends at the first > and so leaked the tail of any attribute containing one into the word count, inventing prose-budget and read-time warnings out of markup nobody reads; it is a parser now. The role-labelled-guide detector matched read-map anywhere and then any <strong> within 800 characters, so an ordinary paragraph followed by "Product launch" warned; it now walks the actual reading-guide element and anchors the label to its start. And artifact-read-time="0 min" carried a number but skipped the tolerance branch because zero is falsy, handing a reader an impossible budget in silence.

  • audience-segmentation split into two rule IDs. It carried BLOCK and WARN severities under one name, which made "how many rules block" unanswerable and was caught by the new docs test below disagreeing with the README. The attribute check keeps audience-segmentation and blocks; the reading-guide check becomes role-labelled-guide and warns. One severity per rule ID.

  • A test now holds the documentation to the code. Asked for by a reviewer after finding the references still describing the pre-change contract, and it paid for itself on the first run by finding size-budget: a rule the checker had been emitting for releases with no row in the documented table at all, so nobody reading the contract knew it existed. The test asserts every emitted rule ID is documented, every documented rule ID can actually fire, and that the rule count in artifact-spine.md and the blocker count in README.md both still match the table. Alongside it, the stale canonical docs are corrected: the README said three blockers, patterns.md omitted reviewState from the required provenance fields and told BLUF authors to write the old three-bullet opener without a stopping claim, and its BLUF example still addressed a job title.

  • The prose-budget claim was wrong about its own corpus. It said the threshold "flags the tail, not the norm" while the most recent month's median sat above it. Measured properly: 45 of 197 artifacts on the whole corpus, 9 of 16 in the most recent month. The second number is the point rather than a miscalibration, since the recent trend is what the rule exists to oppose, and the documentation now says so instead of claiming a quietness it does not have.

  • The flagship examples were undercutting the branch's own thesis, and a reviewer caught both. status-canonical.html asked two named people to confirm a blocker; one of them owned nothing (he appears once, as an escalation path in Next) while the person who actually owned the second blocker was never named and its detail paragraph was fully passive. review-canonical.html said "the one required change" in the summary, "one config-shape change" in the verdict, and then listed two under Required, the second filed by its own stated reason ("cleaner audit trail") as a nicety. Both are the answer-first opener disagreeing with the detail, which no content rule can see, and both taught the opposite of the rule this branch adds. Fixed by naming the real owner and moving the nicety to Optional, with two narrow tests holding the examples to the counts they claim. A third example asserted a latency number as settled that its own open-threads section said was never measured under load; that claim now says what was actually tested.

  • A truthy non-string reviewState was silent in both directions. if not obj.get("reviewState") accepted it as present and isinstance(state, str) and ... skipped validating it, so 123, true and ["human-reviewed"] all passed. The isinstance guard gated the whole check instead of narrowing it. A JSON type slip is squarely in scope for a field that exists because free text produced nine spellings of one state.

  • A Codex retry pass found two more, both about disagreeing with a browser. A trailing slash on a non-void element is a parse error that HTML then ignores, so <section data-owner="Ana"/> opens a section that stays open and owns what follows; treating it as opened-and-closed made the rule warn on an owned section. And the first-duplicate-wins attribute fix had reached the judgment parser only, so the artifact parser still took the last duplicate and the two parsers disagreed about identical markup. Both now share one attribute helper, so they cannot drift apart on the same question again. The same pass confirmed its suggested split of audience-segmentation was already done, and that one suppression comment had been silencing both the block and the warning before it.

  • The skill's own tooling was inflating the length it then measured. prose_words counted block-preformatted content as reader prose, so a code sample, an ASCII diagram, and in particular the mermaid source that embed-svg parks in a collapsed <details> as the no-JS fallback all pushed an artifact toward prose-budget and stretched its computed read-time. <pre> is now skipped; inline <code> deliberately still counts, because it sits inside a sentence and is read with it. The longest shipped example drops by about 400 words and no verdict changes.

  • A final pass found four blockers, every one of them inside a fix for an earlier finding. The implicit-close table was keyed by the element being closed, which cannot express a cascade: <tbody> has to close an open <td>, then its <tr>, then the <thead> around that, and a pair-only table reaches none of them. It also omitted tfoot and the rule that a heading closes an open heading. Re-keyed by the element being opened, so one while unwinds the whole run. The reading-guide tracker was a counter incremented on every start tag including void ones, so a bare <br> inside a guide pushed the depth up with no end tag to bring it back down and a role-looking label anywhere later in the document warned; it is an element stack now, and a self-closed non-void guide stays open the way a browser keeps it. The documentation-parity test scanned only literal _add arguments, so the seven plain-language rules that reach it through a variable were invisible to it and both set-equality assertions stayed green whichever way that family drifted; widening the scan immediately showed all seven missing from the rule table, described only in Rule 10's prose. And the status-example guard counted any name appearing in the blockers section as an owner, so "escalate to Marcus" would have read as ownership, which is the exact confusion the guard exists to catch; it now matches explicit ownership statements only, and was checked against the original defect to confirm it still fails on it.

  • Routed open questions (references/patterns.md). "Open questions" as a bare list is where a decision goes to die: nobody owns it, nothing dates it, and the author's view is missing, so a reader has to build an opinion from scratch before replying and usually does not. Four fields, and the fourth is the one people leave out: the question, who decides, by when, and where it lands if nobody answers. A stated lean can be answered with one word or one correction, which is a cheap reply and therefore a likely one, and it makes the silent default visible. Also closes a dangling cross-reference: the non-goals section already promised open questions get "an owner and a date" and pointed at a section that only covered actions.

  • ci.yml: the suite runs on every push and pull request. release.yml fires only on a vX.Y.Z tag, so until now nothing checked a pull request; every PR merged unverified and a broken validator would only have surfaced at release time, after the version bump was already written. Tests now run on Python 3.11, 3.12 and 3.13, which also means the README's "Python 3.11+ is all you need" is tested rather than asserted for the first time. The shipped examples are validated in a separate job, since that check is version-independent.

  • The example check is one script instead of two copies. It moved out of an inline heredoc in release.yml into .github/scripts/check_examples.py, called by both workflows, because two copies of it would have drifted. It lives under .github/ rather than in the skill payload so it is not installed onto users' machines, and it doubles as a local tool: plain output in a terminal, GitHub annotations under Actions.

  • A grandfathered example is now an error. The inline version validated everything against a hardcoded date, which hid the real risk: an artifact dated before RULES_EFFECTIVE_DATE is exempt from the entire content contract, so a shipped example with a stale date would have passed by never being checked at all. Each example is now validated under its own artifact-created date, and being exempt fails the build, on the grounds that an example must be held to the contract it exists to demonstrate.

  • release.yml verifies CI instead of re-running it. Repeating the suite at tag time duplicated work CI had already done on that commit, but assuming CI ran is the opposite mistake: a tag can point at any commit, including one that never reached main and so was never checked. The release now requires a successful ci.yml run for the tag's exact SHA and refuses to publish without one. A tag push does not trigger ci.yml, which is scoped to branches, so this only ever matches an earlier run on the same commit.

  • The landing page shows dynamic mode. index.html listed the nine kinds and nothing else, so a reader arriving from the 1.3.0 announcement saw no sign of the feature it was about. The three dynamic examples now have their own section.

  • The landing page was the one file breaking a rule the repo teaches. artifact-spine.md states that anything hoverable is also focusable, and all twelve shipped examples carry the global :focus-visible outline. index.html had one :hover and no focus rule at all, so a keyboard user tabbing the card grid got the browser default ring and none of the border treatment a mouse user gets, on the page a reader sees before they ever reach the doctrine. Paired now, with :focus-visible rather than :focus so a clicked card does not stay stuck in the highlighted state after the pointer leaves.

  • The first attempt at those two fixes introduced a worse defect than it closed, caught by an adversarial review round. Asking only whether the declared revision appears among the command's tokens let git diff <rev> HEAD -- <paths> through, which is the two-commit form written with a space instead of .., hides an uncommitted edit exactly like <rev>..HEAD does, and was the one thing this whole pattern exists to prevent. The rule had started policing notation rather than semantics: it rejected the dotted spelling and accepted the spaced one. The revision must now be the last thing the command passes before the --, so no second commit operand can slip in. Separately, stripping raw-text elements without stripping comments first meant an HTML comment merely naming an unbalanced <script> deleted everything up to the next real closing tag, swallowing the block so the artifact was skipped rather than checked, and skipping reads as passing. That is the same bug simulate_no_js hit with <noscript> and it was reintroduced in a second function. Blocks are now located by position: comments come out first, then any block whose start falls inside a <textarea> or <script> span is a sample being displayed rather than a block on the page. Counting could not tell an unbalanced tag eating a real block from an artifact whose only block was a sample, and those two need opposite answers. Eight arrangements verified, including all three of those cases.

  • Two last holes in the files-read guards, both found by the reviewer after the merge. The guard tried to work out which token in the git diff command was the revision, and every way of doing that needs to know which flags take a separate value. Git has several (-S needle, -l 5), so a command carrying one of those and no revision at all had the flag's value reported as its revision. It now asks the answerable question instead, whether the declared revision is one of the command's tokens, which needs no knowledge of flag shapes. (The reviewer's --stat-width 80 and -U 3 spellings are rejected by git outright, so those two do not reproduce; -S and -l do, and the fix stands on those.) Separately, an artifact that demonstrates the pattern by showing the markup inside a <textarea> would have been rejected for carrying two blocks, since the browser renders that sample as text and the regex did not. Raw-text <textarea> and <script> contents are stripped before counting, so the sample no longer counts and the real block is the one parsed. A genuine second block is still rejected.