Skip to content

feat(skills): add the verify-interface verification skill - #79

Draft
Upd4ting wants to merge 40 commits into
mainfrom
feat/verify-interface-skill
Draft

Upd4ting wants to merge 40 commits into
mainfrom
feat/verify-interface-skill

Conversation

@Upd4ting

@Upd4ting Upd4ting commented Nov 3, 2026 •

Copy link
Copy Markdown
Member

Retargeted to main on 2026-11-05; tarball pins removed.

Why

Antony wants every Clean AntelopeJS repo to carry its V5 verification skill. This adds verify-interface to @antelopejs/interface-database-decorators, so a change to the decorators is proved against a real MongoDB before it merges.

What changed

  • antelopejs-sync --verify interface at pack 0.2.17, no bump: SKILL.md, package.json, tsconfig.json, the .claude/skills link and one AGENTS.md line. sync --check is clean.
  • Repo-owned layer in .agents/skills/verify-interface/: 11 features, a mongo:7 service, four doors, runner.ts, scenarios/, 11 seeded breaks, 4 seeded doors.
  • The package exports decorators and plain functions, with no interface proxy a stub provider could answer, so the pack's every-function journey does not apply. Each runtime journey writes a scenario into a scratch module, runs it with ajs module test -f against @antelopejs/mongodb and a Docker MongoDB, and reads the database back with mongosh. Scenarios decrypt, re-hash and parse stored values with Node's crypto, independently of the library. Index builds are awaited through the driver, so load cannot fake a catch.
  • Type journeys: model-signatures and modifier-signatures pin return types and parameter shapes; rejected-calls requires tsc to refuse 49 wrong calls (wrong option types, missing keys, wrong parameter types) without any suppression directive.
  • declarations/packed-subpaths loads the packed package, not the checkout, and imports every name the shipped skills/.../SKILL.md lists from each subpath.
  • One shipped-file change: the skills/database-decorators-interface/SKILL.md line about unbound classes now matches docs 1, 2 and 4 (a class follows the schema of its schema name when one exists, and throws when none does). No source, package or workflow change. No new SAFETY comment, directive or suppression. No ratchet file.
  • Sweeps of README, docs 1 to 5 and the TSDoc: formulas, option defaults, "any or every" quantifiers, per-method options, serialisation helpers, inheritance, events, sync/async equivalence, parameter and return types, every read path, injection defaults. Not every sentence is covered: each verifier round has found gaps, so the claim is the list above. Other prose has a journey or a "Not driven" row in features/README.md.

Known findings that keep the verdict red

Two journeys FAIL on purpose, each naming its issue. A verdict that runs their feature is FAIL until the issue is fixed.

Proof

Head a70786b2d83eeed24839a5c4c815c2979d9c6f32. Break runs went through slot.sh (load under 32, 20 GB or more available) from round 6 on.

