M0 Phase 2: reconcile the attribute docs with what the client implements - #300
Conversation
Roughly a quarter of this repo's own attribute documentation was untrue.
Of 35 documented lvt-* tokens across the four surfaces that teach the
vocabulary, ~10 were wrong.
The phase was scoped around renames -- find what moved upstream, update
the name. It found a category the plan did not anticipate: attributes
that were never implemented at all. lvt-filter (taught in the very skill
reference M1 plans to hand a generating agent) and lvt-value-* ("extract
values from elements") exist in no implementation, apparently ever.
Renames are self-announcing; fabrications are invisible, which is why
these survived longest -- Phase 1's 11-attribute sample missed
lvt-value-* entirely and only the mechanical check surfaced it.
The docs were actively broken rather than stale-but-working: the client
carries exactly one back-compat shim (lvt-no-intercept), so every other
superseded name resolves to nothing, silently, with no console warning.
Corrections: lvt-scroll/highlight/animate -> lvt-fx:*, lvt-throttle/
debounce -> lvt-mod:*, lvt-disable-with -> lvt-form:disable-with,
lvt-{action}-on:{event} -> lvt-el:{method}:on:{state} with its real
methods and states (the documented "loading" state and disable/enable/
focus/blur methods never existed). Removed entries name their
replacement -- native <dialog> for the modal attributes. Added a
namespace migration table, lvt-form:preserve as distinct from lvt-ignore,
and lvt-datatable, which was absent despite being the opt-in that
actually provides the sorting and pagination reference.md attributed to
plain lvt-columns.
No CHANGELOG entry: no example, template or fixture used a dead
attribute, so nothing user-facing regressed. The rot was confined to what
we teach.
Adds TestDocumentedAttributesExist, checked against the vendored bundle
rather than a sibling ../client checkout so it runs in CI and tests the
client that ships. Its first version was self-certifying -- it scanned
all .go files for evidence an attribute exists, including itself, and its
own doc comment names lvt-filter while explaining the bug, so it passed
for the wrong reason. Excluding _test.go fixes it and is right anyway: a
fixture using a made-up attribute must not vouch for the docs that
invented it. Verified by reintroducing the rot and watching it fail.
Also recorded, and more consequential than the docs: `tinkerdown
validate` does not validate attribute names. A document using
lvt-filter, lvt-scroll and a literal lvt-totally-made-up validates clean
with zero errors. This guard closes the docs -> implementation
direction; the direction M1 depends on is generated app ->
implementation, which is unguarded. M1 Phase 3's "self-correct until
validate is clean" is therefore a weaker guarantee than the plan assumed,
and the risk is rewritten as M1-critical-path rather than an M2
improvement.
Verification: GOWORK=off go test ./... green including the root package
with all 32 !ci e2e files (840s).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
ReviewSolid, well-substantiated docs fix — I independently verified the corrected attribute names against the vendored client bundle ( The new 1. The caller does: attr := normalizeAttribute(strings.TrimRight(m[1], "-:"))which strips any trailing
Concretely: if a future edit documents another dead wildcard family that doesn't coincidentally share a name with something real (e.g. 2.
3. Minor/cosmetic: None of these three are blocking — the guard still does its job for every attribute currently in the docs (I confirmed the maps correctly special-case Other notes:
|
Review-bot findings on #300, all three verified. The lvt-value- family entry was dead code that passed by coincidence. The caller trimmed the trailing dash before normalizeAttribute ran its allowlist lookups, so "lvt-value-" became "lvt-value" -- which resolves against the real, unrelated select-binding attribute of that name. The fabricated family was therefore never actually excluded; it was masked by something real. Moved the trim inside normalizeAttribute, after the lookups. That bug was invisible for the same reason the original doc rot was: nothing failed. The guard's own correctness was only ever exercised indirectly, through whichever attributes the doc surfaces happened to contain. So this adds table-driven tests of normalizeAttribute and implemented directly -- the classification rules are now pinned rather than incidentally covered, including the boundary rule that stops "data-lvt-scroll-sticky" from vouching for the long-dead lvt-scroll. Writing those tests immediately caught a wrong assumption of my own: an unknown family like lvt-widget- normalizes to its trimmed name, not the dashed form I had asserted. The guard was right; the expectation was not. Also drops the redundant internal/cmd/pkg entries from readGoSources -- collectGo(".") already recurses into them, so they were scanned twice. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
All three taken — the first was a real bug and your diagnosis was exact. 1. Worth noting that bug was invisible for exactly the reason the original doc rot was: nothing failed. 2. Your suggestion of table-driven tests for the helpers was the most valuable part of the review, and I'd underweighted it. The guard's own correctness was only ever exercised indirectly, through whichever attributes the doc surfaces happened to contain — which is how finding 1 hid. Writing them immediately caught a wrong assumption of mine: an unknown family like 3. One correction to your note on the placeholder branch being dead: All green: guard + helper tests + |
ReviewThis is a genuinely good piece of documentation archaeology, and the meta-story (self-certifying test, That said, I checked the PR's core factual claims against the vendored bundle ( Bug:
|
Review-bot catch on #300, and re-verifying the whole batch found a second instance it had not flagged. lvt-focus-trap is live -- the shipped bundle registers a Tab handler that queries [lvt-focus-trap] and cycles focus within it. lvt-debounce is live too, and distinct from lvt-mod:debounce: it overrides the debounce interval on an auto-wired change binding rather than an explicit lvt-on: handler. Both are restored to the reference, with the distinction between the two debounce attributes spelled out. Two failures compounded. The classification error: I hand-grepped for quoted literals ("lvt-focus-trap"), but the client uses the attribute inside a selector -- querySelectorAll("[lvt-focus-trap]") -- so the literal carries brackets and my pattern missed it. Any attribute used only via selector syntax would have been misclassified the same way, so this was systematic rather than a single slip. The structural failure, which is the one worth keeping: documentedAsRemoved and neverImplemented are skip lists, and TestDocumentedAttributesExist deliberately does not check them. That made "this attribute is gone" the single claim in the guard that nothing verified -- an unfalsifiable escape hatch inside a mechanism built precisely to stop trusting unverified assertions. TestRemovedAttributesAreReallyGone closes it by running the removed-list through the same implemented() matcher, which handles selectors correctly (brackets are not attribute characters). The mechanical check catches exactly what manual searching missed. Verified by reintroducing the mistake and watching it fail. Worth recording that the severity here runs the opposite way to the rot this phase set out to fix: a stale entry misleads someone writing new markup, but a wrong deletion tells someone their working code is invalid. Verification: GOWORK=off go test ./... green including the root package with all 32 !ci e2e files (836s). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
Confirmed and fixed — you were right, and re-verifying the whole batch turned up a second false removal you hadn't flagged.
Root cause of the misclassification: I hand-grepped for quoted literals ( Your structural point was the important half, and I'd underweighted it. Also worth naming: the severity here runs opposite to the rot this phase set out to fix. A stale entry misleads someone writing new markup; a wrong deletion tells someone their working code is invalid. I had that backwards while doing the deleting. On your two minor notes — I've left Full suite green: 836s including all 32 |
ReviewVerified the core claims directly against
Code quality
Minor / non-blocking
Test coverageStrong — new tests exercise the happy path, the classification edge cases (trailing dash, bare namespace, unknown family), and the specific regression each of the three follow-up commits fixed. The PR's own commit history (three rounds of "review bot found X, verified, fixed") is a good sign that the claims here have already been adversarially checked, and my independent spot-checks against the bundle didn't turn up anything the process missed. Process notePer this repo's CLAUDE.md ("check if a No blocking issues found. The two "minor" notes above are optional polish, not correctness concerns. |
Review-bot polish note on #300. The six namespaces share a branch, but lvt-on: differs in kind: its member is an arbitrary DOM event name, not an entry in a fixed dispatch table the way lvt-el:{method} is. Checking only the namespace is a simplification for the others and the only option for lvt-on:, since there is no enumerable member set. Worth saying so, since the shared case arm reads as though all six are structurally identical. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
Took the Left Thanks for the three rounds here — the |
ReviewGreat work on the mechanical guard: TestDocumentedAttributesExist plus the TestNormalizeAttribute/TestImplementedMatchesWholeNames/TestRemovedAttributesAreReallyGone trio is a solid piece of test design (whole-token boundary matching, excluding _test.go from evidence, and pinning the classification rules directly). The 'caught by deliberately reintroducing the rot' verification discipline described in the PR body is exactly right. That said, I found one correctness bug and one completeness gap in docs/reference/lvt-attributes.md itself, the very file this PR is making authoritative, plus a couple of minor nits. Bug: self-contradictory Namespace migration table (docs/reference/lvt-attributes.md around line 377) The new Namespace migration table lists lvt-focus-trap as removed: | lvt-click-away, lvt-window-{event}, lvt-focus-trap, lvt-modal-open, lvt-modal-close | removed, not implemented by the client | But lvt-focus-trap is documented right above (around line 203) as a real, current attribute, and I verified it is actually present in internal/assets/client/tinkerdown-client.browser.js. The PR description itself says this was one of the two false removals caught and restored ('Review found lvt-focus-trap documented as removed while a live Tab-cycling handler for it sits in the shipped bundle... Both restored.'). It looks like the restoration updated the dedicated section and the documentedAsRemoved test map, but missed this table. Same issue one row up (around line 373) for lvt-debounce: | lvt-throttle / lvt-debounce | lvt-mod:throttle / lvt-mod:debounce | This implies lvt-debounce is purely a superseded alias for lvt-mod:debounce, but lines 287-295 of the same file correctly document lvt-debounce as a distinct, still-current attribute (overrides the auto-wired change-binding debounce interval). I confirmed the literal lvt-debounce string is present in the bundle too. This is exactly the failure mode the PR is trying to eliminate, and it is in the table a reader would consult specifically when migrating old markup, so it actively tells them to abandon two attributes that still work. Since normalizeAttribute collapses these bare tokens to themselves and they are not skipped (correctly, since documentedAsRemoved no longer lists them), TestDocumentedAttributesExist won't catch this: the guard only checks whether a token is backed by an implementation somewhere, not whether the surrounding prose correctly characterizes it as removed vs current. Worth a manual fix to the table rows. Gap: lvt-datatable still missing from the primary reference (docs/reference/lvt-attributes.md) The PR description says lvt-datatable was added because it was 'absent despite being the opt-in that actually provides the sorting and pagination reference.md attributed to plain lvt-columns.' It was added to skills/tinkerdown/reference.md, but I don't see it anywhere in docs/reference/lvt-attributes.md, not in a dedicated entry, and not in the 'Tinkerdown-Specific Attributes' summary list, even though it is already documented elsewhere (docs/guides/auto-rendering.md, docs/guides/progressive-complexity.md) as a real, implemented attribute. That page's header still claims 'Complete reference for all lvt-* attributes.' This gap isn't caught by the new test because TestDocumentedAttributesExist only verifies that documented attributes exist; it has no check for the reverse (implemented attributes being documented), so an omission like this is invisible to it. Minor nits
Everything else The regex boundary handling, the _test.go-exclusion fix (with the self-referential lvt-filter-in-comments bug it caught), and the TestRemovedAttributesAreReallyGone closing-the-loop-on-the-skip-lists test are all well-reasoned and well-tested. No security or performance concerns; this is a doc/test-only change with no runtime behavior touched, consistent with the 'no CHANGELOG entry' call. |
Two review-bot polish notes on #300. One was right; the other was wrong, and my first verification of it was wrong too. Right: namespaced attributes are checked at lower fidelity than bare ones. lvt-el:bogus:on:success collapses to its namespace and passes, because members dispatch at runtime rather than existing as literals in the bundle, and lvt-on: takes arbitrary DOM event names with no enumerable set at all. Catching bogus members would mean hard-coding each namespace's member list and keeping it in sync with the client -- a second invariant to rot. Documented as a known limit rather than left for a reader to assume away. Wrong: documentedAsRemoved["lvt-window"] is not unreachable dead weight. I probed normalizeAttribute("lvt-window-{event}"), got "", and called the claim confirmed -- but that string is never what the pipeline produces. The extraction regex's character class excludes "{", so the doc's "lvt-window-{event}" is captured as "lvt-window-" and trims to "lvt-window", with no brace left for the placeholder check to catch. I had tested an input the pipeline never generates. Deleting the entry fails TestDocumentedAttributesExist, which is how it surfaced. Restored, with a comment explaining why it looks dead but is not, and a "lvt-window-" case pinning the truncation so the reasoning is not repeated. That is the fifth instance in this phase of a claim believed because nothing could contradict it -- and the only one committed while fixing the others. What caught it was not care but the existence of a check that could fail. Verification: GOWORK=off go test ./... green including the root package with all 32 !ci e2e files (839s). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
Both notes addressed — one was right, one wasn't, and my first check of the second was wrong in a way worth recording. Namespace fidelity — correct, and now documented.
Deleting the entry failed Worth naming: that's the fifth claim in this phase believed because nothing could contradict it — the docs, the self-certifying scan, the unfalsifiable skip list, my own CI monitor's pass/fail condition, and now this. It's also the only one I committed while fixing the others. What caught it wasn't care; it was that a check existed which could fail. Full suite green: 839s, all 32 |
ReviewI independently spot-checked the corrected doc claims against the vendored bundle ( Code quality / test coverage — One gap the guard structurally can't catch, and does exist: I grepped the bundle for all Minor nit (not blocking): No security or performance concerns for shipped code — this PR only touches docs and a new |
Review-bot finding on #300, verified. The guard walks docs -> implementation only, so attributes that exist and are documented nowhere are structurally invisible to it. lvt-scroll-away is live in the shipped bundle -- reads the attribute, validates top/bottom, warns otherwise -- and appears in none of the four doc surfaces. Six of seven sampled attributes behind it are the same (lvt-spy, lvt-upload, lvt-redact, lvt-fx:region-select, lvt-fx:auto-click); only lvt-persist is documented. This corrects a claim in my own Learn text. I wrote that the reference is now "trustworthy as generation context". It is trustworthy in one direction: accurate, not complete. Only the direction that causes incorrect output was closed. The two gaps are different tiers and the plan now says so. A documented-but-absent attribute makes a generating agent emit a page that silently does nothing -- a correctness bug. An undocumented-but-real one merely means the agent never reaches for a capability that exists -- a ceiling on quality. Conflating them would overstate what Phase 2 achieved. Recorded as a risk with explicit M1 Phase 3 guidance: treat the reference's coverage as a floor and check the bundle before concluding a capability is missing. A full implementation -> docs sweep is its own phase and must not expand M1. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h
|
Verified and recorded — and it forced an honest correction to my own Learn text.
The correction: I'd written that the reference is now "trustworthy as generation context." That overstates it. It's trustworthy in one direction — accurate, not complete — and only the direction that produces incorrect output was closed. The plan now distinguishes the two tiers rather than lumping them:
Agreed it's out of scope here — this phase was renames/removals/fabrications in already-documented entries, and a full implementation → docs sweep is its own phase. Recorded as a risk with concrete M1 Phase 3 guidance: treat the reference's coverage as a floor, and check the bundle before concluding a capability doesn't exist. Left Also worth noting for the record: the two |
Final phase of M0 in the ephemeral-UI reframe plan, following #296 and #299.
Roughly a quarter of this repo's own attribute documentation was untrue. Of 35 documented
lvt-*tokens across the four surfaces that teach the vocabulary, ~10 were wrong.The category the plan didn't anticipate
The phase was scoped around renames — find what moved upstream, update the name. It found something worse: attributes that were never implemented at all.
lvt-filter— taught inskills/tinkerdown/reference.md, the exact file M1 plans to hand a generating agentlvt-value-*("extract values from elements") — missed by Phase 1's 11-attribute sample; only the mechanical check surfaced itRenames are self-announcing: something changed upstream, so there's an event to notice. Fabrications are invisible — no upstream event, no rename, just an attribute that quietly does nothing. That's why these survived longest.
The docs were actively broken, not stale-but-working
The audit posed this as an open question; the answer is the harsher branch. The client carries exactly one back-compat shim (
utils/legacy-attr.ts, forlvt-no-intercept). Every other superseded name resolves to nothing — silently, with no console warning.Corrections
lvt-scroll/lvt-highlight/lvt-animatelvt-fx:*lvt-throttle/lvt-debouncelvt-mod:*lvt-disable-withlvt-form:disable-withlvt-{action}-on:{event}lvt-el:{method}:on:{state}lvt-click-away,lvt-window-{event},lvt-focus-trap,lvt-modal-open/close<dialog>for modals)lvt-filter,lvt-value-*The lifecycle tables were wrong beyond the name: the documented
loadingstate and thedisable/enable/focus/blurmethods never existed (real states arepending/success/error/doneperdom/reactive-attributes.ts:44). Added a § Namespace migration table,lvt-form:preserveas distinct fromlvt-ignore, andlvt-datatable— absent despite being the opt-in that actually provides the sorting and paginationreference.mdattributed to plainlvt-columns.No CHANGELOG entry: no example, template, or fixture used a dead attribute, so nothing user-facing regressed. The rot was confined to what we teach.
The durable fix
TestDocumentedAttributesExistchecks every documentedlvt-*against the vendored bundle (internal/assets/client/tinkerdown-client.browser.js) plus production Go — deliberately not a sibling../clientcheckout, so it runs in CI and tests the client that actually ships.Its first version was self-certifying and passed for the wrong reason. It scanned all
.gofiles for evidence an attribute exists — including itself, and its own doc comment nameslvt-filterwhile explaining the bug. Excluding_test.gofixes it, and is right independently: a fixture using a made-up attribute must not vouch for the docs that invented it. Caught only by deliberately reintroducing the rot and watching the guard fail — a passing guard proves nothing until you've seen it fail.The finding that outlives this PR
tinkerdown validatedoes not validate attribute names. Proved empirically: a document usinglvt-filter,lvt-scroll, and a literallvt-totally-made-upvalidates clean, zero errors. Unknownlvt-*attributes emit as inert HTML.This guard closes the docs → implementation direction. The direction M1 depends on is the reverse — generated app → implementation — which is entirely unguarded. M1 Phase 3's design has the skill "self-correct on validate diagnostics until clean," and a clean pass demonstrably does not mean the attributes exist: an agent hallucinating
lvt-sortablegets green validate and a silently dead page.The § Risks entry is rewritten as M1-critical-path (it was scoped as an M2 improvement) with an explicit fork: M1 Phase 3 either accepts the gap in writing, or pulls forward the attribute-allowlist portion of M2.
Verification
GOWORK=off go test ./...green including the root package with all 32//go:build !cie2e files (840s)skill_examples_test.go+TestLLMSTxtExists5/5; every skill example still validates🤖 Generated with Claude Code
https://claude.ai/code/session_018M9pJSPmG6i1D8s6rpEV4h