Re-vendor the §15 fixture: this copy was two generations behind - #4
Conversation
The vendored `test/xss-lint-fixture.test.js` was the 80-line original. The
template on `tgwab-standards` `origin/main` is 221 lines and carries two
controls this copy had neither of: the `expect(flagged).toContain('case1')`
guard (standards #123) and the `describe('the rule reaches this repo')`
coverage block (standards #111). The fixture specimen itself was already
byte-identical to the template, so only the test file moves.
MEASURED FIRST, because a stale copy and an inert rule look alike and have
different fixes. `eslint --print-config` against real shipped source, read for
whether `no-restricted-syntax` resolves at severity 2 with the JSON.stringify
selector, rather than inferred from the config text:
tools.js severity=2 selector=true
features/text-tools.js severity=2 selector=true
features/code-data.js severity=0 selector=true <- declared §15 opt-out
So this repo genuinely passes the coverage control and the stale copy was the
whole defect. The severity=0 is the repo's own named exemption (the code-data
report is joined with newlines and rendered as text, never parsed as HTML)
resolving exactly as written, not a gap.
BOTH NEW CONTROLS WERE SEEN TO FIRE, not merely seen green:
A. `no-restricted-syntax: 'off'` — 3 failed. The case1 guard reports
"expected [] to include 'case1'". Under the 80-line copy this test held
only the two `not.toContain` assertions, which pass green on an inert
rule; that is the hole #123 closed.
B. rule left ON, glob narrowed to `src/**/*.js` (this repo ships nothing
under `src/`) — 2 passed, 1 failed. Only the coverage block sees it,
because the other two lint the synthetic path `src/__xss-lint-fixture__.js`
and cannot distinguish a covered repo from an inert one. This is the
defect #111 closed, reproduced here.
Restored config verified byte-identical after both calibrations.
Gate: `npm run lint` clean, `npm test` 3 passed.
Refs MichalAFerber/tgwab-standards#129
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh
--- amended 2026-09-05: the coverage control changed under this PR ---
tgwab-standards#131 replaced the `> 0` coverage assertion with a named-file
declaration, because `> 0` passed at 7 of 29 in resizewizard-api and at 3 of 3
on pure build tooling in uploadwizard-app. This PR now vendors the new template
and ships `test/fixtures/xss-lint-covers.json`.
Named as product source: tools.js, features/text-tools.js, features/fancy-text.js.
Deliberately NOT named: features/code-data.js, which carries this repo's declared
§15 opt-out and correctly resolves to severity 0.
Reported by the new run: the rule resolves for 10 of 11 linted files.
The named-file assertion was seen to fire here, not just pass: pointing one name
at features/code-data.js — the opted-out file — fails with "the rule does NOT
resolve for `features/code-data.js`, which this repo names as covered product
source." That is the check distinguishing a covered file from an exempted one.
Gate re-run: `npm run lint` clean, `npm test` 3 passed.
5eded7c to
36930f1
Compare
MichalAFerber
left a comment
There was a problem hiding this comment.
Stands in for: REQUEST CHANGES. GitHub will not take --request-changes from the account that
opened the PR, so this is a --comment review. Comment-only: I have not approved, marked ready,
pushed, or merged anything.
Rule 5 — branch and offset of everything I read
tgwab-standards origin/main 0/0 templates/xss-lint-fixture.test.js 222 lines sha256 fb7f528f…
tgwab-standards refs/pull/131/head bb6bac1 DRAFT, UNMERGED same path 375 lines sha256 81195623…
textwizard-tools refs/pull/4/head 36930f1
resizewizard-api refs/pull/63/head df8dbe6
uploadwizard-app refs/pull/83/head a12e1ad
wizard-web refs/pull/103/head 5936e9b (shared clone parked on fix/resizewizard-pricing-claims, behind 3 / ahead 1 — NOT read)
resizewizard-extension refs/pull/45/head bb73c0e (shared clone parked on fix/clear-data-releases-seat, behind 6 / ahead 1 — NOT read)
mykk.us-extension refs/pull/40/head 5e9b4fb
No git checkout in any shared clone. Every command ran in a detached worktree under
~/GitHub/.worktrees/<repo>/review-*, at the PR head, with a fresh npm ci / pnpm install,
and each worktree was left byte-clean (git status --porcelain empty) and then removed.
This repo
Reproduced, and it holds up
| claim | measured |
|---|---|
| reports 10 of 11 | 10 of 11 linted files; 3 named — exact |
features/code-data.js is the one uncovered file |
exact — it is the declared §15 opt-out, rule resolves to 0 |
npm run lint clean, 3 tests pass |
clean, Tests 3 passed (3) |
I also mutated the declaration six ways in a throwaway worktree. Every one fires, so the
control is real and not decorative:
| mutation | result |
|---|---|
name features/code-data.js (the opt-out) |
✗ "the rule does NOT resolve for features/code-data.js" |
{"covers":[]} with no declaredGap |
✗ "declaredGap MUST say why…" |
name test/fixtures/xss-lint-fixture.js |
✗ "a file this repo GAINS by adopting the kit" |
name src/nope.js |
✗ "named … but does not exist" |
name scripts/build.js |
✗ "is build tooling, not product source" |
| delete the sidecar | ✗ "xss-lint-covers.json is missing" |
HIGH — the declaration names three files that build no markup, and omits the three that do.
test/fixtures/xss-lint-covers.json:2
"covers": ["tools.js", "features/text-tools.js", "features/fancy-text.js"],
"why": "The tool host and two feature modules that build result markup from user-supplied text."Measured across all three named files — no innerHTML, no insertAdjacentHTML, no outerHTML,
no createElement, no textContent, and no template literal containing an HTML tag:
$ grep -nE 'innerHTML|insertAdjacentHTML|outerHTML|createElement|textContent' \
tools.js features/text-tools.js features/fancy-text.js
(no output)
$ grep -nE '`[^`]*<[a-zA-Z/]' tools.js features/text-tools.js features/fancy-text.js
(no output)
Control, because that is two negatives in a row: the same two patterns against
features/_editor.js return 15: ${options.length ?
…straight away. The instrument works.
tools.js in particular is a static catalog — export const tools = [{id, name, icon, color, category, tagline, description, keywords}, …], 121 lines of data, no DOM at all. Calling it "the
tool host … that builds result markup from user-supplied text" is not a small imprecision; it is
the one sentence a reviewer is being asked to check.
The files that actually do the §15-relevant thing are covered by the rule and unnamed:
features/_editor.js:13 container.innerHTML = `…`
features/diff.js:48 container.innerHTML = `…`
features/diff.js:88 output.innerHTML = renderDiff(parts) <- parts derives from user text
features/qr-code.js:57 container.innerHTML = `…`
Coverage does not change either way — the rule resolves for all 10 non-opt-out files, so the
assertion passes regardless. What changes is whether the declaration means anything. #131's own
argument for this design is that "a named file can be wrong in an interesting way, and a human can
say so in review." This is that. Please name features/_editor.js, features/diff.js, and
features/qr-code.js, and rewrite why to something that survives a grep.
HIGH — stale kit (see set-level finding 1)
This PR carries the unanchored NOT_PRODUCT_SOURCE[1]. It happens to be harmless for the
current declaration — none of the named paths has a scripts/, test/ or migrations/ segment —
but it is a byte difference from what wizard-web, resizewizard-extension and mykk.us-extension
shipped forty minutes later, and from tgwab-standards #131. Re-vendor after #131 merges.
MEDIUM — the body's own evidence needs one correction
"The count is still reported on every run so a human sees the shape" — measured false here.
npm test in this worktree prints Tests 3 passed (3) and nothing else. See set-level finding 3.
Set-level findings (identical in all six comments)
HIGH — the kit is not byte-identical across the six. It splits two ways, and nobody declared it.
templates/xss-lint-fixture.js — sha256 ff94e2e4… in all six and in both standards refs. Clean.
templates/xss-lint-fixture.test.js:
| sha256 | repos |
|---|---|
81195623… |
wizard-web #103 · resizewizard-extension #45 · mykk.us-extension #40 — matches tgwab-standards refs/pull/131/head |
79920a9c… |
textwizard-tools #4 · resizewizard-api #63 · uploadwizard-app #83 — matches nothing on either standards ref |
fb7f528f… |
tgwab-standards origin/main (the retired > 0 version) |
The whole difference:
const NOT_PRODUCT_SOURCE = [
/(^|\/)[^/]*\.config\.(js|mjs|cjs)$/,
- /(^|\/)(scripts|migrations|tests?|__tests__|e2e|loadtest|test-kit)\//, <- the three earliest PRs
+ /^(scripts|migrations|tests?|__tests__|e2e|loadtest|test-kit)\//, <- #131 head, and the three later PRs
];plus the comment paragraph explaining the anchor. Commit times say what happened: #4 22:24, #63
22:27, #83 22:29 all carry the unanchored form; #103 22:32, #45 22:34, #40 22:38 carry the
anchored one; tgwab-standards #131 was amended at 22:47. The anchor was discovered while
doing wizard-web — #131's own comment says so — and propagated forward but never back.
This is live, not cosmetic. I dropped the group-B copy into the wizard-web worktree, left
wizard-web's own covers.json untouched, and ran it:
× resolves for every file this repo NAMES as covered product source
→ DS §15: `../../apps/punctuationwizard/src/scripts/tool-mount.js` is build tooling,
not product source.
Same declaration, two vendored copies, opposite verdicts, on real browser source. That is exactly
the comparability §15's byte-identity rule exists to protect.
HIGH — all six vendor from an unmerged draft. Merge order is a gate, not a preference.
tgwab-standards #131 is state=OPEN isDraft=true. Until it lands, origin/main templates/
still carries the 222-line > 0 control, so no repo in this set is byte-identical to the
standard as written, which is the §15 requirement each PR body cites. #131 has already moved
once under three of these PRs. Nothing here should merge before #131 does, and the three group-B
repos must re-vendor from #131's merged form afterwards.
HIGH — "the count is still reported on every run so a human sees the shape" is false in five of six.
Every body carries that sentence. Measured, with controls:
| run | coverage line printed? |
|---|---|
npm test / pnpm test (default reporter, non-TTY) — what CI runs |
no |
CI=true npm test |
no |
| same run, but with the test failing | yes |
vitest run --reporter=verbose |
yes |
turbo run test (wizard-web only — turbo gives the child a pty) |
yes |
Controls, because an empty result is a claim about the instrument: (a) the identical console.log
does print through the same pipe when the assertion fails, so the pipe and the grep both work;
(b) a fresh two-line passing test that only calls console.log also prints nothing under the
default reporter; (c) plain node -e 'console.log(...)' through the same pipe prints. Vitest 4's
default reporter suppresses console.log from passing tests on a non-TTY.
No repo in the set sets a reporter ("test": "vitest run", no reporters in any vitest config).
So the number is visible exactly when the test is red — when it is the least interesting thing on
screen — and invisible on every green CI run, which is the case the sentence is about.
MEDIUM — the sidecar cannot express a partial gap, so the two repos that have one say nothing.
declaredGap is read only when covers is empty. A repo with genuine partial coverage has
nowhere to put it and the test never asks. Consequence inside this very set: uploadwizard-app
declares "our .astro/.ts product surface is invisible to this rule" — and wizard-web, same
Astro stack, 73 .astro + 8 .ts invisible, 2 of them containing JSON.stringify, declares
nothing, purely because its covers is non-empty. Same gap, opposite visibility, decided by a
schema branch rather than by the facts. Worth raising on #131 before it merges.
LOW — two prose defects repeated in all six bodies.
- "the 221-line template on
origin/main" — it is 222 lines (wc -l, file ends\n). whyis the only human-reviewable field in the sidecar and nothing reads it. Four of the six
whystrings I checked contain a factual error (see the per-repo sections). A field no gate
touches is fine; a field no gate touches that the design calls "the whole point" deserves a
reviewer checklist item in §15.
|
Blocking — the The mechanism here is sound; I want that said first, because the six-way mutation matrix on this repo's declaration is what makes the new control credible for the whole set, and it all fires. The problem is the content of the declaration, which no mutation can reach — prose is not measured.
So the declaration names three files that build no markup at all and omits all three that do. Not imprecise — inverted. Rule 4, because a zero is exactly the reading that should be distrusted: the instrument produces non-zero on the same corpus in the same run — 9, 3, and 3 on the omitted files — so the three zeros are a measurement rather than a broken grep. I also checked Why this is worse than the gen2 fixture it replaces. The old fixture proved the rule was wired and claimed nothing about coverage. This one makes an affirmative claim about which files carry the hazard, and that claim is green while pointing at the wrong files. A future reader trusts it, and the three files that actually construct DOM from user-supplied text are the ones nobody is watching. An empty The fix is a swap, not a rewrite: name
Everything else in this PR reproduces exactly: 10 of 11 linted, Also blocking, separately and for the whole set: this vendors tgwab-standards#131 while #131 is still an open draft. #131 merges first — otherwise the estate's best-covered adopters end up furthest from the published standard. |
|
Blocking — this vendors a kit version that exists nowhere in
It is not one generation behind. It is an unsourced fork, and the delta is 22 diff lines: a comment paragraph appearing in no History search over Why this matters more here than it would elsewhere: §15's entire comparability argument rests on the kit shipping byte-identical, and this PR's own body invokes that requirement by name. A re-vendor PR that vendors a file with no canonical source fails at its one job — and it is precisely the outcome the amendment says it exists to prevent, "so six repos do not vendor the weak version and get re-fixed next sweep." No behavioural impact today — this is latent, not live. The unanchored regex only differs on a mid-path Three of the six sibling PRs ( Also blocking, and it applies to all six: #131 is still an open draft. Every one of these ships Correcting my own earlier reading on this PR set: I measured these at "365–375 lines, both markers present, ahead of the template" and treated that range as one population. It is two, and the hyphen was doing the work of hiding the finding. |
|
Correction to my comment above — the SHAs I quoted were wrong. The finding and the partition are unchanged; the identifiers were not blob SHAs. Canonical git blob SHAs, read from GitHub's tree API (authoritative — no local hashing, no ambiguity about which function or which content):
Two independent instruments agreed on the partition and disagreed on the digits, which is what prompted me to go get the canonical value rather than pick a side. The partition was always the finding; quoting an unverifiable identifier as evidence for it was a defect in my comment, not in the finding. |
…e three that build none
The declaration was inverted. It named `tools.js`, `features/text-tools.js` and
`features/fancy-text.js` as "the tool host and two feature modules that build
result markup from user-supplied text." Measured across all 11 linted files,
counting occurrences of innerHTML, insertAdjacentHTML, outerHTML and
createElement:
features/_editor.js 9 <- omitted
features/diff.js 3 <- omitted
features/qr-code.js 3 <- omitted
the other eight 0 <- three of them were the ones named
So the three named files build no markup at all and all three that do were
missing. Green, and pointing at the wrong files — which is worse than the
generation it replaced, because that one proved the rule was WIRED and claimed
nothing about which files carry the hazard.
Counted with `grep -o` per construct rather than `grep -c`, so a line carrying
two constructs counts twice, and every one of the 15 nonzero sites was read
rather than trusted: all 15 are real assignments or `document.createElement`
calls, none in a comment. The zeros were read too — `tools.js` matched
`<[a-zA-Z/]` once, and it is `features/<id>.js` inside a comment on line 3, not
markup. The instrument produced 9, 3 and 3 on the same corpus in the same run,
so the eight zeros are a measurement rather than a dead grep.
`features/code-data.js` stays out and is still correct: it carries the declared
§15 opt-out, its report is join('\n')-ed and rendered as text, and it scores 0
on every construct. Control: adding it to the corrected declaration fails with
"the rule does NOT resolve for `features/code-data.js`", so the gate still tells
a covered file from an exempted one.
Verified: npm test 3 passed; npm run lint clean; JSON parses.
Not in this commit: the kit re-vendor. tgwab-standards#131 moved again today
(template blob 3f6ed048 -> b0dc3063) and this repo re-vendors after it merges.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh
…identical Vendored from `ds/comment-accuracy` at f022390. Previous copy was b71e006. Three things changed in the kit since this repo's copy: 1. The count is written with process.stdout.write instead of console.log. vitest 4 selects its reporter with `[isAgent ? 'agent' : 'default']`, and the agent reporter is MinimalReporter with `silent: 'passed-only'` — console output from PASSING tests is dropped. std-env sets isAgent from CLAUDECODE / AI_AGENT / CURSOR_AGENT, so every agent-run `npm test` in this estate hid the §15 count while every CI run showed it. Hardening, not a repair: the line was in this repo's CI log already. 2. Names in xss-lint-covers.json resolve from the cwd first and the repo root second. The sidecar was already found from the test file's own directory; the names inside it were still cwd-relative, so the robustness stopped at the file. wizard-web writes `../../apps/…` and mykk.us-extension writes `content.js`; both are valid now. 3. The build-tooling denylist is applied to the repo-root-relative form, which closes the `../../scripts/foo.js` escape the file documented as open. ALSO CLOSES A DIVERGENCE THAT SHOULD NOT HAVE EXISTED. Three of the six adopter PRs carried blob b71e006, a 365-line copy that appears NOWHERE in the template's history — a working copy that never landed in templates/, carrying the UNANCHORED denylist `/(^|\/)(scripts|…)\//` instead of `/^(scripts|…)\//`. Latent in all three, because no sidecar in any of them names a mid-path `scripts/` segment, so all three were green. §15's comparability argument is that the kit ships byte-identical; six repos, three generations, all green is exactly the silence it exists to prevent. Verified: npm test 3 passed; lint clean; the vendored file hashes to e35e98a, byte-identical to the template. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh
|
Ready for review — devops attestation. Blob identity verified independently, not taken on report. Read through the git trees API at this PR's head and at All six adopters carry that same blob. §15's comparability argument rests on byte-identity, and line counts already hid a divergence tonight — 365 and 375 were reported as one range "365–375" when they were two populations, one of which ( MERGE ORDER: One finding withdrawn on the way here, and it is worth recording why. Three reviewers independently found that a §15 coverage line "does not appear in test output". It was an artefact of a shared instrument: So the count did reach CI all along, and the |
The vendored
test/xss-lint-fixture.test.jshere was the 80-line original. The template ontgwab-standardsorigin/mainis 221 lines and carries two controls this copy had neither of — theexpect(flagged).toContain('case1')guard (standards #123) and thedescribe('the rule reaches this repo')coverage block (standards #111).The fixture specimen
test/fixtures/xss-lint-fixture.jswas already byte-identical to the template, so only the test file moves.The measurement came first
A stale copy and an inert rule look identical from the config text and have different fixes, so this was measured before anything was vendored —
eslint --print-configagainst real shipped source, read for whetherno-restricted-syntaxresolves at severity 2 carrying the JSON.stringify selector:tools.jsfeatures/text-tools.jsfeatures/code-data.jsThis repo passes the coverage control, so the stale copy was the whole defect — it is not in the
resizewizard-apiclass. The severity-0 is the repo's own declared §15 opt-out resolving exactly as written (the code-data report isjoin('\n')-ed and rendered as text, never parsed as HTML), not a gap.Both new controls were seen to fire
A control that has only ever been observed green is not yet evidence, so each was induced to fail:
A —
no-restricted-syntax: 'off'→ 3 failed, the guard reporting:Under the 80-line copy that test held only the two
not.toContainassertions, which pass green on a wholly inert rule. That is the hole #123 closed.B — rule left ON, glob narrowed to
src/**/*.js(this repo ships nothing undersrc/) → 2 passed, 1 failed:This is the sharper one: the first two tests lint the synthetic path
src/__xss-lint-fixture__.jsand therefore cannot tell a covered repo from an inert one. Only the coverage block sees it. That is the defect #111 closed, reproduced here on purpose.eslint.config.jswas verified byte-identical to its original after both calibrations (git diffempty); no config change ships in this PR.Gate
npm run lint— cleannpm test— 3 passed (3)Repo state read (rule 5)
Rule 6: worktrees
eslint-v10,lint-adopt(both merged branches);gh pr list --state all --search fixture→ #2, #3, both MERGED. No competing work.Refs MichalAFerber/tgwab-standards#129
🤖 Generated with Claude Code
https://claude.ai/code/session_01CRX8K6Ys4CDRDcdknCuPVh
Amended 2026-09-05 — the coverage control changed under this PR
devops caught that the control this PR vendors was itself a gate reporting coverage it did not have:
expect(covered.length).toBeGreaterThan(0)passed at 7 of 29 inresizewizard-apiand at 3 of 3 on pure build tooling inuploadwizard-app. Fixed intgwab-standards#131before these land, so six repos do not vendor the weak version and get re-fixed next sweep.The assertion now names files. Each adopter ships
test/fixtures/xss-lint-covers.jsonnaming the product source the rule must cover; the test asserts the rule actually resolves for each, and refuses build tooling, kit files, and names that do not exist. The count is still reported on every run so a human sees the shape.A sidecar rather than a field in the test file, because §15 requires the kit to ship byte-identical and the whole comparability argument depends on that.
A floor proportion would have been worse —
uploadwizard-appscores 100%.Honest about what it is: nothing can check that a named file is genuinely product source. It is a reviewable declaration, not a measurement, and the template says so in place — reading the old check as a measurement is exactly how it failed.
This repo: names
tools.js,features/text-tools.js,features/fancy-text.js. Reports 10 of 11.features/code-data.jsis deliberately not named — it carries the declared §15 opt-out. Seen to fire: naming it fails with "the rule does NOT resolve forfeatures/code-data.js", which is the check telling a covered file from an exempted one.Amended 2026-09-05 — the
coversdeclaration was inverted, and is corrected at62ba7feThe review is right and the fix is the swap it asked for. No softened justification: the entry is
replaced, not reworded.
The full ranking, all 11 linted files, zeros included
Counted with
grep -oper construct and summed — notgrep -c, so a line carrying twoconstructs counts twice.
innerHTMLinsertAdjacentHTMLouterHTMLcreateElementfeatures/_editor.jsfeatures/diff.jsfeatures/qr-code.jsfeatures/analyzers.jsfeatures/case-converter.jsfeatures/code-data.jsfeatures/fancy-text.jsfeatures/generators.jsfeatures/text-tools.jsfeatures/translators.jstools.jsNo omitted file outranks a named one. The three named are the only three with a nonzero score.
The counts were read, not trusted
All 15 nonzero sites are real assignments or
document.createElementcalls — none in acomment or a string:
The zeros were read too, and one of them was a trap:
tools.jsmatches<[a-zA-Z/]once, at line3 —
features/<id>.js, inside a comment. Code-shaped text in a comment is exactly what inflatesthis kind of tally, so it is named here rather than left in a total.
Rule 4: the instrument produced 9, 3 and 3 on the same corpus in the same run, so the eight
zeros are a measurement and not a dead grep. I also checked the three former entries for markup by
a route the grep cannot see, since the review invited that: zero template literals containing an
HTML tag, zero
<tagoccurrences outside that one comment, zeroappend/insertBefore/replaceChildren/setHTML.tools.jsis a 121-line registry with nodocument.*reference atall;
text-tools.jsandfancy-text.jsare string transformers, andfancy-text.js's only DOMtouch is one
.value. There is no such route. The entry was wrong, not imprecise.features/code-data.jsstays out, and the gate still tells it apartIts §15 opt-out is unchanged and still correct. Seen to fire: adding it to the corrected
declaration fails with
the rule does NOT resolve forfeatures/code-data.js``.On the MEDIUM about the count never being printed — measured false
The set-level finding says
npm testhere printsTests 3 passed (3)and nothing else. That istrue locally and not true in CI, which is the run the sentence is about. This repo's own CI log
at the reviewed head
36930f1:Full evidence, five repos, and the local/CI mechanism, is on
tgwab-standards#131— where thereport is now written to stdout so the two agree.
Gate
npm test— 3 passed (3)npm run lint— cleanxss-lint-covers.jsonparsesStill a draft. The kit re-vendor is not in this push —
tgwab-standards#131moved again today(template blob
3f6ed048→b0dc3063), so this repo re-vendors from its merged form.