Check Result
antelopejs-verify map clean
sync --check Guardrails are in sync.
End-to-end run: new, doctor, journeys, containments, seeded-doors, doors doctor ok; 63 PASS, 2 FAIL (#80, #81); clean leaves 0 h8v5-* containers
seeded, 11 features 11/11 CAUGHT
seeded-doors 4/4 FLAGGED
containments 3/3 CONTAINED
V4 eval node scripts/evals.mjs seeded --repo <clone> --min-catch 1.0 --min-door-catch 1.0 --jobs 3 (agent-skills main 625e0db) breaks 11/11, doors 4/4, control PASS+NOTES, exit 0, $1.38 over 16 cases
GITHUB_BASE_REF=campaign/integration pnpm run checks Checks: 8 passed, 0 failed
antelopejs-api-diff origin/campaign/integration API: 1 packages, 25 entries unchanged; Runtime API: 13 entries unchanged
Suppressions base to HEAD all rules 29 to 29; SAFETY: 9 to 9; @ts-expect-error 0 to 0
Verifier breaks of rounds 6 to 9 (R2 to R5, B1 to B6, X1 to X7, Y1 to Y6, Z2, Z6) Every one CAUGHT in my runs (logs extras6 to extras12 under ~/.cache/campaign2/v5/logs/)
Own breaks, rounds 2 to 9 Every one CAUGHT (patches under ~/.cache/campaign2/v5/extra3 to extra13). Rounds 6 to 9: DatumStaticMetadata copying the schema, a digest algorithm allowlist, getMetadata always inheriting, wildcard unlock returning an empty record, package files without skills or docs, package exports without relation or common, raw get, raw getAll, dropped request context, a cache key that ignores the instance

Attention

  • The old GitGuardian finding on commit 98dac2d (scenarios/hashing.ts, a startsWith on the scrypt prefix) stays in the PR history. The literal is now built from parts; the prefix is public format text. Antony resolves it in GitGuardian.
  • The pack's minor-bump journey is not in smoke: campaign/integration already exports more than the published 0.1.12 and package.json is still 0.1.12. Add it in the PR that bumps the version.
  • docs/4 says the toDatabase result "has no prototype". It is a plain object with Object.prototype; the journey checks only that it is not a table instance.
  • Under load above 40 the Mongo module sometimes resets its connection, and mocha's 2 s timeouts can fail the repo's own tests (seen at load 27 to 33); rerun then.
  • A verifier, not the owner, must judge this PR.

Upd4ting and others added 19 commits October 31, 2026 00:25
* refactor: look up the index groups, container keys, floating values and schemas by own key

* refactor: read class prototypes through a checked helper

* refactor(modifiers): give the event handler alias no type parameter

* refactor(encryption): check that the auth-tag methods are callable

* refactor(table): collect the mixins without reading any

* refactor(database): insert the fixture rows without an any local

* test: read the stored rows, the module exports and the sort keys without any

* chore: lower the ratchet ceilings

* refactor(modifiers): type the receiver of the generated accessors

* chore: lower the no-unsafe-return ceiling

* refactor(records): look up the unlock arguments and the floating values by own key

* chore: lower the no-inherited-key-lookup ceiling

* refactor(table): assert the mixin subclass to the declared return type

* refactor(modifiers): restore the container unlock lookup to its base form
…the package (#55)

* test(metadata): pin the metadata stored by an older copy of the package

* fix(metadata): upgrade the table metadata stored by an older copy of the package
* test(modifiers): pin lookups by names shared with Object.prototype members

* fix(modifiers): look up a container entry by own key
* test(encryption): pin which stored objects are revived as dates

* fix(encryption): revive only the date marker the encoder writes

* refactor(encryption): type the stored values the date reviver reads
…ng it (#54)

* test(metadata): pin the metadata of subclasses of a table

* fix(metadata): copy the metadata a subclass inherits instead of sharing it

* test(metadata): pin the schema binding of a subclass built before RegisterSchema

* fix(metadata): bind a subclass to the schema registered under its schema name

* refactor(modifiers): copy the modifier lists of a parent from the record helpers

* refactor(modifiers): keep listsCopied private to the record helpers
* test(modifiers): pin modifier fields and events named like Object.prototype members

* fix(modifiers): look up modified fields and event names by own key

* test(modifiers): pin that a modifier on a field named __proto__ is refused

* fix(modifiers): refuse a modifier on a field named __proto__

* refactor(modifiers): keep the refused-field check with the record helpers
* docs: typecheck every example and drop hand-written version prose

* fix(docs): stop the docs program from emitting declarations
…t contracts (#62)

* test(modifiers): pin modifier fields and events named like Object.prototype members

* fix(modifiers): look up modified fields and event names by own key

* build: run pnpm 10.33.0

pnpm before 10.20 fails a frozen install of some file: tarball dependencies
(ENOENT in the store index), which the campaign tarball pins need.

* build(deps): test against the interface-database 0.3.0 tarball

Producer: AntelopeJS/interface-database feat/typed-query-stages at 4939b45, version 0.3.0.
The devDependency range ^0.1.11 excludes 0.3.0, so the contract suite never
ran against it.

* test: read the schema stage's id through interface-database 0.3.0's stage union

The recorded options are typed per stage now, so the tests match the schema
stage and its id instead of reading options.id off any stage.

* test(modifiers): pin that a modifier on a field named __proto__ is refused

* fix(modifiers): refuse a modifier on a field named __proto__

* refactor(modifiers): keep the refused-field check with the record helpers

* test: read the schema stage's id through interface-database 0.3.0's stage union in the subclass binding tests

The subclass binding tests landed after the 0.3.0 tarball pin was written, and
read options.id off any stage; the recorded options are typed per stage now.

* feat(database-decorators)!: type the fixture generator by its table class

Fixture now checks that the class it is called with is the decorated class or
a subclass of it, so it calls the generator with that class without an
assertion. The stored generator gives plain objects instead of records of
unknown values.

BREAKING CHANGE: DatumGeneratorOutput is `Row | Row[] | undefined` over
objects (`Row = object` by default) instead of records of unknown values.

* feat(database-decorators)!: type the model's schema name and index lookups

BasicDataModel keeps the schema name its table class registers into, which is
undefined for a table class without @RegisterTable; the type now says so
instead of asserting a string. getBy takes an index named by a string field of
the table and keys typed as interface-database's selection keys.

BREAKING CHANGE: DataModel.schemaName is `string | undefined`. getBy's index is
`Extract<keyof T, string>` and its keys are `IndexKey` (a value proxy or a
string, number or boolean), no longer `any`.

* feat(database-decorators)!: type the auto-date event handlers by the field they stamp

BREAKING CHANGE: AutoDateModifier.insert and update take
`(object: Partial<Record<F, Date>>, field: F)` instead of a record of unknown
values and a string.

* feat(database-decorators)!: refuse @Index on a symbol-keyed field

An index names a database field, so the decorator throws for a symbol key
instead of recording the symbol as a string.

BREAKING CHANGE: @Index on a symbol-keyed property throws at class
declaration.

* feat(database-decorators)!: drop the once-used type parameters of getModifiedFields and toDatabase

getModifiedFields takes any modifier class and toDatabase takes the table
instance as `{ constructor: any }`. getBy's IndexKey is derived from
Table.getAll instead of interface-database's common subpath.

BREAKING CHANGE: getModifiedFields has one type parameter (the object's type)
instead of two; toDatabase has none.

* feat(database-decorators)!: type the stored unlock arguments as unknown values

The arguments a modifier is unlocked or locked with are whatever the caller
passed for it, so ModifiersDynamicMetadata.fields records them as unknown
arrays and the engine no longer spreads `any` into modifier methods.

BREAKING CHANGE: ModifiersDynamicMetadata.fields is
`Record<string, Record<string, unknown[]>>` instead of `any[]` lists.

* feat(database-decorators)!: record each modifier event with its handler

attachModifier now stores the modifier method it found for an event next to
the field and the modifier, so triggerEvent calls that method with the
modifier as `this` instead of looking it up by name through two assertions.

BREAKING CHANGE: the entries of ModifiersStaticMetadata.events are
`[field, modified, handler]`, with the new exported ModifierEvent type for the
handler.

* build: set the package version to 0.2.0

* chore: lower the ratchet ceilings to the findings left after the contract changes

* test: assert the error type of the symbol-keyed index refusal, not its wording

* build(deps): test against interface-database 6bef278
…nd give (#67)

* feat(database-decorators)!: name the row forms the conversions read and give

A table row has three forms: the instance, the plain form and the stored
form. The conversions typed the last two as `any` or open dictionaries, which
hid what every caller already relies on. They now name them:

- `PlainData<T>` is what `fromPlainData` reads: every field optional and
  possibly partial, a field under a container modifier possibly its value per
  key, the plain form 40 consumer calls already write.
- `PlainRow<T>` is what `toPlainData` gives: the set fields of `T` by name.
- `StoredRow<T>` is what `toDatabase` gives: typed `T` as `Table<T>` reads it,
  with `_internal` declared.
- `fromDatabase` takes the row as `Table<T>` types it, and gives it back with
  the class's prototype.
- `update(obj)` without an id takes `_id` in its type, as the docs said.
- `DatumStaticMetadata.primary` is the literal `"_id"`: nothing writes it.
- `unlockrequest` reads the stored form through `ValueProxy#cast` instead of
  an `any`; its signature keeps its answers.

`_internal.data`, `floating` and the modifier state stop being `any`: what they
hold is known to the modifiers that wrote them, so they read as `unknown`.

BREAKING CHANGE: `fromDatabase`, `fromPlainData`, `toDatabase`, `toPlainData`
and the matching `BasicDataModel` statics no longer accept or return `any`.
`triggerEvent` loses a type parameter nothing reads. `primary` no longer takes
another string.

* feat(database-decorators)!: keep reads of a stored row unchecked

`fromDatabase` and the model's static take what the database answered, as
before. A typed parameter measured +2 `no-unsafe-*` rows in interface-data-api
and three compile breaks, and it cleared nothing here: the two rows that stay
are the driver boundary, and an `as` in their place would only trade one
finding for another. `triggerEvent` keeps its type parameter for the same
reason: without it, that `any` copy is a new `no-unsafe-argument`.

`StoredRow<T>` is no longer `T`: a stored row has no prototype, so typing it as
the class made every spread of it a `no-misused-spread`. It now reads as the
set fields. The model's own writes keep the `T` they build.

* fix(modifiers): keep assigning a floating value by a plain assignment's rules

* chore(suppressions): lower the SAFETY budget to the comments left
… typed query operands (#70)

* build(deps): pin interface-database to the query value tarball

* feat(database-decorators)!: take a text field in unlockrequest

`unlockrequest` read the proxy of a field by `keyof T`, which includes
numbers and symbols. A query operand is a name or a number held in a
query value, never a symbol, so the field is a text key.

BREAKING CHANGE: `unlockrequest` takes `K extends keyof T & string`.

* chore(ratchet): leave the ceilings to the ceiling pass

* build(deps): pin interface-database to the structural operand tarball

* build(deps): pin interface-database to the IsQueryValue tarball

* build(deps): pin interface-database to the type-only tarball

* fix(database-decorators): keep unlockrequest's published key constraint

Narrow the key to text inside unlockrequest instead of tightening K, so interface-data-api's Validation.UnlockRequest<T, K extends keyof T> keeps compiling.
…what a decorator holds (#71)

* refactor(database-decorators): build on interface-core 0.4's typed decorator targets

The table modifier decorators get a DecoratorTarget, whose constructor is
a Function, so they attach through an internal attachModifierTo that
takes the class as a Function and the field as a property key; the
public attachModifier keeps its typed signature and delegates to it.
RegisterTable's handler receives a Class<Table> through MakeClassDecorator's
class parameter. unlockrequest takes any Constructible table, since it
only reads the table's modifier metadata, so a caller holding a
Class<object> no longer has to claim the row type. Building a modifier's
field entry moves to internal/records.

Pins interface-core to the 6a8b526 tarball (0.4.0).

* feat(database-decorators): let attachModifier take the class a decorator receives

attachModifier takes the table class as a Function and the field as a
property key, what it reads (the class's metadata, name and prototype),
so a property decorator can pass its DecoratorTarget's constructor and
key as they come, as the docs show. The internal attachModifierTo this
branch added is gone: the table modifiers call attachModifier again, and
the package gains no runtime export.
* chore(ratchet): delete the guardrail ratchet

* build(sync): drop the ratchet step
* docs(readme): use one Discord invite (#68)

* docs(github): send questions and ideas to Discord (#76)

* fix(fixtures): let two processes register a fixture on an empty table (#74)

* test(fixtures): cover two processes registering a fixture at once

* fix(fixtures): let two processes register a fixture on an empty table

* docs(fixtures): keep to what the fixture insert checks

* Merge commit from fork

* fix(model): keep modifier data on a partial update of another field

A modifier event creates an empty `_internal` on the instance. The update
writes that key as one value, so it erased what the row stored there.

* fix(hash): hash values with scrypt by default

`@Hashed()` now stores `$scrypt$ln=15,r=8,p=1$<salt>$<key>`. Rows in the
earlier digest format still verify, and `isHashOutdated` flags them so that a
caller can write them again after a successful test. `testHashAsync` runs the
test on the thread pool. An explicit `algorithm` keeps its digest format.

* fix(hash): raise the default scrypt cost to p=3

N=2^15, r=8, p=3 is the minimum OWASP lists for that N. It takes about 150 ms
and the same 32 MiB as p=1.

* test(hash): give suites that spend scrypt runs a longer limit

At p=3 a test with several runs passes mocha's 2 s limit on a busy machine.

* fix(hash): refuse a stored hash outside the cost bounds

A stored hash names its own cost, so a tampered row could ask for an expensive
run. Salt and key must take 16 to 64 bytes, the memory 128 MiB at most and p 16
at most. A row outside that, or with an N that r cannot hold, verifies nothing
and no longer throws.

* chore(release): v0.1.12

* docs(hash): mark the stored-hash upgrade snippet as a fragment

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
* docs: pass the database-decorators docs to the docs standard

* docs: turn on the docs standard

* docs: say when fromDatabase returns undefined and when a digest test runs scrypt
@gitguardian

gitguardian Bot commented Nov 3, 2026 •

Copy link
Copy Markdown

⚠️ GitGuardian has uncovered 4 secrets following the scan of your pull request.

Please consider investigating the findings and remediating the incidents. Failure to do so may lead to compromising the associated services or software components.

🔎 Detected hardcoded secrets in your pull request
GitGuardian id GitGuardian status Secret Commit Filename
39448498 Triggered Generic Password 4a959c7 src/tests/helpers/hash-rows.ts View secret
39448499 Triggered Generic Password 4a959c7 src/tests/helpers/hash-rows.ts View secret
39487054 Triggered Generic Password 98dac2d .agents/skills/verify-interface/scenarios/hashing.ts View secret
39532297 Triggered Generic Password da0a091 .agents/skills/verify-interface/scenarios/hashing.ts View secret
🛠 Guidelines to remediate hardcoded secrets
  1. Understand the implications of revoking this secret by investigating where it is used in your code.
  2. Replace and store your secrets safely. Learn here the best practices.
  3. Revoke and rotate these secrets.
  4. If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.

To avoid such incidents in the future consider


🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.

@Upd4ting
Upd4ting changed the base branch from campaign/integration to main November 5, 2026 05:44

This branch has not been deployed

No deployments
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