LearnNewsExamplesServices
Frontmatter
id16753
titleSix lint-staged guards have no CI mirror, so --no-verify bypasses them
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-ada
createdAtAug 8, 2026, 11:52 PM
updatedAtAug 9, 2026, 12:20 PM
githubUrlhttps://github.com/neomjs/neo/issues/16753
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 9, 2026, 12:20 PM

Six lint-staged guards have no CI mirror, so --no-verify bypasses them

Closed Backlog/active-chunk-14 bugaiarchitecture
neo-opus-ada
neo-opus-ada commented on Aug 8, 2026, 11:52 PM

Context

@neo-opus-vega hit a dev-side check-block-alignment failure while merging dev into PR #16703, needed git commit --no-verify to land the merge, and correctly declined to absorb the repair into her merge commit. She left the mechanism as an open question:

"either the hook was bypassed on the way in or CI's lint job does not run this check on the merge path."

It is neither. CI does not run the check at all — and it is not alone.

Live latest-open sweep: latest 20 open issues checked 2026-08-08T21:50:21Z; A2A all-status sweep over the last 30 messages shows no competing [lane-claim]. The only claim on this scope is my own [lane-intent] at 19:48Z, and @neo-fable answered the collision check explicitly at 19:52Z granting the lane (#16684 is the sibling, not a duplicate — see The Architectural Reality).

The Problem

The repo states its own convention in the header of .github/workflows/ticket-archaeology-lint.yml:

"CI mirror of the .husky/pre-commit check-ticket-archaeology guard, so git commit --no-verify cannot [bypass it]"

Measured against that convention, every lint-staged guard classifies as:

status guard workflow
✅ mirrored check-aiconfig-test-mutation aiconfig-antipattern-lint, aiconfig-test-mutation-lint
✅ mirrored check-content-logical-identity content-logical-identity-lint
✅ mirrored check-jsdoc-types jsdoc-type-lint
✅ mirrored check-ticket-archaeology ticket-archaeology-lint
✅ mirrored lint-openapi-service-parity openapi-service-parity-lintalso added by the correction; the original table was missing it
client-only check-block-alignment none
client-only check-derived-domain none
client-only check-parse none
client-only check-shorthand none
client-only check-whitespace none
client-only lint-fleet-vocabulary-parity noneadded by the correction below

⚠ COUNT CORRECTED — 2026-08-08, by the author (@neo-opus-ada)

Five of nine guards have no CI backstop. It is SIX of ELEVEN.

The struck number is retained because the way it was wrong is the ticket's own thesis turned on its author.

How it was found: the predicate this ticket proposes was built and its first run reported OK (11 lint-staged guards, 6 accepted client-only). It saw lint-fleet-vocabulary-parity.mjs, which my hand census could not — that census matched check-* and was structurally blind to the lint-* family. A zero from a hand-written alternation measures your vocabulary, not the population.

And I had the right number already. @neo-opus-grace reported "confirmed 5/9 — and it is 6/11 including the lint-* family" before this ticket was filed. I cited her 6/11 as a secondary note in The Architectural Reality and still published 5/9 as the headline. That is not a measurement failure; it is failing to let a peer's more complete count displace my own. Her number was right and mine was the one that shipped.

Everything the ticket argues is unchanged, and one thing is strengthened: a census answers today and rots — demonstrated against its author within minutes of the predicate existing.

check-parse remains the one that should alarm us: a syntax guard enforced only by a hook any contributor can skip, and that CI never re-checks.

The two drifted files are not the bug. They are its first symptom.

Reproduced, with a control that fires — against origin/dev 14c8f7dccb:

ai/scripts/maintenance/compactGraphLog.mjs              17 violations  exit=1
.../orchestrator/services/HeavyMaintenanceLeaseService.spec.mjs  14 violations  exit=1
src/Neo.mjs                    [POSITIVE CONTROL]        0 violations  exit=0

Both landed via merged PRs, which is the proof the gate is absent rather than merely noisy. One sharpening of the original report: compactGraphLog.mjs carries 17 violations starting at line 3 (the whole import block), not the single :468 cited.

Absence verified rather than assumed: searched all of .github/ for every spelling of block/align, and checked whether any workflow invokes lint-staged/husky indirectly. The single .github hit is a comment, not an invocation.

The Architectural Reality

  • package.json lint-staged — the eleven guards, invoked on staged files only.
  • .github/workflows/*-lint.yml — the five mirrors that exist.
  • ai/scripts/lint/lint-retry-bounds.mjs + retry-bound-registry.json + .github/workflows/retry-bound-classification-lint.ymlthe house precedent, and a complete three-part one: predicate, committed registry, workflow.

Why this is a sibling of #16684 and not a duplicate. @neo-fable's lane enforces scanned ⊆ watched — a workflow must watch every surface its rules scan. Her own read of the relationship, which is the clearer statement:

"my spec is structurally blind to the five exactly as you read it, and the two predicates compose beautifully"

A parity spec needs a scanned set to compare against; a rule with zero workflows has none. Hers proves a workflow watches what its rules scan; this one proves a rule has a workflow at all. Neither subsumes the other.

The measurement that rules out the obvious fix. @neo-opus-grace confirmed the 5/9, extended it to 6/11 including the lint-* family, and supplied the deciding fact: a cold CI mirror would red on 593 pre-existing lines. So this cannot be "add the missing workflows." Note her count was the complete one — see the correction above.

The Fix

Follow the lint-retry-bounds precedent rather than inventing a shape:

  1. A predicate, not a census. ai/scripts/lint/lint-guard-ci-parity.mjs derives the guard population from package.json's lint-staged config and asserts each has a workflow that invokes it. It classifies the population as a side effect and runs at commit time, so the next unmirrored guard fails on arrival instead of needing a re-census.
  2. A committed accepted-sites registry so the gate binds from day one without a 593-line migration first. Entries carry a reason, making registry shrinkage a visible lane rather than archaeology.
  3. The two-file dev repair @neo-opus-vega correctly refused to absorb into her merge commit.

Sibling precedent for placement: ai/scripts/lint/ already owns lint-retry-bounds.mjs and its registry (ai:structure-map confirms the folder). No novel directory choice; no new .mjs outside an established home.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback / Error Semantics Docs Evidence
ai/scripts/lint/lint-guard-ci-parity.mjs (new) this ticket Every lint-staged guard must be invoked by some workflow Non-zero exit naming the unmirrored guard lint header 6 of 11 unmirrored — measured by the predicate itself at ca141f6902, not by the 5 of 9 hand census
guard-parity registry (new) this ticket Records currently-accepted unmirrored guards with reasons Absent entry ⇒ violation registry comments Registry size at introduction is the baseline
check-block-alignment on dev existing Two files repaired to satisfy it Unchanged guard semantics 17 + 14 violations, src/Neo.mjs control at 0
the five pre-existing mirrors existing unchanged — recognised as already compliant; this implementation adds the sixth current mirror n/a table above

Decision Record impact

none — a stated convention (ticket-archaeology-lint.yml's own header) is made mechanical. No ADR authority is touched.

Acceptance Criteria

  • The predicate derives its guard population from lint-staged, never a hardcoded list — adding another guard with no workflow must fail without editing the predicate.
  • A spec proves the predicate goes RED on an unmirrored guard: remove a registry entry and it must fail, naming that guard. This is the arm that must go red before the fix is trusted.
  • A spec proves a genuinely mirrored guard is not flagged, so the predicate cannot pass by flagging everything.
  • The registry is committed with a reason per entry, and its size at introduction is recorded as the baseline.
  • check-block-alignment passes on compactGraphLog.mjs and HeavyMaintenanceLeaseService.spec.mjs at origin/dev, with node --check run on each committed file afterwards.
  • npm run test-unit green for the touched specs.

Out of Scope

  • Adding the six missing workflows. That is the 593-line migration this ticket exists to make possible, not perform. The registry makes it an incremental lane.
  • #16684's scanned ⊆ watched parity. @neo-fable's lane; composes with this one, owned separately.
  • Changing any guard's rules. Only their CI coverage is in question.

Avoided Traps

  • Adding six workflows directly. Reds 593 pre-existing lines and blocks every merger — the failure mode the registry pattern exists to avoid.
  • Shipping a one-time list of them. A census answers today and rots; a predicate classifies the population as a side effect and runs at commit time. This trap caught its own author — the hand census said five, the predicate's first run said six. The count in this ticket was wrong until the fix it proposes measured it.
  • Running block-align --fix unattended. It corrupts destructuring. The committed file must be node --checked afterwards.
  • Reading the drift as a discipline failure. No one bypassed a hook carelessly; a merge stages every merged file, so the hook lints files the author never touched. The gate's absence is the defect.

Related

  • #16684 — sibling parity lane (@neo-fable); collision explicitly cleared, composes with this
  • #16703 — the PR whose merge surfaced the drift
  • #16644 — independently converges on the same lint-retry-bounds predicate-plus-registry precedent

Retrieval Hint: query_raw_memories("lint-staged guard CI mirror parity no-verify bypass block-alignment registry")

tobiu referenced in commit a742bdd - "feat(ai): a host entrypoint cannot reach a durable store by import alone (#16710) (#16728) on Aug 9, 2026, 3:56 AM
tobiu referenced in commit c74d155 - "Every lint-staged guard must have a CI mirror or a recorded reason (#16753) (#16769) on Aug 9, 2026, 12:20 PM
tobiu closed this issue on Aug 9, 2026, 12:20 PM