Repository navigation
[quality] Extend the merged callable-core mutation pattern to scripts/validate-architecture-assets.mjs #1759
Description
Finding
stryker.config.mjs sets:
mutate: ['src/**/*.{js,mjs}', 'scripts/lib/**/*.mjs'],Every top-level scripts/*.mjs is outside that scope. Measured at b490a7f
with the repository's own installed Stryker:
$ TZ=UTC npx stryker run --mutate 'scripts/*.mjs'
INFO ProjectReader Found 29 of 771 file(s) to be mutated.
INFO Instrumenter Instrumented 29 source file(s) with 4410 mutant(s)
$ TZ=UTC npx stryker run --mutate scripts/validate-architecture-assets.mjs
INFO Instrumenter Instrumented 1 source file(s) with 325 mutant(s)
npm run test:mutation generates none of those 4410 mutants.
This matters because the unit lane can no longer tell an asserted rule from an
unasserted one in that tree. npm run test:unit:coverage:check (node v26.12.0,
rev b490a7f) reports scripts/ and src/ at 100.00% lines / 100.00%
regions — the gate is saturated, which is exactly the condition
CONTRIBUTING.md's mutation-testing section describes: "The unit coverage gate
shows that a line ran under test, not that any test would notice if it
changed." For these 29 files nothing checks the second part. They are the data
gate that the required Validate repository job runs on every pull request,
one step per validator.
This is not a coverage gap. Both lanes report full coverage of this tree
(the e2e lane scopes to src/** and reports no missing source files). The gap
is assertion strength, and mutation testing is the only gate the repository has
for it.
Approved bounded implementation
On 2026-10-22, the maintainer explicitly approved exactly this additional
validator extraction and its protected merge after required checks. The
implementation is published in #1787; do not open a competing PR.
Use that PR for the live check, queue and merge state. This does not authorize a
repository-wide rollout.
#1752 established the fix pattern and deliberately stopped there: extract the
rules into scripts/lib/<name>.mjs and keep the top-level script as the
input/reporting adapter, so the existing scripts/lib/**/*.mjs mutation target
covers them with no glob, workflow or threshold change. #1757 did this for
awards and merged on 2026-10-21, measuring 101 killed / 0 survived.
#1752 also recorded that "Broader adoption and final acceptance remain
maintainer decisions". The maintainer has now approved this one additional
validator; that approval does not extend to further validators.
Recommended next target: scripts/validate-architecture-assets.mjs — the
largest unmutated file (421 lines, 325 mutants) and the one guarding the
highest-risk surface, since it validates assets imported from third-party
architecture submissions, including the symlink-root containment that
tests/validate-architecture-assets-symlink-root.test.mjs exists for. A
non-equivalent surviving mutant can expose an unasserted validation or
containment rule. Equivalent metadata/default mutations need explicit triage,
not a claimed behavioral fix.
The governance decision reserved by #1752 is approved for this bounded case.
Recommendation
One explicitly approved PR closes this:
- Extract the rules from
scripts/validate-architecture-assets.mjsinto
scripts/lib/validate-architecture-assets.mjs, with invocation-local
findings and an explicit assets root, asscripts/lib/validate-awards.mjs
does. - Keep
scripts/validate-architecture-assets.mjsas the adapter: repository
root and flag resolution, diagnostic formatting, exit behaviour and
success/repair output unchanged. This validator is filesystem-only;
it does not have a JSON input interface. - Point the existing cases in
tests/validate-architecture-assets.test.mjs
andtests/validate-architecture-assets-symlink-root.test.mjsat the
extracted core, retaining subprocess contracts for CLI success,
validation failure, absent asset directories, malformed SVG and
--fixreporting/write behaviour. - Report the measured
npm run test:mutation -- --mutate scripts/lib/validate-architecture-assets.mjs
result in the PR, kill behaviorally distinguishable survivors and
individually explain any equivalent mutations.
The published head is 2d5b8b95a5830022876564ba496298e8a3c60a9f.
Its 83 focused cases, full required unit coverage, six lint gates and build
pass. Scoped mutation improved from 269 killed / 43 survived to
304 killed / 8 equivalent survivors / 0 NoCoverage / 97.44%, with no
timeouts or errors. All 35 behavioral survivors were killed. The eight
equivalents are three unused description strings, a quality flag on a
font-only allowlist, two default UTF-8 encodings and two nondirectory sentinel
strings. No suppressions or threshold changes.
No change to stryker.config.mjs's mutate glob, to the Mutation testing
workflow, or to the break threshold is needed or proposed. Because the extraction moves production code, it is outside this
lane's PR remit either way; it needs a maintainer or a lane that may touch
non-test source.
Priority
- Impact: medium
- Effort: medium
Note: an earlier attempt to file this was auto-folded into #1758 because the
body incidentally named a workflow path. The two findings are unrelated: #1758
is about a missing validator step in the pull-request lane; this one is about
stryker.config.mjs never mutating scripts/*.mjs.
Hive Agent: quality | Instance: hosted-available-lke648397-260827-5n31 | SHA: b490a7f
— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88
Activity
hivecommons-hive commentedon Oct 22, 2026
What to reply
/hive approve: go with the recommendation/hive decision <your instructions>: go ahead, following your instructions/hive help: show these againIf this issue is waiting only for reporter confirmation after a merged fix, the reporter or a maintainer can reply
/fixedinstead.Only a maintainer with write access can un-park; everything else is ignored. Full command reference: https://github.com/hivecommons/hive/blob/v5/src/docs/maintainer-commands.md.
mrbobbytables commentedon Oct 22, 2026
The maintainer explicitly approved exactly this second callable-core extraction as part of the bounded P2/P3 maintenance batch. Implementation is in progress; do not start a competing PR. Source inspection corrects a template assumption: this validator does not parse JSON. The CLI contracts to preserve are filesystem asset success/failure, absent asset directories, malformed SVG, --fix reporting/write behavior and symlink containment. No JSON interface will be invented. Findings and fixed-file reporting remain invocation-local, the repository/assets root is explicit, and mutation scope, workflows and thresholds stay unchanged. The awards pilot1752 remains accepted and closed; measurements for this additional extraction belong here.
hivecommons-hive commentedon Oct 22, 2026
To start work, reply
/hive approve(or/hive decision A). A plain reply acknowledges the issue but leaves it parked.hivecommons-hive commentedon Oct 22, 2026
Folded finding. This was filed as a separate issue, but 1 open agent-filed issue (plus 2 closed as completed in the last 14 days) already cite
stryker.config.mjs, so it was folded into the oldest of them instead of opening another. Treat this issue as the tracker for that component: prefer one structural fix that covers every folded variant over a PR per variant.[quality] Surviving mutants in the shared ISO 8601 timestamp gate (validate-utils isoUtcTime/checkIsoTimestamp)
Finding
StrykerJS over
scripts/lib/validate-utils.mjson main (18caa5e) scores91.67% with 16 surviving mutants. The cluster that matters sits in the
strict ISO 8601 UTC timestamp gate —
isoUtcTime()andcheckIsoTimestamp()—the helper that
validate-community-groups.mjs,validate-community-people.mjs,validate-enduser-members.mjsandvalidate-launch-metrics.mjsall depend on to reject loose, locale-dependentdates and to make their staleness/future-skew windows mean anything.
Line coverage is saturated (
npm run test:unit:coverage:checkreports srcfiles at 100.00% lines and 100.00% regions), so this is a behavioural gap, not
a reachability one: the existing tests execute the guard without pinning what
it decides.
Five mutants are genuinely unkilled behaviour:
validate-utils.mjs:117— all four mutants over the compositeroundTripsexpression (two
ConditionalExpression, twoLogicalOperator). Replacingthe year arm with
true, or welding the arms together with||, changes notest. Nothing asserts that a four-digit year below
0100is rejected, andDate.UTC(26, 0, 1)is1926-01-01— the year arm is the only one that cancatch it, because every other out-of-range component cascades into the
next-higher unit and trips a second arm as well.
validate-utils.mjs:174—time > Date.now() + futureToleranceMsmutated to>=. The two existing future-window tests sit 30s inside and 24h outside thewindow, so neither observes the boundary itself; whether a value exactly
futureToleranceMsahead is inside or outside the skew allowance iscurrently unspecified.
Recommendation
Add behavioural tests to
tests/validate-utils.test.mjs(no production change):Date.UTCremaps into the 1900s (0026-01-01T00:00:00Z,0099-12-31T23:59:59Z), isolating the year arm of the round-trip checkfutureToleranceMsahead of a fixed clock is accepted, and the nextmillisecond is rejected with the
futureMessage. The clock has to bepinned — a value derived from a live
Date.now()is already in the pastby the time the comparison runs
pinning what the
ISO_8601_PATTERNanchors are there forThat takes the file to 94.27% with 11 survivors, verified locally.
The remaining 11 are equivalent mutants and should not be chased:
roundTripsmutants: an out-of-range componentalways cascades into the next-higher unit (
2026-02-31rolls the month,T24:00rolls the day,:60rolls the minute), so no input makes exactlyone of those arms false, and replacing it with
truecan never flip theresult.
Buffer.from(text, 'utf8')→Buffer.from(text, ''): Node acceptsthe empty encoding as utf8.
ISO_8601_PATTERNanchor mutants, and line 111'svalue ?? ''default:
isoUtcTimeresolves the accepted value withDate.parse(value)over the whole string, and V8's
Date.parserejects every wrapped form(leading/trailing whitespace, prefixes, suffixes, parenthesised comments), so
dropping an anchor cannot make the helper accept anything it rejects today.
futureToleranceMs !== undefined→true: with the option absent,Date.now() + undefinedisNaNand the comparison is false either way.CNCF_HOST_SUFFIXES.join(' or ')→join(''): the list holds asingle entry, so the separator never appears in the message.
Priority
validators and a loose date that silently defeats their staleness checks
Evidence
TZ=UTC npx stryker run --mutate scripts/lib/validate-utils.mjs(tap runner, repo
stryker.config.mjs), run locally at rev18caa5eon2026-10-22: 91.67%, 16 survivors → 94.27%, 11 survivors with the tests above.
TZ=UTC npm run test:unit:coverage:checkat the same rev,exit 0, src files 100.00% lines / 100.00% regions.
and this Node-only validator helper is outside its scope, so no end-to-end
coverage source for it exists to read.
🐝 Hive Agent:
quality| Instance:hosted-available-lke648397-260827-5n31| SHA:18caa5e— hive: agent=quality backend=copilot model=claude-opus-5 copilot=1.0.88