Commit Graph

11 Commits

Author SHA1 Message Date
ethernet 3f867b2ca9 feat(lints): require an age-gated .npmrc beside every package-lock.json
npm reads the project's own .npmrc, $HOME, and the global config. It
does not walk up parent directories (npm/npm#11437 is still open), so
the root .npmrc gates the root install and nothing else. Two nested
projects were resolving with no supply-chain quarantine at all:
scripts/whatsapp-bridge and plugins/platforms/photon/sidecar. Both now
carry their own gate, and `npm ci` in each returns its lockfile
byte-identical.

A project is a directory holding both package-lock.json and
package.json -- the pair npm resolves against. A lockfile with no
manifest beside it is a vendored artifact; nix/ keeps one for a
hash-pinned fetch that never consults npm config.

The required floor is read from the root .npmrc instead of hardcoded,
so the standard is raised in one place. A nested project may be
stricter, never weaker.
2026-08-14 09:04:06 -04:00
ethernet 05650c9944 feat(lints): hold package.json overrides to the exact-version rule
An override forces a version on every transitive consumer, so a ranged
override re-floats packages the manifest never names. That is a wider
hole than a ranged direct dependency, and the lint did not look at it.

Overrides nest, so the walk is recursive. `npm:<pkg>@<version>` aliases
are now recognised too, and held to the same rule on their version
tail; before this they were rejected outright even when exact.

Three ranges are pinned to close the lint on the live tree. yauzl and
the bridge's protobufjs take their locked versions; the root protobufjs
override is dormant (no protobufjs resolves anywhere in the root tree)
and takes its own declared floor. Re-resolving both lockfiles with
`npm install --package-lock-only` returns them byte-identical.
2026-08-14 08:59:56 -04:00
ethernet be3fa8b6a5 fix(lints): anchor uv exclude-newer-package key edits to whole keys
An unanchored key regex matched inside a longer key with the same
suffix: removing `h2` from `{ python-h2 = false, h2 = "..." }`
produced `{ python- }`. The structural verifier caught the unparseable
result and refused the write, so no file was corrupted -- but the fixer
raised, and `run.py --fix` is deliberately not continue-on-error, so
the autofix job would go red on main and stay red.

A word boundary is insufficient: \b does not fire between `-` and `h`.
Use a lookbehind that rejects word characters, dots, and hyphens.

Both edit paths and both TOML forms are covered; reverting the anchor
fails the new tests.
2026-08-14 08:57:23 -04:00
ethernet 5b15b192d6 lints: adopt three AGENTS.md policies as enforced checks
- no-hardcoded-hermes-home: the Known Pitfalls rule (source of 5 bugs
  in PR #3575). flags Path.home()/'.hermes' + expanduser variants in
  production code; a get_hermes_home()/HERMES_HOME mention within 8
  lines counts as a guard hint (fallbacks and deliberate HOME anchors
  like the desktop-ssh token root pass untouched), '# hermes-home: ok'
  is the explicit override. found and fixed one real bug: the telegram
  adapter's gmail-triage script path ignored the active profile.
- no-ansi-erase-eol: \033[K leaks as literal ?[K under prompt_toolkit
  patch_stdout; comments documenting the pitfall are exempt.
- skill-frontmatter-standards: the HARDLINE authoring rules that were
  review-only — description <=60 chars/ends with period/no marketing
  words, name present, platforms: required when skill scripts import
  POSIX-only primitives. live tree already clean on all three.
2026-08-14 08:33:56 -04:00
ethernet 6514dea0a3 lints: migrate tests + infographic workflow, add dep-specifier lints
five new registered lints, all blocking, none autofixable:

- engines-satisfiable: from tests/test_engines_satisfiable.py, which
  lived in the python pytest lane — a package.json-only PR never ran
  it, the exact classifier gap that let the npm-12 floor outage ship.
- no-shadowed-test-definitions: from the pytest guard; same AST scan,
  per-file annotations instead of one aggregated assert.
- no-committed-infographics: from infographic-check.yml; deletes the
  workflow and its ci.yml wiring (one fewer runner per PR).
- workflow-sha-pins: the AGENTS.md action-pinning policy (commit SHA,
  never tag/branch), previously enforced only by review. repo is
  already clean — every uses: ref is pinned.
- pyproject-dep-bounds: the AGENTS.md upper-bound policy (>=floor,<
  ceiling; git deps SHA-pinned) plus the pin-consistency invariant
  pulled out of tests/test_packaging_metadata.py (the lazy_deps-coupled
  tests stay put). self-referential hermes-agent[extra] includes exempt.
- packagejson-exact-versions: every tracked package.json dependency
  must be an exact version or file:/link:/workspace: ref — ranges float
  onto minutes-old releases via unreviewable lockfile churn. fixed the
  one violator (whatsapp-bridge: express/pino/qrcode-terminal caret
  ranges pinned to their locked versions, lockfile mirror regenerated).

autofix bot also gains ruff check --fix (continue-on-error like npm
run fix; ruff-blocking stays the PR gate), with .py added to the
patch-guard allowance.
2026-08-14 08:33:55 -04:00
ethernet 4874e15dc6 lints: support uv's exclude-newer-package section form too
reading was always plain tomllib — the inline-only restriction was a
write-side artifact. the [tool.uv.exclude-newer-package] section form
is the same one-key-per-line shape the fixer already edits in uv.lock,
so both files now share one section editor; plan() reads entries via
tomllib regardless of form. the loud 'unsupported form' failure is
gone — both TOML shapes are managed.
2026-08-14 08:33:55 -04:00
ethernet 9377af7c33 lints: add uv-stale-exclude (autofix, pyproject + uv.lock)
the python twin of npmrc-stale-exclude, for [tool.uv]
exclude-newer-package. two findings, both autofixable:

- 'pkg = false' disables the age gate for that package forever — an
  unbounded hole. replaced with the uv-documented explicit timestamp:
  the locked version's newest upload-time (from uv.lock) + 1 day, so
  the exception admits exactly the release it was added for.
- entries whose locked versions have all outgrown the exclude-newer
  span (+1d grace) are removed; so are entries matching no locked
  package.

no network: publish dates come from uv.lock's upload-time fields;
versions without one fail open as young. names are PEP 503-normalized
to bridge pyproject's huggingface_hub and the lock's huggingface-hub.

the fixer edits BOTH files, mirroring the change into uv.lock's
[options.exclude-newer-package] so uv lock --check stays green
without re-resolution (admitted locked versions are unchanged by
construction; verified against the real lock). before each write a
tomllib-based verifier strips the exclude-newer-package data from old
and new and refuses unless everything else is structurally identical
— a lock diff that touches a dependency url/hash aborts the fix.

live run on the repo: vercel + huggingface_hub entries removed (both
locked >14d), nemo-relay + h2 converted from false to timestamps.
2026-08-14 08:33:55 -04:00
ethernet a06b587af5 autofix: carve lockfiles out of the .json allowance; verify npmrc writes
guard: package-lock.json slipped through the .json extension allowance
(a hole inherited from the old js-autofix guard). rather than a global
deny list, the allowance itself now excludes lockfile basenames — they
fall through to the default rejection like any other unallowed path. a
bot-authored, auto-merged lockfile rewrite is exactly the shape of a
supply-chain attack.

fixer: before writing, verify the new .npmrc is the original minus
deletions of exclude lines, comments, and blanks only — every surviving
line must appear in the original in order, and every dropped line must
be an exclude entry, comment, or blank. any other delta (touched
directives, edits, insertions) is a fixer bug and aborts the write.
2026-08-14 08:33:55 -04:00
ethernet b7511316a4 lints: add npmrc-stale-exclude (autofix, network)
a min-release-age-exclude[] exists to let resolution pick a release
younger than min-release-age. once every locked version of the
package is itself older than the gate (+1d boundary grace), the
exclude is dead weight and a standing hole in the age gate — this
lint automates the 'remove when > 2wks old' instruction those
entries' comments already carry.

matches exclude patterns (exact, @scope/*) against the sibling
lockfile's locked versions and checks registry publish dates. fails
open per pattern on any fetch error or unknown date — never removes
an exclude on partial data. network lint, so advisory on PRs by
policy; enforcement is the autofix pass on main. the fixer drops a
group's comment block only when no exclude in the group survives.

'# lint: keep' marks intentional standing excludes; applied to
@assistant-ui/* and the ink blocks in .npmrc. also removes the five
already-stale entries the first live run found (react-router,
eslint, @eslint/*, tar, concurrently — each locked >14d).

discovery fix: register lint modules in sys.modules before exec so
@dataclass can resolve annotations.
2026-08-14 08:33:55 -04:00
ethernet 179194a2b2 lints: bound each fixer by its own globs, not the union
run_fixes now snapshots the working tree around every fixer and
validates each change against that lint's OWN fix_touches — a fixer
that writes a file only another lint's globs allow is an error, and
under-reported paths are attributed to the fixer that made them.
snapshot uses git status --porcelain so created files count too.
2026-08-14 08:33:54 -04:00
ethernet c864fc1a0f add project-wide lint registry + runner
lints declare severity (blocking/advisory) and fixability (autofix)
explicitly. runner gates PRs only on blocking unfixable findings;
fixable ones are advisory because the autofix bot resolves them on
merge. first two registered lints wrap the existing checkers:
windows-footguns (was its own workflow job) and subprocess-stdin
(was wired to no workflow at all).
2026-08-14 08:33:54 -04:00