Skip to content

[quality] Extend the merged callable-core mutation pattern to scripts/validate-architecture-assets.mjs #1759

Closed

Description

@hivecommons-hive

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.mjs into
    scripts/lib/validate-architecture-assets.mjs, with invocation-local
    findings and an explicit assets root, as scripts/lib/validate-awards.mjs
    does.
  • Keep scripts/validate-architecture-assets.mjs as 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
    and tests/validate-architecture-assets-symlink-root.test.mjs at the
    extracted core, retaining subprocess contracts for CLI success,
    validation failure, absent asset directories, malformed SVG and
    --fix reporting/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

  1. added
    testingApproved by a Hive merger/owner for auto-merge on green CI
    agent/qualityApproved by a Hive merger/owner for auto-merge on green CI
    stage/045-needs-decisionIssue parked on a person: needs-human or a maintainer decision is owed before the hive picks it up
    on Oct 22, 2026
  2. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    What to reply

    • /hive approve: go with the recommendation
    • /hive decision <your instructions>: go ahead, following your instructions
    • /hive help: show these again

    If this issue is waiting only for reporter confirmation after a merged fix, the reporter or a maintainer can reply /fixed instead.

    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.

  3. mrbobbytables commented on Oct 22, 2026

    @mrbobbytables
    Member

    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.

  4. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    To start work, reply /hive approve (or /hive decision A). A plain reply acknowledges the issue but leaves it parked.

  5. hivecommons-hive commented on Oct 22, 2026

    @hivecommons-hive
    ContributorAuthor

    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.mjs on main (18caa5e) scores
    91.67% with 16 surviving mutants. The cluster that matters sits in the
    strict ISO 8601 UTC timestamp gate — isoUtcTime() and checkIsoTimestamp() —
    the helper that validate-community-groups.mjs,
    validate-community-people.mjs, validate-enduser-members.mjs and
    validate-launch-metrics.mjs all depend on to reject loose, locale-dependent
    dates and to make their staleness/future-skew windows mean anything.

    Line coverage is saturated (npm run test:unit:coverage:check reports src
    files 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 composite roundTrips
      expression (two ConditionalExpression, two LogicalOperator). Replacing
      the year arm with true, or welding the arms together with ||, changes no
      test. Nothing asserts that a four-digit year below 0100 is rejected, and
      Date.UTC(26, 0, 1) is 1926-01-01 — the year arm is the only one that can
      catch 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() + futureToleranceMs mutated to
      >=. The two existing future-window tests sit 30s inside and 24h outside the
      window, so neither observes the boundary itself; whether a value exactly
      futureToleranceMs ahead is inside or outside the skew allowance is
      currently unspecified.

    Recommendation

    Add behavioural tests to tests/validate-utils.test.mjs (no production change):

    • reject a year Date.UTC remaps into the 1900s (0026-01-01T00:00:00Z,
      0099-12-31T23:59:59Z), isolating the year arm of the round-trip check
    • pin the future-tolerance boundary: a value exactly
      futureToleranceMs ahead of a fixed clock is accepted, and the next
      millisecond is rejected with the futureMessage. The clock has to be
      pinned — a value derived from a live Date.now() is already in the past
      by the time the comparison runs
    • assert that text wrapped around an otherwise valid instant is rejected,
      pinning what the ISO_8601_PATTERN anchors are there for

    That takes the file to 94.27% with 11 survivors, verified locally.

    The remaining 11 are equivalent mutants and should not be chased:

    • lines 118-122, the per-arm roundTrips mutants: an out-of-range component
      always cascades into the next-higher unit (2026-02-31 rolls the month,
      T24:00 rolls the day, :60 rolls the minute), so no input makes exactly
      one of those arms false, and replacing it with true can never flip the
      result.
    • line 36, Buffer.from(text, 'utf8') → Buffer.from(text, ''): Node accepts
      the empty encoding as utf8.
    • line 90, both ISO_8601_PATTERN anchor mutants, and line 111's value ?? ''
      default: isoUtcTime resolves the accepted value with Date.parse(value)
      over the whole string, and V8's Date.parse rejects every wrapped form
      (leading/trailing whitespace, prefixes, suffixes, parenthesised comments), so
      dropping an anchor cannot make the helper accept anything it rejects today.
    • line 173, futureToleranceMs !== undefined → true: with the option absent,
      Date.now() + undefined is NaN and the comparison is false either way.
    • line 264, CNCF_HOST_SUFFIXES.join(' or ') → join(''): the list holds a
      single entry, so the separator never appears in the message.

    Priority

    • Impact: medium — the guard is the only thing standing between four
      validators and a loose date that silently defeats their staleness checks
    • Effort: low — tests only, no production change

    Evidence

    • Mutation: TZ=UTC npx stryker run --mutate scripts/lib/validate-utils.mjs
      (tap runner, repo stryker.config.mjs), run locally at rev 18caa5e on
      2026-10-22: 91.67%, 16 survivors → 94.27%, 11 survivors with the tests above.
    • Unit coverage: TZ=UTC npm run test:unit:coverage:check at the same rev,
      exit 0, src files 100.00% lines / 100.00% regions.
    • End-to-end: not claimed. The Playwright suite instruments the browser bundle,
      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

  6. added and removed
    stage/045-needs-decisionIssue parked on a person: needs-human or a maintainer decision is owed before the hive picks it up
    on Oct 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent/qualityApproved by a Hive merger/owner for auto-merge on green CItestingApproved by a Hive merger/owner for auto-merge on green CI

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions