Skip to content

Add instructions directory, gate committed build output and repair the type gate - #9

Merged
ernysans merged 4 commits into
mainfrom
ernysans-core-node-security-foundation
Aug 22, 2026
Merged

Add instructions directory, gate committed build output and repair the type gate#9
ernysans merged 4 commits into
mainfrom
ernysans-core-node-security-foundation

Conversation

@ernysans

@ernysans ernysans commented Aug 22, 2026

Copy link
Copy Markdown
Member

Summary

Brings this repository's tooling and documentation up to the standard used across the codebase. Foundation only — no change to any type in src/model/ or src/interface/. The compiled output is byte-identical to main.

Two CI gates are the substance here. Both close gaps where a change could pass review and not take effect.

1. The committed build output is now gated

lib/ is committed, exports points straight at it, and there is no prepare script — so installing this package from git performs no build. Consumers execute the committed lib/; reviewers read src/. Those are different files, and nothing in a PR diff can reveal a mismatch, because the src/ diff is exactly what it claims to be.

There is no drift today — this is a latent hazard, not a live defect. Verified by deleting lib/ entirely, rebuilding, and observing zero diff.

CI now regenerates the output after every build and fails if it differs from what was committed.

The check uses git status --porcelain, not the more obvious git diff --exit-code. Measured, not assumed:

Drift shape git diff --exit-code git status --porcelain
Existing module changed detected detected
New module added exit 0 — MISSED detected
Module removed detected detected

git diff sees only tracked files, so a new module compiles to untracked output and slips through — i.e. the obvious form would have been inert exactly when someone adds a module.

2. The type gate was inert in two separate ways, and is now repaired

The runtime suite cannot fail on a type change. Interfaces are erased, so a test that declares an object literal and reads a property back is testing JavaScript. Measured:

Mutation npm test result
Deleted pending?: number from an interface outright 480/480 pass, exit 0
Changed one enum member's value 3 fail, exit 1

npx vitest run --typecheck was not a fallback: typecheck.include defaults to **/*.test-d.ts, this repo has none, so it checked zero files and reported Type Errors no errors with the field deleted.

And tsc -p tsconfig.test.json failed on a clean tree (TS2688) because typeRoots was narrowed to node_modules/@types by the base config — it had evidently never run successfully.

Fixed by widening typeRoots, exposed as npm run typecheck, and gated in CI. Positive-controlled against three mutations — deleted interface field, wrong-typed assignment, removed enum member — each now exits non-zero, with the clean tree at 0.

3. Disclosure guard

.github/scripts/check-private-markers.sh scans tracked files, commit messages and authorship. Commit messages are included because they never appear in a file diff.

  • It self-tests on every run, so a pass means the patterns were demonstrated rather than trusted.
  • It holds no exemption for itself. Patterns are assembled from fragments so the script does not match itself, which means no allow-list and therefore no blind spot — verified by planting a synthetic marker inside the script and watching the check cite its own line number.
  • Pattern tuning was evidence-driven: a looser variant produced 105 false positives (101 base64 integrity hashes, 4 from a video id in a fixture) against 0 true positives. A check that fires on every lockfile refresh gets removed, which is the same inert outcome as no check.

4. Instructions, docs, dependencies

  • New .github/instructions/ (6 files) and a rewritten .github/copilot-instructions.md.
  • CONTRIBUTING.md was boilerplate from an unrelated project — it told contributors to run bower install and polymer test. Rewritten.
  • zod@^4.4.3 added as a runtime dependency, unused for now, so the schema work that follows touches no manifest.
  • The runtime git dependency carried no #ref, so it floated to whatever the default branch pointed at on any plain npm install. The lockfile hid this from CI, which runs npm ci. Now pinned to the exact commit already resolved — behaviour unchanged, the manifest simply states it.
  • npm audit fix (no --force): 17 → 13 advisories (5 high → 2 high).

Verification

All exit codes observed on the final tree:

npm run build       0        lib/ drift          0 files
npm test            0        (480 passed)
npm run typecheck   0
npm run lint        0
private markers     0

Mutation tests run, each reverted and re-verified green: interface field deleted (suite blind, typecheck catches), enum value changed (3 red), enum member removed (typecheck catches), wrong-typed assignment (typecheck catches), new/changed/removed module vs both drift probes, synthetic marker in a tracked file, in a commit message, inside the guard itself, and an attribution trailer.

Attribution-trailer count in commit messages: 0, with the grep positive-controlled (it returns 1 against a synthetic trailer). Authorship: a single identity for both author and committer across all four commits, so a squash merge cannot synthesise a trailer server-side.

Found but deliberately NOT fixed

Reported rather than silently changed, since narrowing a published type is a breaking change for consumers:

  • 8 bare any remain in src/; T | any is 0 — the earlier narrowing has not regressed (both counts positive-controlled). Six of the eight are timestamp fields whose honest type is a three-way union needing the server SDK's FieldValue — a type this package should not pull into every consumer's dependency closure. The other two are a diagnostic snapshot and a deliberate catch-all index signature. The real resolution is runtime schemas, not a type edit.
  • BaseFirestore carries [x: string]: any, which disables excess-property checking for every extending interface everywhere. Deliberate for a sparse document store, but it is why runtime validation is necessary rather than optional.
  • Three settings make the toolchain more permissive than it looks: @typescript-eslint/no-explicit-any is off; tsconfig.json sets "strict": true then overrides it with "noImplicitAny": false and "strictNullChecks": false; and ESLint's files is scoped to src/**/*.ts, so test/ is not linted. Each is a compile-breaking change to flip and belongs in its own reviewed unit of work.
  • 13 advisories remain, all transitive through the one runtime dependency and unreachable from this package's own code, which has no runtime logic. Not suppressed with overrides, since forcing an unrelated version could break a consumer's runtime in a way this package cannot test.

Notes for the follow-up schema work

This branch is the base for it. zod is installed, npm run typecheck now actually checks, and the committed-output gate means a schema that never gets compiled into lib/ will fail CI rather than silently never running.

This package commits its build output and its exports point directly at it,
and there is no prepare script, so installing it straight from git performs
no build. Consumers therefore execute the committed output while reviewers
read the TypeScript source. Those are different files, and nothing in a pull
request diff can reveal a mismatch between them: the source diff looks
correct because the source is exactly what it claims to be.

Add a build step that fails when the committed output differs from a clean
build of the source, with a message naming the fix. It uses
`git status --porcelain` rather than `git diff --exit-code` because git diff
only sees tracked files, so a newly added source module compiles to untracked
output files and git diff exits 0 -- the case that arises precisely when
someone adds a module.

Also add a check for private markers and attribution trailers covering
tracked files, commit messages and commit authorship. Commit messages are
included because they never appear in a file diff. The script proves its own
patterns against synthetic markers on every run, so a pass means the patterns
were demonstrated rather than assumed, and it holds no exemption for itself.
Adds .github/instructions/ with six task-scoped guides and rewrites
.github/copilot-instructions.md to point at them.

The most important content is the public-repository constraint. The
"private" flag in package.json means "do not publish to the npm registry"
and says nothing about visibility; this repository is public while the
services consuming it are not. Every guide leads with that, because the
leak vector is code comments, fixtures, commit messages and PR bodies
rather than documentation alone.

Repairs the type gate, which was inert in two separate ways:

  - tsconfig.test.json could not resolve the vitest globals types because
    typeRoots was narrowed to node_modules/@types by the base config, so
    `tsc -p tsconfig.test.json` failed on a clean tree and had evidently
    never run. Widening typeRoots fixes it.
  - `vitest run --typecheck` is not a substitute: its typecheck.include
    defaults to **/*.test-d.ts and this project has no such files, so it
    checked zero files and reported "no errors" regardless.

This matters because the runtime suite cannot fail on a type change --
interfaces are erased, so a test that declares a literal and reads a
property back still passes once the field is deleted from the source.
Deleting a field outright left 480/480 tests green. The repaired
typecheck catches that, a wrong-typed assignment, and a removed enum
member. Exposed as `npm run typecheck` and gated in CI.

Adds zod as a runtime dependency for the schema layer that will follow,
and pins the existing git dependency to the exact commit the lockfile
already resolved. That spec carried no ref, so it floated to whatever the
default branch pointed at on any plain `npm install`; the lockfile hid
this from CI, which runs `npm ci`. The pin records existing behaviour --
the resolved commit is unchanged.
CONTRIBUTING.md described a different project entirely -- it instructed
contributors to run `bower install` and `polymer test`, neither of which
exists here, and none of its guidance matched this toolchain. Replaced
with the real workflow.

Both documents now lead with the two things most likely to cause a
silently wrong change: that this repository is public while its consumers
are not, and that the committed build output is what consumers execute,
so a src/ change that is not rebuilt is reviewed, approved, merged and
never run.

Also documents that `npm test` and `npm run typecheck` check different
things and that both are required, since interfaces are erased at runtime
and the suite cannot fail on a type change; records the CI step order;
and corrects the dependency list.
`npm audit fix` without --force, so every change is semver-compatible.
Takes the advisory count from 17 (1 low, 11 moderate, 5 high) to 13
(11 moderate, 2 high). Only package-lock.json changes.

Verified afterwards: npm ci, build, test, typecheck and lint all exit 0,
and the build output is unchanged.

The remaining 13 all arrive transitively through the single runtime
dependency and are not reachable from this package's own code, which has
no runtime logic. None can be resolved from here -- they need an upstream
release. They are left visible rather than suppressed with an `overrides`
entry, since forcing an unrelated version could break a consumer's
runtime in a way this package cannot test.
@ernysans
ernysans merged commit f8979b8 into main Aug 22, 2026
2 checks passed
@ernysans
ernysans deleted the ernysans-core-node-security-foundation branch August 22, 2026 22:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant