diff --git a/.agents/setup b/.agents/setup index eea855e..ae9def6 100755 --- a/.agents/setup +++ b/.agents/setup @@ -20,7 +20,12 @@ if [[ "$(node -p 'process.versions.node.split(".")[0]')" != "$major" ]]; then fi pin="$(node -p 'JSON.parse(require("node:fs").readFileSync("package.json", "utf8")).packageManager || "pnpm"')" manager="${pin%%@*}" -corepack enable +if ! corepack enable >/dev/null 2>&1; then + shims="$HOME/.local/bin" + mkdir -p "$shims" + corepack enable --install-directory "$shims" + export PATH="$shims:$PATH" +fi if [[ "$pin" == *@* ]]; then corepack prepare "$pin" --activate fi diff --git a/.agents/skills/verify-interface/SKILL.md b/.agents/skills/verify-interface/SKILL.md index 2185ad8..d4db715 100644 --- a/.agents/skills/verify-interface/SKILL.md +++ b/.agents/skills/verify-interface/SKILL.md @@ -63,14 +63,15 @@ Each step writes `/evidence////` with `command.t node_modules/.bin/antelopejs-verify clean ``` -Each journey deletes its scratch folder and `HOME`, its install included, when it ends: passed, failed or interrupted. Only its evidence stays. `clean` stops what the run started, by recorded PID and container name, and removes everything but `evidence/` and `run.json`, the folders `context.prepared` kept included. Run it after every run, failed ones included. +Each journey deletes its scratch folder and `HOME`, its install included, when it ends: passed, failed or interrupted. Only its evidence stays. `clean` stops what the run started, by recorded PID and container name, and removes everything but `evidence/` and `run.json`, the folders `context.prepared` kept included. Run it after every run, failed ones included. It cleans only a run this checkout created, the folder `new` printed: it refuses the shared runs folder, a repository's folder of runs and another checkout's run, which belong to other sessions. Never delete under the runs folder yourself, with `rm` or a glob over run names. ## The repository's own layer - `verify.config.ts` default-exports a `VerifyConfig` from `@antelopejs/tooling-configs/verify`: - `doors`: one entry per door kind (`data`, `format`, `contract`, `outbound`, `exposure`, `publish`), each `null` when it cannot apply or `{ globs, funnels, why }`. - - `journeys`: the repository's journeys by id, each `{ title, entryPoint, run(context) }`, using the check helpers the same import exports. A check states what the product must do; an environment that went away is never a failing check. When a journey fails, the harness reruns each service's readiness command, up to four times over about 3.5 s, and looks at every `ajs project dev` the journey started, its own readiness probe included. The journey ends NOT VERIFIED, not FAIL, when a service still fails every rerun, an app was killed by SIGKILL, SIGTERM, SIGHUP or SIGINT that the harness did not send, or the app the harness started failed to bind the port the harness gave it (`EADDRINUSE` naming that port). An app that exits on its own with an exit code, one that crashes by another signal such as SIGABRT or SIGSEGV, and one that logs `EADDRINUSE` for any other port are the product's failure. A change that takes its own service down still reads as an outage: the service stays down, so the journey is NOT VERIFIED. For anything else the journey depends on, such as a process it started with `context.command` or an outside endpoint, probe it when a check fails and call `context.notVerified(reason)` when it no longer answers. - - `context.prepared(key, build)` does the preparation many journeys repeat once per run, such as scaffolding and installing a project, and returns the journey's own copy at `/`. The first journey that asks for `key` runs `build(folder)` on the empty folder with its usual steps, after a step `prepared ` that marks them as the build and says whether it was kept. A build is kept until `clean` only when it returns, records at least one check, passes every check, and leaves only files, directories and symlinks in the folder. Every later journey of the run, also in a later `journey` command, gets a fresh copy and one step, `prepared `, that names the journey that built it and holds that build's evidence under `build/`. A build that is not kept is built again by the next journey, and rerunning the journey that built builds again. An absolute symlink into the folder points into each copy; other symlinks keep their target. `build` writes only inside the folder it is given: a write elsewhere, such as a sibling under `` or the repository, stays with the journey that built. Two concurrent `journey` commands that reach the same key at once both build, and the first to finish is kept. The commands `build` runs get a `HOME` of their own that is deleted when it ends: redo after `prepared` anything a later step needs in `HOME`, such as `ajs config set git`. Pass the same `build` for a key in every journey. + - `journeys`: the repository's journeys by id, each `{ title, entryPoint, run(context) }`, using the check helpers the same import exports. A check states what the product must do; an environment that went away is never a failing check. When a journey fails, the harness reruns each service's readiness command, up to four times over about 3.5 s, and looks at every `ajs project dev` the journey started, its own readiness probe included. The journey ends NOT VERIFIED, not FAIL, when a service still fails every rerun, an app was killed by SIGKILL, SIGTERM, SIGHUP or SIGINT that the harness did not send, or the app the harness started failed to bind the port the harness gave it (`EADDRINUSE` naming that port), or the first failing step is an install, image pull or image build whose failed requests went only to fixed public registry names that also fail a fresh DNS and TCP probe at verdict time. A registry that answers that probe makes the failure the product's, whatever the output says. An app that exits on its own with an exit code, one that crashes by another signal such as SIGABRT or SIGSEGV, and one that logs `EADDRINUSE` for any other port are the product's failure. A change that takes its own service down still reads as an outage: the service stays down, so the journey is NOT VERIFIED. For anything else the journey depends on, such as a process it started with `context.command` or an outside endpoint, probe it when a check fails and call `context.notVerified(reason)` when it no longer answers. + + - `context.prepared(key, build)` does the preparation many journeys repeat once per run, such as scaffolding and installing a project, and returns the journey's own copy at `/`. The first journey that asks for `key` runs `build(folder)` on the empty folder with its usual steps, after a step `prepared ` that marks them as the build and says whether it was kept. A build is kept until `clean` only when it returns, records at least one check, passes every check, and leaves only files, directories and symlinks in the folder. Every later journey of the run, also in a later `journey` command, gets a fresh copy and one step, `prepared `, that names the journey that built it and holds that build's evidence under `build/`. A build that is not kept is built again by the next journey, and rerunning the journey that built builds again. An absolute symlink into the folder points into each copy; other symlinks keep their target. A file with a hard link outside the folder, such as a package file pnpm links from its store, is linked into each copy rather than copied: `context.edit` gives it a new inode, and a journey that writes it another way replaces it instead of writing it in place. `build` writes only inside the folder it is given: a write elsewhere, such as a sibling under `` or the repository, stays with the journey that built. Two concurrent `journey` commands that reach the same key at once both build, and the first to finish is kept. A project `build` creates with `scaffold` starts each copy as `scaffold` starts one: a copy with no installed module gets a module cache an earlier passing journey kept, and a passing journey keeps the registry modules installed into its copy. The commands `build` runs get a `HOME` of their own that is deleted when it ends: redo after `prepared` anything a later step needs in `HOME`, such as `ajs config set git`. Pass the same `build` for a key in every journey. - `env`, `setup` (commands replacing the build), `userFacing` and `shared` (globs replacing the kind's default; `shared` lists the files whose change runs every feature), and `formal` (formal checks: `name`, `covers`, `command`, `violation` with an optional `states` group for the counterexample length, and optionally `behaviour` and `baseCommand`). - `services`: image, port, readiness command and a `reset` command run before every journey, as for a module. `new` starts them in Docker on free ports; a journey reads them with `context.service(name)` and `context.native(...)`. - `interface.provide(api, services)` runs in the stub provider after the generic stubs attach, for example to hand a client pointed at `services` to `internal.SetClient`. diff --git a/AGENTS.md b/AGENTS.md index 042ce3d..a03545b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -88,6 +88,7 @@ Verify a boundary against the imports a developer or agent would plausibly write | String-keyed object tables are read through own keys only | `antelopejs/no-inherited-key-lookup` | | A type guard checks each member it narrows to; keys with `Object.hasOwn` | `antelopejs/no-presence-only-type-guard` | | A type guard checks the nested elements and values it claims | review; no tool | +| `Object.assign` writes no `unknown`, `any` or mistyped value over a field | `antelopejs/no-unsafe-object-assign` | | Every file type-aware lint reads is listed by a tsconfig | `antelopejs/require-tsconfig-include` | | Params ≤ 5, function ≤ 120 lines, file ≤ 500 lines, depth ≤ 4, complexity ≤ 20 | `eslint/max-*`, `eslint/complexity` | | Cleanup preserves declarations, runtime exports and DMS auto-imports | `antelopejs-api-diff ` | diff --git a/README.md b/README.md index 1f208b2..e923d31 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,7 @@ The package declares this `@antelopejs/*` peer dependency: | Package | Supported | Tested with | | ---------------------------- | ---------------- | ----------- | -| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `0.2.4` | +| `@antelopejs/interface-core` | `>=0.2.4 <1.0.0` | `0.4.0` | diff --git a/package.json b/package.json index 7bd6e87..d51fbae 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "lint:fix": "oxlint --fix", "lint": "oxlint", "prepack": "pnpm run build", - "release": "pnpm run lint && pnpm run prepack && release-it", + "release": "pnpm run lint && pnpm run typecheck && pnpm run prepack && release-it", "test": "pnpm run build && ajs module test .", "test:staged-query": "pnpm run build && node scripts/test-staged-query.cjs", "format:check": "oxfmt --check .", @@ -127,7 +127,7 @@ "@antelopejs/core": "^1.21.0", "@antelopejs/interface-core": ">=0.2.4 <1.0.0", "@antelopejs/mongodb": "1.6.0", - "@antelopejs/tooling-configs": "^0.4.0", + "@antelopejs/tooling-configs": "^0.5.0", "@types/chai": "^4.3.20", "@types/mocha": "^10.0.10", "@types/node": "^22.19.15", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5786f57..4f2e9e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,13 +16,13 @@ importers: version: 1.21.0 '@antelopejs/interface-core': specifier: '>=0.2.4 <1.0.0' - version: 0.2.4 + version: 0.4.0 '@antelopejs/mongodb': specifier: 1.6.0 version: 1.6.0(socks@2.8.7) '@antelopejs/tooling-configs': - specifier: ^0.4.0 - version: 0.4.0(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001)) + specifier: ^0.5.0 + version: 0.5.0(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001)) '@types/chai': specifier: ^4.3.20 version: 4.3.20 @@ -85,9 +85,6 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - '@antelopejs/interface-core@0.2.4': - resolution: {integrity: sha512-2qF6/KwahCQvvNuC/piUvPWD4wq2Npmco941HMg2/bMBoJKbhMH16WcXXRvPYznNSkU0U+6vqK6KE+2N5NojSA==} - '@antelopejs/interface-core@0.4.0': resolution: {integrity: sha512-IPX6dtt3KHknclN4mLOYeRYL2Odz1Pts+dtaZul62NghsBUX/0OFHD4Qhoaf+Beva4hjInbaF918ajr4Z5HVWQ==} @@ -102,8 +99,8 @@ packages: '@antelopejs/mongodb@1.6.0': resolution: {integrity: sha512-HKPw1FC+NDqHe0B7ceEUGpbH8oOg9AcdEhcBjSnBwrmf0Z0hWwn2T95p0hEcW3TunaFRoueAJsQjSdUqdckSFA==} - '@antelopejs/tooling-configs@0.4.0': - resolution: {integrity: sha512-qmi9CD5OZkWgIFGYZTQ63eyFBMEqGfY8967smEKyQVM0WW1n7kybqeG0ycB0wP+zgT4/LHehF6h7byLvTMBE1A==} + '@antelopejs/tooling-configs@0.5.0': + resolution: {integrity: sha512-vTZyx0FpSzArjSdb119i+CTGPsYe903/D/JY+0KVUgBjdw/2+LTRt58NUinDtvnf9H18uAlF2xwWJXsSPTv/6A==} engines: {node: '>=22.18.0'} hasBin: true peerDependencies: @@ -2788,17 +2785,13 @@ snapshots: semver: 7.7.4 typescript: 5.9.3 - '@antelopejs/interface-core@0.2.4': - dependencies: - reflect-metadata: 0.2.2 - '@antelopejs/interface-core@0.4.0': dependencies: reflect-metadata: 0.2.2 - '@antelopejs/interface-database-catalog@0.0.3(@antelopejs/interface-core@0.2.4)': + '@antelopejs/interface-database-catalog@0.0.3(@antelopejs/interface-core@0.4.0)': dependencies: - '@antelopejs/interface-core': 0.2.4 + '@antelopejs/interface-core': 0.4.0 '@antelopejs/interface-mongodb@0.0.6(socks@2.8.7)': dependencies: @@ -2815,9 +2808,9 @@ snapshots: '@antelopejs/mongodb@1.6.0(socks@2.8.7)': dependencies: - '@antelopejs/interface-core': 0.2.4 + '@antelopejs/interface-core': 0.4.0 '@antelopejs/interface-database': 'link:' - '@antelopejs/interface-database-catalog': 0.0.3(@antelopejs/interface-core@0.2.4) + '@antelopejs/interface-database-catalog': 0.0.3(@antelopejs/interface-core@0.4.0) chai: 4.5.0 mongodb: 6.21.0(socks@2.8.7) randomstring: 1.3.2 @@ -2834,7 +2827,7 @@ snapshots: - snappy - socks - '@antelopejs/tooling-configs@0.4.0(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))': + '@antelopejs/tooling-configs@0.5.0(eslint-plugin-perfectionist@5.11.0(eslint@10.10.0(jiti@2.7.0))(typescript@5.9.3))(knip@6.34.0)(oxfmt@0.66.0)(oxlint@1.81.0(oxlint-tsgolint@7.0.2001))': dependencies: '@oxlint/plugins': 1.81.0 dts-bundle-generator: 9.5.1 diff --git a/scripts/test-staged-query.cjs b/scripts/test-staged-query.cjs index e799290..19798ad 100644 --- a/scripts/test-staged-query.cjs +++ b/scripts/test-staged-query.cjs @@ -101,10 +101,10 @@ assert.equal(executableTable instanceof executable.Table, true); assert.equal(executableTable instanceof executable.Selection, true); assert.equal(executableTable instanceof executable.Stream, true); assert.equal(executableTable instanceof executable.Query, true); -assert.ok(executableTable.run instanceof Function); -assert.ok(executableTable.atomicMutation instanceof Function); +assert.equal("run" in executableTable, true); +assert.equal("atomicMutation" in executableTable, true); assert.equal(executableInsert instanceof executable.Query, true); -assert.ok(executableInsert.run instanceof Function); +assert.equal("run" in executableInsert, true); const originalRun = Object.getOwnPropertyDescriptor( executable.Query.prototype, diff --git a/src/tests/atomic_mutation.test.ts b/src/tests/atomic_mutation.test.ts index 7fa6ec7..bead285 100644 --- a/src/tests/atomic_mutation.test.ts +++ b/src/tests/atomic_mutation.test.ts @@ -274,7 +274,10 @@ async function ValidateTerminal() { { args: ["record-a"] }, ]) { const query = table.atomicMutation("record-a", request); - Object.assign(query.build().at(-1)!, terminal); + const stage: { stage: string; options?: unknown; args: unknown[] } = query + .build() + .at(-1)!; + Object.assign(stage, terminal); await rejectionOf(query.run(), TypeError); } }