Skip to content

build(rest): give the package a test-layer tsc program and ledger its 37 errors - #12570

Merged
os-litant merged 1 commit into
mainfrom
claude/issue-12542-rest-test-typecheck-program
Aug 26, 2026
Merged

os-litant merged 1 commit into
mainfrom
claude/issue-12542-rest-test-typecheck-program

Conversation

@os-litant

Copy link
Copy Markdown
Collaborator

Fixes #12542

packages/rest/tsconfig.json excludes **/*.spec.ts and **/*.test.ts, and the
package's typecheck script was tsc --noEmit against that config and nothing
else. So no tsc program compiled a single test file in the package — the
shape AGENTS.md names twice ("never exclude *.test.ts", "a @ts-expect-error
in a file no tsc program compiles is a phantom check"), sitting live in one of
the repo's largest packages.

Premise, re-derived rather than inherited

At 5fbd58e0d (my branch base; the card measured at 52a982388):

claim measured
include / exclude ["src/**/*"] / ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
typecheck script tsc --noEmit, that program and nothing else
test files 149 (git ls-tree -r --name-only HEAD -- packages/rest | grep -cE '\.test\.ts$')
.spec.ts files 0 — the **/*.spec.ts half of the exclusion covers nothing
all under src/? yes, 149/149 — nothing hidden by an unreachable include
tsconfig.test.json / test-typecheck-debt.json absent
check:test-typecheck wired no

The card's numbers hold exactly, at both shas. Only the .spec.ts line is an
addition: the exclusion's second glob was already inert.

What landed — an adoption, not an invention

packages/rest/tsconfig.test.json follows packages/client's and
packages/spec's landed shape: module semantics only, plus lib, with
strictness inherited and untouched. It is named by the typecheck script
through the shared scripts/check-test-typecheck.mts gate, because a config no
script invokes is exactly the phantom this change is about:

"typecheck": "tsc --noEmit && pnpm check:test-typecheck"

tsx joins devDependencies for the same reason packages/client and
packages/spec carry it — the shared gate runs through it.

What the program INHERITED, and did not re-declare

packages/rest/tsconfig.json already carries two #9960 settings, both
load-bearing, and this file deliberately re-declares neither:

  • rootDir: ".." — already widened to packages/ there, as a consequence
    of the paths rule rather than a preference. So the TS6059 pile that forced
    packages/client's sibling config to widen rootDir does not arise here; it
    was paid for already.
  • paths: { "@objectstack/metadata-protocol": [".../src/index.ts"] } — a
    child that declared its own paths would replace this map rather than
    merge into it, silently sending that specifier back to dist/. This file
    declares no paths at all, so the rule stands and 22 of the producer's source
    files are in the test program exactly as they are in the build one.

The number nobody had (ruling 5), measured at 5fbd58e0d

37 errors across 13 files, with the workspace closure built first
(pnpm --filter '@objectstack/rest^...' build, exit 0).

code count note
TS2554 14 wrong arity, all fourteen "Expected 2-5 arguments, but got 1"
TS18048 13 "possibly undefined", all in src/export-integration.test.ts
TS2345 5
TS7006 4 implicit any
TS6133 1 unused local

Neither stop-and-report fork fires: 37 is above the "small, client-scale"
threshold and nowhere near "enormous", and it is not one producer-side defect
wearing many files' clothes — five codes across 13 files, the largest single
file holding 17.

Ruling 4's question, answered for all 37: would this error exist on
origin/main if the program had always been there? Yes, for every one.
No
test file is edited by this PR; the program differs from the build config only
in module semantics and lib, and each of the 37 sits on a line that predates
this branch. None is caused by the change, so all 37 are ledger entries in
packages/rest/test-typecheck-debt.json — EXACT and shrink-only.

Why the 155 in TEST_DEBT became 37

Not a rescale — the old entry predicted it. 121 of its 155 were TS2835 plus the
implicit-any pile TS2835 causes, and 16 more were one Array.prototype.at
message against a lib older than es2022. This package is "type": "module",
so NodeNext was compiling extensionless relative imports as unresolvable ESM and
every symbol they named became any. Under vitest-matching semantics TS2835
x72 goes to 0, TS2550 x16 to 0, TS7006 x49 to 4. What is left is a different
shape, and it grew in one place while collapsing in four: TS18048 x13 is a class
the 155 never contained at all — "possibly undefined" reads that only become
visible once the imports above them resolve to real types. Sizing this off the
TS2835 line alone would have said "155 − 121 = 34" and been wrong in both
directions. That is the #8612 lesson, measured a second time.

No test file is edited (ruling 3), and the #12421 coupling

Zero test files are touched: the deliverable is the program plus the measured
ledger. PR #12421 holds packages/rest/src/rest.test.ts, which carries 4 of
the 37 and therefore has a ledger entry — which collides with nothing, where an
edit would have been a serial breach.

⚠️ The two interact by design, and the next reader needs to know it. The
ledger is EXACT and shrink-only, so once #12421 merges: an error it adds to
rest.test.ts reds the ledger on its run ("the debt GREW"); an error it
removes reds the entry as stale until re-recorded ("the debt SHRANK"). Both
are the pin working, and both land on the change that caused them.

Both @ts-expect-error directives in this layer — in src/rest.test.ts and
src/rest-api-plugin-slot-lookups.test.ts — were phantom checks that evaluated
never. Under this program neither reports TS2578, so both are live and each is
suppressing a real error.

Anti-vacuity: the program can say no (ruling 6)

Membership. tsc -p tsconfig.test.json --listFiles puts 149 of
packages/rest/src/**/*.test.ts in the program — the exact census — out of 489
files total.

Ablation. A real type error planted in src/analytics-routes.test.ts, an
unledgered file, so the everyday verdict is what gets exercised:

HEAD blob hash             : d533503f6e47ac65d36be9066dcded31dc48aca8
mutated disk hash          : 7a92cb594097f9f902f1a303ca83f637e7238a8a
injected marker occurrences: 1
MUTATION CONFIRMED ON DISK
GATE_EXIT=1
  - src/analytics-routes.test.ts: 2 type error(s) in a file the ledger does not cover.
post-restore disk hash     : d533503f6e47ac65d36be9066dcded31dc48aca8
RESTORE PROVEN: disk hash == HEAD blob hash, and `git diff HEAD` is empty for the path

The gate exits non-zero and names the file. The restore ran from a trap on
EXIT/INT/TERM against absolute paths, used git checkout HEAD -- path
(never the bare form, which restores from a polluted index), and is proven by
blob hash — never by an exit code. It reports 2 errors, not 1: the second is
noUnusedLocals on the planted binding, which is itself evidence that inherited
strictness is live. No build or dist/ is involved on either leg — tsc reads
these sources directly — so there is no artifact staleness to preflight.

Two mechanical consequences, both required by gates rather than chosen

1. @objectstack/rest graduates out of TEST_DEBT. Once the typecheck
script names the sibling config, check:type-check-coverage reports
has a TEST_DEBT entry but ... no longer hides its tests -- it graduated. The
entry is deleted and a graduation note recorded in its place, per the file's own
convention. Headline moves: 19 → 18 packages hiding tests, 1110 → 965
hidden files, 1461 → 1306 frozen errors — exactly −1 package, −149 files,
−155 errors.

2. check:type-source-resolution sees a new program, and this is the one
judgement call in the PR — flagged rather than buried.
Since #11490 that gate's
population is every tsconfig*.json a typecheck script names, so the new
program joins it and reports six workspace deps the build program never reached:
driver-sql, metadata, plugin-hono-server, plugin-security,
service-analytics, service-datasource — all six via tsconfig.test.json.

I recorded them in that package's registry entry, with the program-set numbers
stated in place as the registry's own rule requires: before, at 5fbd58e0d,
--list reported 93 programs / 77 packages / 54 entries / 233 pairs; after,
94 / 77 / 54 / 239. +1 program, +0 entries, +6 pairs.

The gate's failure text asks for paths rules instead and calls registry
widening "not the fix", so here is why I did not do that — measured both ways
on the same checkout
, not argued:

  • paths redirecting the six to source takes the test layer from 37 errors to
    42
    , and the +5 are TS6133 in
    ../plugins/plugin-hono-server/src/{hono-plugin,current-user-endpoints}.ts and
    ../drivers/driver-sql/src/sql-driver.tsother packages' source billed to
    packages/rest/test-typecheck-debt.json
    , where they would then go red on
    those packages' PRs. Worse, those five are not even real: both packages run
    pnpm --filter ... typecheck green on this same tree (exit 0, measured), so
    the borrowed program MANUFACTURES diagnostics that belong to nobody. A ledger
    seeded with those cannot be paid down by the package that owns the file.
  • It would also make the type program diverge from the runtime one:
    packages/rest/vitest.config.ts aliases exactly two of the six
    (plugin-hono-server, service-datasource) to source and resolves the other
    four through dist/. Blanket paths is not fidelity to vitest either.
  • The landed precedent for this exact shape is the registry entry, twice:
    @objectstack/client and @objectstack/trigger-record-change both carry
    test-program deps there, the latter being the package that took this very
    #5286 sibling route.
  • Ruling 1 is "module semantics ONLY". A six-entry paths block is not that.

⚠️ The tension is real and I am naming it, not resolving it unilaterally: the
registry's doc-block permits a widening when the set of programs changed
which is literally true here, 93 to 94 — but adds "that is a change to this file,
not to a package", written when the gate read only tsconfig.json and a package
could not move the population. This PR is the first case where a package does.
Happy to switch to paths if a maintainer reads that sentence as binding.

Verification

All runs below are at 2dfb401c1, the pushed head, taken after the final
commit. Every exit code captured before any pipe (redirect, then $?).

  • pnpm --filter @objectstack/rest typecheck — exit 0. Gate's own verdict line:
    check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json; 13 file(s) / 37 error(s) held in test-typecheck-debt.json (shrink-only).
  • 25 gate families from node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack against the real change set, all exit 0
    including check:type-check-coverage, check:type-source-resolution,
    check:published-files, check:test-source-alias, check:nul-bytes,
    check:pm-dispatch-gates and scripts/pm/bare-root-worklist.mjs --self-test
    (the last two are the convention-triggered pair this diff incurs by editing
    gate scripts).
  • pnpm lint (eslint . --no-inline-config, the whole repo, not narrowed) —
    exit 0, VERDICT command-exit 0.
  • Heavy runs serialised through scripts/pm/os-verify-lock.sh; every one ended
    VERDICT command-exit 0.

One declared narrowing: pnpm check:type-check-debt was NOT run. It is
check-type-check-coverage.mjs --re-measure, and it refuses on this worktree —
--re-measure cannot run: 32 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. That is a PREREQUISITE NOT MET, not a
red gate: it declines to measure rather than measuring a different world. Its
prerequisite is the full workspace build that lint.yml runs before the step,
which CI does on this PR regardless. Three things make the omission a
measurement rather than a gap: --re-measure scores each ledgered package under
its own tsconfig, and @objectstack/rest is in neither ledger after this
diff, so it is not among the projects measured; outside packages/rest this diff
changes only two gate scripts' data and prose — no package's tsconfig and no
package's source; and the lockfile delta is exactly three lines in
packages/rest's own importer, adding an already-resolved tsx@4.23.12, so no
other package's closure moved. The structural half of the same gate —
check:type-check-coverage, which owns TESTS_COVERED, PINS_CHECKED, RECONCILED
and the composition invariants, and is what this diff actually moves — ran green
above.

Changeset: deliberately none, and the rule applied

packages/rest's files is ["dist", "README.md", "CHANGELOG.md"]. This PR
changes no src/ file, so dist/ is byte-unaffected; tsconfig.test.json and
test-typecheck-debt.json are not published; and tsx is a devDependency no
consumer installs. Nothing is released, which is the skip-changeset label's
own definition in pr-automation.yml. The in-tree precedent for this exact
change shape is 951b025e4
build(trigger-record-change): graduate the package out of the TEST_DEBT ledger
(PR #11489), package.json + tsconfig.test.json + the coverage gate, no
changeset.


Generated by Claude Code

… 37 errors

`packages/rest/tsconfig.json` excluded `**/*.test.ts` and `typecheck` was
`tsc --noEmit` against that config, so no tsc program compiled any of the
package's 149 test files and both `@ts-expect-error` directives in that layer
were phantom checks.

Adopts the #5286 sibling route: `tsconfig.test.json` with vitest-matching
module semantics and untouched strictness, named by the `typecheck` script
through the shared `check-test-typecheck.mts` gate. No test file is edited;
the 37 errors the program finds (13 files) are recorded per file in
`test-typecheck-debt.json`, EXACT and shrink-only.

Two mechanical consequences, both required by gates rather than chosen:
`@objectstack/rest` graduates out of TEST_DEBT (155 raw, re-measured 37), and
its `check:type-source-resolution` registry entry gains the six workspace deps
the new program reaches, with the program-set numbers stated in place.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UjujZN219uFzBhSYfMykCd
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

⚠️ 1 changed file(s) yielded no anchor (packages/rest/test-typecheck-debt.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/rest/test-typecheck-debt.json) — pages documenting those are invisible to this run
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1b7e3d2ce20cca14357581b7697743fa67f1ee7epackageMentionDocs.

@github-actions github-actions Bot added the dependencies Pull requests that update a dependency file label Aug 26, 2026
@os-litant os-litant added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Aug 26, 2026 — with Claude
@os-litant
os-litant marked this pull request as ready for review August 26, 2026 11:25
@os-litant
os-litant added this pull request to the merge queue Aug 26, 2026
Merged via the queue into main with commit 3f41a21 Aug 26, 2026
38 checks passed
@os-litant
os-litant deleted the claude/issue-12542-rest-test-typecheck-program branch August 26, 2026 11:50
baozhoutao pushed a commit that referenced this pull request Sep 2, 2026
…sc program

check:type-check-coverage's isUncheckedSourceCandidate skipped depth === 0
(the package root) unconditionally, so a package-root .ts file was invisible
to SOURCES_COVERED regardless of content. This is why #13284's driver-memory
/ plugin-hono-server manifests went unchecked for as long as they did.

Per the triage on #14386 (comment 5504408509), this admits depth === 0 only
for a declared, exact-name allowlist (ROOT_SOURCE_FILES, currently just
objectstack.config.ts) rather than every package-root file -- the wider
104-file question stays explicitly unresolved. The uncheckedByDir
aggregation now keys a root-level file at '.' (which posix.join collapses
to the package's own directory) instead of the rel.slice(0, -1) garbage key
the old indexOf('/') === -1 arithmetic produced.

The three sites the widened predicate then surfaces (plugin-auth,
plugin-security, service-i18n) are put into a program: widened include on
the existing sibling noEmit programs for the first two, a new sibling
tsconfig.typecheck.json (following the driver-memory shape from #13284) for
service-i18n, which had none to widen.

Putting service-i18n's manifest into a program onboards that package's
first tsc program to reach the bare @objectstack/spec specifier (src/ only
ever imports subpaths), which check:type-source-resolution correctly
flagged. Repaired via that gate's own documented onboarding-limb registry
re-baseline (the sanctioned tool for a dep reached only through a
newly-onboarded program -- paths is measured wrong for this shape on
PR #12570), with --list before/after numbers stated in place.

Fixes #14386
os-sales pushed a commit that referenced this pull request Sep 3, 2026
… for the onboarded test program

Onboarding a `tsconfig.test.json` moves this package's tsc PROGRAM SET, which
`check:type-source-resolution` judges per program. `service-cluster` had NO
`typecheck` script before, so it ran zero counted programs; both deps the gate
now reports are reached only through the program this change added.

Taken on the onboarding limb the registry's own doc-block opens, on its three
stated terms. Provenance measured four ways by varying only what `typecheck`
names: absent with no script, absent naming `tsconfig.json` alone, PRESENT
naming `tsconfig.test.json`. The build program carries no dist-resolved
workspace type import at all, so the exposure is only reachable through the
onboarded program rather than merely first seen there.

`--list` before 57/78 packages, 118 programs, 288 pairs; after 58/78, 119, 290.
+1 package, +1 program, +2 pairs -- this entry and nothing else.

`paths` was measured rather than argued and is the wrong tool here: redirecting
the two deps to source takes the test layer from 0 errors to 435, all TS6059
and all in `packages/spec/src` and `packages/core/src` -- another package's
diagnostics billed to one that cannot pay them down (PR #12570, #8021).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
hotlong pushed a commit that referenced this pull request Sep 3, 2026
…st tsc program (#15004)

`check:type-source-resolution` went red on the pin: `@objectstack/runtime`,
`@objectstack/objectql` and `@objectstack/plugin-security` were newly reached
by `tsconfig.test.json` and resolved through each package's `exports` map to
`dist/index.d.ts` — a build artifact. For a pin whose whole subject is the
readers the reader program is about to CHANGE, a type verdict about the last
`pnpm build` is the vacuous-green direction that gate exists to refuse.

Fixed the way the gate prescribes for a dep exposed through an EXISTING
program: `paths` rules, never a widening of its shrink-only registry. Three
star-less (exact-match) rules, so `@objectstack/objectql/core` keeps resolving
through the package's own `exports` map untouched.

Measured, because the registry's own notes record `paths` billing other
packages' diagnostics into the consumer's ledger (PR #12570): with the
workspace closure built, `pnpm --filter @objectstack/cli check:test-typecheck`
reports 3 files / 28 errors / 6 pinned signatures — byte for byte the
pre-existing `test-typecheck-debt.json`. Zero new diagnostics.

Also corrects the pin header's reverse-verification count to the measured 24
and records the `effect: 'writes'` -> `effect: 'pure'` row, a loss no
presence-check would have found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
os-warren pushed a commit that referenced this pull request Sep 5, 2026
The sibling gate `check:type-source-resolution` is red on the same specifier,
and it is red at the PR head `b65fafc81` as well -- measured with both files of
the alias fix-up reverted in place under a trap, then restored byte-identical.
It was invisible only because `Lint & Repo Gates` halts at step 130
(`Test-source alias gate`) and skips 131-148; this gate is step 132, so
unblocking 130 walks the job straight into it.

Same defect class, same specifier, same round-1 import (`celEngine`), and the
remedy the gate dictates: ONE `paths` rule for the bare name in the package's
`tsconfig.json`, copied in shape from the `@objectstack/service-messaging` rule
already beside it. `@objectstack/formula` publishes no subpath in its `exports`,
so a subpath rule would match nothing on disk and fall back to node resolution.
`tsconfig.test.json` inherits this map rather than declaring its own, which is
what carries the rule to the test layer that reaches the specifier.

Measured rather than assumed, because #12570 records this exact rule billing
another package's source diagnostics into this ledger:

- `check:type-source-resolution` 0 -- "125 tsc program(s) across 78 packages
  scanned; 61 registered as still resolving a workspace dep's types through
  `dist/`".
- `pnpm --filter @objectstack/plugin-auth typecheck` 0, with
  `check:test-typecheck` reporting the ledger UNCHANGED at 10 file(s) /
  94 error(s) / 23 pinned signature(s) -- nothing was added to it.
- `tsc -p tsconfig.test.json --listFiles`: 15 `packages/formula/src/*.ts` files
  now in the program, 0 files under `packages/formula/dist/`, program 1243 ->
  1257 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y
baozhoutao pushed a commit that referenced this pull request Sep 6, 2026
… the closure at the point of refusal

The refusal names `paths` first. That remedy has a precondition it never
stated: the consuming program's `rootDir` has to contain the dependency's
source. Where it does not, tsc admits the dep's file graph and then reports
TS6059 for all of it, billed to the consuming package's test-typecheck ledger
— the PR #12570 shape, reached from the `paths` limb instead of the
re-baseline one. Both the ledger and `rootDir` are maintainer-only, so at that
point the author has no self-serve remedy and the message named none.

The knowledge was already in this file — the header's with-`paths` red and the
PR #12570 reading over the registry — and nowhere in the text an author reads.

Written as a CONDITION, never a blanket closure: `paths` stays correct wherever
the program's `rootDir` does contain the source, and a message overstating the
closure would be worse than the current silence.

The refusing sentence, the ⛔ SHRINK-ONLY testimony and the ONBOARDED exception
are untouched, so `check-ratchet-remedy-authority` still classifies this gate as
`refused`. Two self-test pins added for the new halves and the battery floor
raised 10 -> 12 to match.

Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
… TS2322 it hid (objectstack-ai#15032)

* fix(service-cluster): compile the test layer with tsc, repair the TS2322 it hid

`packages/services/service-cluster` had no `typecheck` script at all, so no tsc
program read the package: turbo/CI typecheck lanes skipped it silently (a
zero-matching filter run exits 0), while tsup and vitest both type-STRIP. Its
`tsconfig.json` does include the tests and always did — the program existed and
was never invoked.

That hid a TS2322 in `src/memory/memory.contract.test.ts`, the package's
contract witness: a concise arrow body passed as a `PubSubHandler` returns
`Array.prototype.push`'s `number` where the contract declares
`void | Promise<void>`, and the void-return relaxation does not forgive a UNION
target. Fixed with a block body — the handler is side-effect-only by contract.
The spec contract is untouched.

Wired by the route objectstack-ai#14062 settled for `packages/plugins/**`: a sibling
`tsconfig.test.json` changing module semantics only (strictness inherited,
untouched), named by a new `typecheck` script through `check:test-typecheck`.
Measured 1/1 errors before (build semantics / new config — they agree, so no
config-tier pile), 0/0 after, over a 410-file program covering all 7 test files.

No `test-typecheck-debt.json` is added; its absence is the zero. The package's
`DEBT` entry in `scripts/check-type-check-coverage.mjs` is deleted rather than
lowered, which is the graduation that gate's own invariant requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8

* fix(service-cluster): re-baseline the type-source-resolution registry for the onboarded test program

Onboarding a `tsconfig.test.json` moves this package's tsc PROGRAM SET, which
`check:type-source-resolution` judges per program. `service-cluster` had NO
`typecheck` script before, so it ran zero counted programs; both deps the gate
now reports are reached only through the program this change added.

Taken on the onboarding limb the registry's own doc-block opens, on its three
stated terms. Provenance measured four ways by varying only what `typecheck`
names: absent with no script, absent naming `tsconfig.json` alone, PRESENT
naming `tsconfig.test.json`. The build program carries no dist-resolved
workspace type import at all, so the exposure is only reachable through the
onboarded program rather than merely first seen there.

`--list` before 57/78 packages, 118 programs, 288 pairs; after 58/78, 119, 290.
+1 package, +1 program, +2 pairs -- this entry and nothing else.

`paths` was measured rather than argued and is the wrong tool here: redirecting
the two deps to source takes the test layer from 0 errors to 435, all TS6059
and all in `packages/spec/src` and `packages/core/src` -- another package's
diagnostics billed to one that cannot pay them down (PR objectstack-ai#12570, objectstack-ai#8021).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
… through every entry path with the flattened collections absent (objectstack-ai#15039)

* test(cli): the option-B acceptance pin — a two-package fixture booted through every entry path (objectstack-ai#15004)

Reader program 1/4 of the ADR-0130 D4 option-B ruling (objectstack-ai#14512 comment
5528589044). Option B removes the flattened top-level collections from a
multi-package artifact so `packages[]` carries each definition once; the
ruled order is readers first, emitter last, and the failure mode that order
exists to contain is a reader nobody enumerated — silent, because the
collection is simply absent and nothing throws.

This commit adds the pin that makes that loud, and nothing else: three new
test files, zero production files.

- `test/fixtures/option-b-collection-zoo.ts` — two ordinary `defineStack`
  packages carrying one member of every collection family, composed with
  `manifest: 'preserve'`, in both shapes. The key set separating the shapes
  is DERIVED from `ObjectStackDefinitionSchema` ∩ `AssembledPackageBodySchema`
  rather than transcribed, so a collection family added later joins the probe
  automatically.
- `test/fixtures/option-b-reader-probe.ts` — the instrument. Every row calls
  a reader this repo ships, or boots a real kernel carrying the real
  `AppPlugin` and reports what that plugin handed to a subsystem. No row
  reads `bundle.<collection>` and calls it a measurement.
- `test/option-b-reader-acceptance.pin.test.ts` — the pin. `OPTION_B_LOSSES`
  records exactly which subsystems lose their collection today; the assertion
  is set equality, shrink-only, audited in both directions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m

* test(cli): resolve the pin's three workspace deps to SOURCE in the test tsc program (objectstack-ai#15004)

`check:type-source-resolution` went red on the pin: `@objectstack/runtime`,
`@objectstack/objectql` and `@objectstack/plugin-security` were newly reached
by `tsconfig.test.json` and resolved through each package's `exports` map to
`dist/index.d.ts` — a build artifact. For a pin whose whole subject is the
readers the reader program is about to CHANGE, a type verdict about the last
`pnpm build` is the vacuous-green direction that gate exists to refuse.

Fixed the way the gate prescribes for a dep exposed through an EXISTING
program: `paths` rules, never a widening of its shrink-only registry. Three
star-less (exact-match) rules, so `@objectstack/objectql/core` keeps resolving
through the package's own `exports` map untouched.

Measured, because the registry's own notes record `paths` billing other
packages' diagnostics into the consumer's ledger (PR objectstack-ai#12570): with the
workspace closure built, `pnpm --filter @objectstack/cli check:test-typecheck`
reports 3 files / 28 errors / 6 pinned signatures — byte for byte the
pre-existing `test-typecheck-debt.json`. Zero new diagnostics.

Also corrects the pin header's reverse-verification count to the measured 24
and records the `effect: 'writes'` -> `effect: 'pure'` row, a loss no
presence-check would have found.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
…, not the better-auth role scalar (objectstack-ai#15948)

* fix(plugin-auth): session payload positions[] carries the security positions, not auth roles

`customSession` built `user.positions` from a hand-rolled union — the
better-auth `sys_user.role` scalar split on commas, the active membership
mapped to `org_*`, and `platform_admin` — and read nothing from
`sys_user_position`, the ADR-0057 D4 table that is the source of truth for
custom positions. The Console binds that array straight through as the CEL
root `current_user` (objectui `expressionUser.ts`: `positions: user.positions
?? []`), so an `action.visible` narrowed by a business position answered FALSE
for everyone, including its holder — silently, because the root and the key
were both bound and CEL raised nothing.

`EvalUserSchema` had declared the opposite all along: `positions` is
"built-in identity names + position names", exposed to "every predicate
surface ... with an identical shape" so a predicate "evaluates identically
wherever it is written". The payload was violating a declared contract, not
merely carrying an ambiguous name. The documented example survived because
`org_admin` sits on both axes — the one name that could not reveal the split.

The derivation is deleted rather than repaired: `resolve-authz-context.ts`
states that every entry point must resolve authorization through it and never
re-read the `sys_*` grant tables itself, and this callback was doing exactly
what that forbids. It now asks `resolveUserAuthzGrants` — the same authority
`/auth/me/permissions` is served from and every server-side evaluator resolves
`ExecutionContext.positions` through — scoped to the session's active
organization. `isPlatformAdmin` is derived from that array (ADR-0068 D2
defines it as an alias of `'platform_admin' in positions`), so one authority
answers both and cannot disagree with itself.

The better-auth role scalar is not lost: `user.role` stays on the payload
verbatim, which is the ADR-0090 D3 documented exception for third-party
schema.

Fails closed and warns on an unreadable grant store, matching what both halves
of the old derivation already did silently.

Refs objectstack-ai#15136

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* test(plugin-auth): migrate the four fixtures that pinned the removed auth-axis derivation

They asserted `positions[]` WAS the better-auth `sys_user.role` scalar split on
commas — the derivation the objectstack-ai#15136 ruling removed — so they are fixtures to
migrate, not evidence against it. Each now asserts the scalar is absent from
the array while remaining untouched on the payload, which is the half of
ADR-0068 D2 that did not change. One case is added for the axis the payload was
missing entirely: a `sys_user_position` assignment reaching `positions[]`.

Also declares in `packages/spec` which axis `positions` is, and that the
better-auth role scalar is not it, with the regenerated reference page.

Part of objectstack-ai#15136

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* docs(spec): register the ADR-0087 semantic entry for the positions[] meaning change

A meaning change with no key move: nothing here can be found by grepping for a
removed spelling, and there is no schema rejection to catch a stale reader, so
the ledger row is the only channel that reaches an upgrader. The acceptance
criteria name the discriminator the defect itself hid behind — `org_admin`
sits on both axes and cannot tell them apart — and require the check be made
against a real session by a name that exists on one side only.

Part of objectstack-ai#15136

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* fix(plugin-auth)!: derive isPlatformAdmin from the posture RUNG, never from a name in positions[]

Contract review found a privilege-escalation path this PR would have
introduced. `positions.includes('platform_admin')` is the exact form
`resolve-authz-context.ts` forbids at `hasPlatformAdminStanding`: an ADR-0057 D4
`sys_user_position` row may spell that very name, and a platform-RBAC
assignment is not the ADR-0068 D2 capability grant.

That read was defensible while `positions[]` carried the auth axis, where
nothing a tenant writes could put the word there. Moving it to the security
axis moved the string into a space a tenant can WRITE: `sys_user_position` is
`apiEnabled`, a tenant-level admin passes the ADR-0090 D12 gate outright, and a
delegate passes `assertAssignmentWrite`'s `boundSets.every(...)` vacuously for
a position carrying no position-bound set. Measured on the real pipeline: a
plain org member with such a row got `isPlatformAdmin: true` and passed
`judgePlatformAdmin` — the `/admin/*` mount gate — while the rung said false.
Pre-fix all three answered false, so the disagreement was introduced here.

Both readers of the old "name implies rung" equivalence are re-bound:

- `auth-manager.ts` derives the alias from `grants.posture === 'PLATFORM_ADMIN'`
  on the same envelope, which is byte-for-byte what `hasPlatformAdminStanding`
  returns, so payload, predicate and gate cannot disagree.
- `platform-admin-gate.ts` drops its positions leg. That line is pre-existing,
  but this PR is what makes it dangerous, so it is in scope here.

Its fixture (`admin-ban-endpoints.test.ts`) is migrated per the ruling, which
named that file's cases as fixtures to migrate, and gains a case refusing the
escalation shape.

New pin `session-platform-admin-rung-agreement.test.ts` drives a real session
carrying a D4 row spelled `platform_admin` and requires all three predicates to
agree on false, with a genuine unscoped `admin_full_access` grant as the
control that they agree on true. It states its own population: it does NOT
cover the validity-window or catalogue shapes, which are the consolidation
suite's — that suite was green throughout the window this escalation was live,
which is the lesson.

Part of objectstack-ai#15136

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* docs(changeset): carve out the no-active-org membership projection, and correct the role-word attribution

Two corrections the contract review's standing guard asked for.

The changeset claimed "membership-derived names are unaffected" without
qualification. Measured: for a session carrying no active organization the
resolver projects every membership, so those names are now ADDED where the old
derivation contributed none — `[]` becomes `[org_admin, everyone]`. Pinned
rather than merely asserted; the first attempt at that pin failed because the
default fixture DOES stamp an active org, which is a different branch.

The deviation rationale attributed the identifier ban to `check:role-word`.
That gate ratchets the reserved word in docs prose; the identifier ban over
authored metadata lives in `packages/lint`. A TypeScript payload key trips
neither mechanically until documented, so the ADR-level prohibition is what
rules — the conclusion is unchanged, the wording was wrong.

Also names the new three-way-agreement pin beside PIN 6, and scopes PIN 3's
reference to the session's active organization: unscoped, it agreed only
because the fixture has one org, and a scoping regression would have passed it.

Part of objectstack-ai#15136

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* test(plugin-auth): migrate the /admin/sso/* bridge fixtures off the removed positions leg

Same species as the fixture the ruling named: the platform-admin session shape
stood for a real payload back when the gate admitted on the `platform_admin`
NAME. It carries the rung-derived alias now, because that is what the gate
reads. `positions` keeps the name, since a genuine platform admin really does
have it projected — which is exactly why the name alone cannot be the signal.

Adds the escalation shape as a refused case on all four bridges. Without it,
restoring the array leg would reopen these operator routes and every existing
case would still pass.

Part of objectstack-ai#15136

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* fix(plugin-auth): widen the customSession dataEngine double to its seam's arity

`Type Check · workspace` (run 33978981028) failed `@objectstack/plugin-auth#typecheck`
on one ARRIVED signature: TS2554 "Expected 1 arguments, but got 2" at
src/auth-manager.test.ts:3543, where the `sys_user_position` case overrides
`makeDataEngine`'s `find` and delegates `inner(object, q)` to a double declared
one-parameter, `vi.fn(async (object: string) => ...)`.

CHOSE (a) widen the double; NOT (b) drop the second argument at the delegation.

The double stands in for `IDataEngine.find(objectName, query?, options?)`
(packages/spec/src/contracts/data-engine.ts:259), and every production read that
reaches this fake goes through `resolve-authz-context.ts` `tryFind`, which always
calls `ql.find(object, { where, limit, context })` — two arguments, never one.

(b) would also have typechecked, by making the delegation call `inner(object)`.
That is worse: it fixes the compiler by teaching the double a call shape
production never produces. `find` here is a `vi.fn`, so its recorded calls are
assertable, and the tenant-scoped `context` `tryFind` threads is exactly the kind
of claim a future test would assert over `engine.find.mock.calls` — against a
shape that cannot occur. A delegation that silently drops an argument the seam
passes is how a double stops expressing the condition it exists to test.

Scope: this one type error. No production file touched — the round-2 Clause-(2)
contract review's design stands unmodified. Nothing added to
test-typecheck-debt.json (MAINTAINER-ONLY, ratchets down only); the gate reports
the ledger unchanged at 10 file(s) / 94 error(s) / 23 pinned signature(s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* test(plugin-auth): satisfy check:test-source-alias on the positions-axis pin

`check:test-source-alias` is the only step making `Lint & Repo Gates` red on
this PR: job 101340631783 fails at step 130 and skips steps 131-148, so the
merge-driver gate that reds other PRs on this seat never runs here. Base-branch
control: the same gate exits 0 on `origin/main` f7db8f4, and the test file is
absent from that ref, so the redness is this PR's.

Both findings are remedied exactly as the gate's own text dictates.

1. `session-positions-security-axis.test.ts:235` paid
   `import('@objectstack/core')` inside an `it()` body -- a CLOCKED window, and
   `@objectstack/core` is one of the specifiers this package still resolves
   through `dist/`. A module-top side-effect import pays that transform during
   collection, which vitest clocks against nothing. The dynamic call stays
   exactly where it is, and no timeout was widened -- widening relocates the
   cliff to the next heavier shard.

2. `@objectstack/formula` was a NEW unaliased artifact import for this package
   (`celEngine`, the real CEL engine the pins evaluate `current_user.positions`
   with). One anchored entry in the package's `vitest.config.ts` resolves it to
   `packages/formula/src/index.ts`, which exists in the checkout.
   `KNOWN_UNALIASED_TEST_IMPORTS` is shrink-only, so no registry row was added
   or widened; plugin-auth's existing entry is untouched.

Verified: `check:test-source-alias` --self-test and the real run both exit 0,
the real run reporting the same counts as the base-branch control (72 packages
scanned / 61 registered / 49 published subpaths). `pnpm --filter
@objectstack/plugin-auth test` still 102 files / 2142 tests passed, and the
package's full `typecheck` task is green with its test-typecheck ledger
unchanged at 10 file(s) / 94 error(s) / 23 pinned signature(s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

* test(plugin-auth): resolve @objectstack/formula's TYPES to source too

The sibling gate `check:type-source-resolution` is red on the same specifier,
and it is red at the PR head `b65fafc81` as well -- measured with both files of
the alias fix-up reverted in place under a trap, then restored byte-identical.
It was invisible only because `Lint & Repo Gates` halts at step 130
(`Test-source alias gate`) and skips 131-148; this gate is step 132, so
unblocking 130 walks the job straight into it.

Same defect class, same specifier, same round-1 import (`celEngine`), and the
remedy the gate dictates: ONE `paths` rule for the bare name in the package's
`tsconfig.json`, copied in shape from the `@objectstack/service-messaging` rule
already beside it. `@objectstack/formula` publishes no subpath in its `exports`,
so a subpath rule would match nothing on disk and fall back to node resolution.
`tsconfig.test.json` inherits this map rather than declaring its own, which is
what carries the rule to the test layer that reaches the specifier.

Measured rather than assumed, because objectstack-ai#12570 records this exact rule billing
another package's source diagnostics into this ledger:

- `check:type-source-resolution` 0 -- "125 tsc program(s) across 78 packages
  scanned; 61 registered as still resolving a workspace dep's types through
  `dist/`".
- `pnpm --filter @objectstack/plugin-auth typecheck` 0, with
  `check:test-typecheck` reporting the ledger UNCHANGED at 10 file(s) /
  94 error(s) / 23 pinned signature(s) -- nothing was added to it.
- `tsc -p tsconfig.test.json --listFiles`: 15 `packages/formula/src/*.ts` files
  now in the program, 0 files under `packages/formula/dist/`, program 1243 ->
  1257 files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XpTx2tbq3pZRYAdoGt6E6Y

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 9, 2026
… the maintainer-only closure at the point of refusal (objectstack-ai#16257)

* docs(check-type-source-resolution): name the `paths` precondition and the closure at the point of refusal

The refusal names `paths` first. That remedy has a precondition it never
stated: the consuming program's `rootDir` has to contain the dependency's
source. Where it does not, tsc admits the dep's file graph and then reports
TS6059 for all of it, billed to the consuming package's test-typecheck ledger
— the PR objectstack-ai#12570 shape, reached from the `paths` limb instead of the
re-baseline one. Both the ledger and `rootDir` are maintainer-only, so at that
point the author has no self-serve remedy and the message named none.

The knowledge was already in this file — the header's with-`paths` red and the
PR objectstack-ai#12570 reading over the registry — and nowhere in the text an author reads.

Written as a CONDITION, never a blanket closure: `paths` stays correct wherever
the program's `rootDir` does contain the source, and a message overstating the
closure would be worse than the current silence.

The refusing sentence, the ⛔ SHRINK-ONLY testimony and the ONBOARDED exception
are untouched, so `check-ratchet-remedy-authority` still classifies this gate as
`refused`. Two self-test pins added for the new halves and the battery floor
raised 10 -> 12 to match.

Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(check-type-source-resolution): record the re-measured `paths` -> TS6059 leg

Measured on this branch rather than carried over from the filing: the runtime
test program goes 191 -> 204 errors under the `paths` remedy, +13, all TS6059
in `packages/services/service-realtime/src/**`, no new code error.

Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
Co-authored-by: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dependencies Pull requests that update a dependency file size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants