Today cost roughly sixteen push-and-wait cycles discovering serially what one
local run could have surfaced at once. This is the local half of that, built
around one rule: a local gate is worth having only if it reaches the same
verdict CI would.
THE PREFLIGHT IS THE POINT
scripts/ci_preflight.py checks that this machine agrees with the lockfiles,
and refuses everything downstream when it does not. That is not theoretical
hygiene. Run right now it says:
black 26.1.0 != locked 24.10.0
pytest 9.0.2 != locked 8.4.1
coverage 7.10.1 != locked 7.10.2
which is exactly today's failure: `poetry run black --check .` passes here and
fails on GitHub, and an earlier commit titled "satisfy the black gate" had
already reformatted five files with the WRONG black, making the gate worse
while looking like a fix. A green local gate that disagrees with CI is not a
weak check, it is a harmful one, because people stop reading the real result.
It is stdlib-only and run by the system interpreter, so it works when the
environment it is checking is broken -- which is when it is needed. And it
only ever CHECKS. scripts/dev_sync.sh repairs. A gate that silently fixes
your environment hides the drift it exists to surface.
TOOLS COME FROM THE REPOSITORY, NEVER FROM PATH
Every invocation is `poetry run ...` or `pnpm -C js ...`. A bare tool name is
precisely how a local gate ends up disagreeing with CI, so the preflight
inspects those environments rather than PATH.
THE BUDGET IS THE DESIGN
Measured here: preflight 2.6s, lock check 0.6s, black 2.5s, typecheck 7.3s,
core vitest 24.0s, determinism subset 5.4s — about 44s. What that excludes
was measured too: the cli suite alone is 176.3s, three times the whole
budget, and full pytest is ~350s. Those go in ci_parity.sh, which is what you
run before calling a branch ready. A hook that takes minutes becomes
`git push --no-verify` inside a week, at which point it is a control everyone
believes in and nobody runs.
The cli suite is slow for a good reason — real daemons, real sockets, real
subprocesses instead of mocks — so the answer is to run it less often, not to
make it lie faster.
THE SMOKE TEST IS NOW NODE, NOT BASH
js/scripts/smoke-cli.mjs replaces an inline shell test that used mktemp,
export and `test -n`, and therefore could only run on Unix — leaving the one
artifact users actually execute never smoke-tested on Windows. Every Windows
failure today was a Unix assumption leaking. It also checks more than the
original: that the bundle runs with no node_modules in sight, that the secret
honours the requested length, and that revealing twice yields the same value,
because determinism is the product rather than an incidental property.
The lint step is written but commented out: `pnpm -C js lint` still exits
non-zero on 35 known assertions, and shipping a red check into the push gate
is how gates get routed around. It goes in when it goes green.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
Reverts the revert. The round trip was the point: the claim that this is easy
to back out is now tested rather than asserted. `git revert` of the single
gate commit followed by `pnpm install` left no oxlint.config.ts, no tools/
directory, no reference in package.json, and a workspace that typechecks and
passes all 684 tests without it.
Same instinct as killing a live mutation run earlier to prove its SIGTERM
cleanup worked: an escape hatch nobody has opened is a guess.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
Vendors two rules from github.com/dmmulroy/anti-slop into
js/tools/oxlint/anti-slop and wires oxlint into the workspace. This is the
first JS linter in the repo.
TWO of the project's fifteen rules are enabled. The rest were measured
against this codebase and rejected, with reasons recorded in
oxlint.config.ts, because "why is this off" is the question a future reader
will have. Briefly: no-conditional-empty-object-spread has 77 hits that are
all correct, since that idiom is REQUIRED by exactOptionalPropertyTypes;
no-unsafe-dictionary-type and no-runtime-typeof have 176 hits that are
overwhelmingly the agent's untrusted socket protocol, where a precise type
would be false confidence and the per-field typeof checks ARE the boundary
validation both rules ask for; three more have zero hits because the suite
already uses real dependency seams rather than module mocks.
The two that are on catch the same move: an assertion that manufactures
certainty the code never established. That is the failure mode a vault cannot
afford, because a wrong belief about a decrypted record's shape is a wrong
belief about someone's secrets.
They find 44 sites, and no-widen-then-assert finds none. Deliberately NOT
wired into CI yet: a gate that lands red is a gate people learn to ignore.
The findings get triaged and fixed first, then the CI step goes in green.
Kept deliberately revertible and separate from any change it motivates:
nothing under packages/*/src imports it, it is dev-only, and the shipped CLI
bundles with no externals so it cannot reach the published artifact —
verified, the rebuilt bundle contains no reference to it.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01Vtx94nXDmNgXEeThUZP4QN
- scripts/generate_ts_port_fixtures.py: deterministic fixtures from public
test mnemonics (passwords v1+v2, BIP-39/85, TOTP, Nostr keys, managed
seeds, fingerprints, index keys, entry schemas, V3 vault payload).
Byte-identical across runs; v1 values match the frozen entropy-integrity
vectors.
- js/: pnpm workspace with @seedpass/core and @seedpass/test-vectors.
Core implements BIP-85, password gen v1 (frozen) + v2, TOTP, Nostr keys,
fingerprints, vault index key, and V3 AES-GCM decrypt using only
noble/scure crypto + WebCrypto. Strict TS, no Node/DOM deps in core.
- 82 parity tests pass in Node and a jsdom browser-like environment.
- docs/typescript_port_compatibility_matrix.md tracks per-area status.
Co-Authored-By: Claude Fable 5 <[email protected]>