Skip to content

refactor(formula): remove the never-bound EvalContext.api declaration - #18736

Merged
huangyiirene merged 3 commits into
mainfrom
claude/issue-18318-remove-evalcontext-api
Sep 17, 2026
Merged

huangyiirene merged 3 commits into
mainfrom
claude/issue-18318-remove-evalcontext-api

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #18318

Clause-②: yes

Execution of the maintainer ruling on batch #148 item 1, letter B (comment 5716041368, 「同意」): EvalContext declared a kernel query API — api?: { exists, count, lookup }, docblocked as os.exists / os.count / os.lookup, "implemented opportunistically by call sites that have a query engine" — and buildScope() never read it. The declaration is removed, with the reason written at the deletion site. No shim, no alias, no reserved spelling; the three functions are not implemented here, and the real capability need (reading a related record's field inside a predicate) is tracked on its own card, #18682, which nothing in this PR advances or blocks.

The premise, re-measured on the branch base (df1b275)

reading value control
ctx.api / .api in packages/formula/src 0 hits ctx.user in stdlib.ts: 3 hits
os.exists / os.count / os.lookup in the tree, CHANGELOGs and dist/ excluded 1 hit — the docblock being deleted the same grep finds the three names nowhere else, so no stdlib entry, validator row, doc page or skill file has to follow

So the card's reading holds exactly: the member was declared, documented, and bound nowhere.

The LEVEL is minor, and that is a reading rather than an assumption

The question the grade turns on: can a consumer set api today and get anything out of it? No.

  • buildScope() binds record, previous, input, os.user / os.org / os.env, and extra — it has never read ctx.api, so every implementation ever passed there was discarded before evaluation.
  • A predicate calling os.lookup fails whether or not the call site supplies api — pinned as a test in this PR.
  • The whole of the break is therefore a compile error on an EvalContext literal that carries api, and the sweep below finds zero such literals in this repo and zero in the pinned sibling.

This is the shape PR #18717 graded minor in the same round — what changed never delivered the semantics it reported, so depending on it meant depending on nothing. The launch-window major ban (scripts/check-changeset-no-major.mjs) also applies and is recorded here as a constraint, not the justification. The changeset is @objectstack/formula: minor and carries the migration line it owes: api: { … } → delete the property; there is no replacement key and no result changes.

Consumer sweep — derived by TYPE, with a firing control

⛔ Not by text. A text grep is structurally useless here: the identically-named ctx.api on the hook ScopedContext (packages/spec/src/contracts/scoped-context.ts) is a live, unrelated surface this PR does not touch, which is why the dispatching seat's own api: *{ grep returned 234 files and was reported as an instrument failure.

Population — the 19 workspace packages that declare @objectstack/formula. Every one has a typecheck script, so no filter matched zero scripts and exited 0 without running anything.

Measurement leg (at d706b60b57, after turbo run build — 73 tasks successful, so each package resolves formula through its rebuilt dist/index.d.ts): all 19 packages typecheck green, error TS count 0.

Firing control — the same 19-package run with a REQUIRED member injected into EvalContext (__ablationProbeFiringControl: 'fire'), formula rebuilt, and the marker proved present in dist/ by node scripts/ablation-dist-preflight.mjs @objectstack/formula '__ablationProbeFiringControl' before the colour was read:

  • 12 of 19 packages red; 21 distinct error TS2345 supply sites across 13 files, plus two more files caught a layer down by the test-typecheck ledger gate (platform-objects, plugin-auth) — e.g. metadata-protocol/src/seed-loader.ts:1026, objectql/src/validation/rule-validator.ts (6 sites), service-automation/src/engine.ts, plugin-approvals/src/approval-service.ts.
  • ⇒ the instrument reaches consumer code and goes red when EvalContext changes incompatibly. The green above is a measurement, not a silence.

Restore leg: git checkout HEAD -- packages/formula/src/types.ts, git hash-object equal to the HEAD blob 27f991bbd7…, whole-tree git status --porcelain empty, formula rebuilt, and ablation-dist-preflight … --absent green — so no mutated byte survives in dist/ for a later run to read.

The opposite direction the card asked about — does anything CONSTRUCT an EvalContext carrying api? — is answered by the same green leg: after the removal such a literal is a compile error, and none exists.

Pinned sibling (Post-Task Checklist step 4, Console Pin Gate): objectui at .objectui-sha = 53ded82bf7a494f54e344e19099dbf00854b8694 names EvalContext zero times; control in the same tree — four distinct from '@objectstack/formula' import lines (ExpressionEngine, validateExpression, collectCelRootIdentifiers, firstUndeclaredReference) across packages/app-shell. Nothing there is broken by this removal and no pin bump is owed.

Tests

packages/formula: 31 files / 876 tests pass, pnpm typecheck green. The new src/eval-context-no-query-api.test.ts pins the retirement in both directions — a type-level pin (@ts-expect-error on api in an EvalContext literal) and two runtime pins (buildScope binds nothing from a context still carrying one; a predicate calling os.lookup is refused even when an implementation is supplied).

The type-level pin is verified live rather than assumed: formula's test-typecheck ledger reports the same 3 files / 7 errors / 4 signatures as before this PR. That ledger is exact and shrink-only, so an @ts-expect-error that had become unused would arrive as TS2578 and red it. It stayed silent, which means the directive is used — api really is rejected.

Gates

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 57 gate families from the change set itself (the 3 paths above). All 57 were run at this branch's head and every one exited 0; --ran reconciliation with the exit codes recorded reports 57 derived / 57 run / 0 NOT-MEASURED / 0 UNRUN. The artifact-roster, wide-population and path-scheduled families the same tool prints outside that total remain CI's.
  • pnpm lint is CI's repo-wide run; the local delivery is a declared narrowing with its three readings: ① the population eslint's own config admits is 6819 files (git ls-files filtered to lintable extensions, each asked through ESLint#isPathIgnored; 0 ignored) — note eslint.config.mjs's own header records 4659 from an older tree, a drift this PR does not touch; ② the narrowed run linted 2 files (--format json, result length), 0 errors / 0 warnings; ③ the config enables no type-aware linting (no parserOptions.project, no projectService, no typed @typescript-eslint rules — its header says so and the config carries no such key), so this diff cannot move any untouched file's verdict.
  • skip-changeset measured rather than assumed: @objectstack/formula's files[] is dist, README.md, CHANGELOG.md; after the build, packages/formula/dist/index.d.ts carries the EvalContext interface with grep -c "api?:" = 0 — positive control grep -c "EvalContext" = 10 in the same file. Published bytes move, so the label would be wrong and a changeset is owed and present.

Acceptance notes

  • Out of scope, noted and not filed: eslint.config.mjs's header states "across all 4659 linted files" while the population reads 6819 today. A stale prose count in a config header — an observation, not a defect, a contract violation or an authoring trap. Carrier: the next PR that edits that header.
  • The identically-named ctx.api on the hook ScopedContext is untouched and stays live; nothing in this PR narrows it.
  • needs:contract-review belongs to the dispatching seat, which hangs it on both carriers. This PR neither hangs, removes nor waits on it.
  • Draft on purpose: the PR body is written once, in the opening stroke, and never patched — anything it needs later is named in the dev report for the seat to edit.

Generated by Claude Code

…ontext

`EvalContext.api` declared `exists` / `count` / `lookup` and promised
`os.exists / os.count / os.lookup`, "implemented opportunistically by call
sites that have a query engine". `buildScope()` read `ctx.api` zero times, so
no call site could bind them however well it cooperated: a predicate calling
one faulted with `found no matching overload for 'dyn.lookup(string, dyn)'`,
and an unevaluable predicate refuses the write it guards — a validation rule
authored against the declaration locked every write on its object.

The declaration is removed rather than implemented, with the reason written at
the deletion site. No shim, no alias, no reserved spelling.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>
Clause-② is declared on the card and on the PR body; the changeset body
carries the same line. The bump is `minor` rather than `major`: the removed
declaration never delivered the semantics it reported, so nothing a consumer
could depend on at runtime moves — the whole of the break is a compile error
on an `EvalContext` literal that carries `api`.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation tests tooling labels Sep 17, 2026
@github-actions

github-actions Bot commented Sep 17, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

1 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • 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 — 7 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 4691ad17d273f23d90bd5ef495728dd2a12ee020 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 43bff01ddc6bda3558ad94763e72262c6697163e — the merge of head 2b053aebd7ba4bd24fe469c91790308d31333b28 into base 4691ad17d273f23d90bd5ef495728dd2a12ee020, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 43bff01ddc6bda3558ad94763e72262c6697163e && git checkout 43bff01ddc6bda3558ad94763e72262c6697163e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 4691ad17d273f23d90bd5ef495728dd2a12ee020 2b053aebd7ba4bd24fe469c91790308d31333b28 && git checkout -B drift-repro 4691ad17d273f23d90bd5ef495728dd2a12ee020 && git merge --no-ff 2b053aebd7ba4bd24fe469c91790308d31333b28

node scripts/docs-audit/affected-docs.mjs --json 4691ad17d273f23d90bd5ef495728dd2a12ee020

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

…disposition

The launch window refuses `major`, so the level cannot carry breaking-ness:
`.github/workflows/pr-automation.yml` puts it on the `**BREAKING**` banner and
the ADR-0087 disposition instead. The bump stays `minor`.

The disposition is `runtime-interface-only`, named at
`packages/formula/src/types.ts#EvalContext` and verified by the gate: a
published runtime TypeScript interface with no Zod schema, no `packages/spec`
declaration and no stored representation, so the ledger has nothing to project
and the compiler is the channel that reaches every affected caller.

`type-surface-only` was tried first and refused, twice, on measurement: the
dotted member spelling does not resolve through an interface, and at the
interface the base side is already concrete, which is the shape predicate 4
exists to refuse.

Claude-Session: https://claude.ai/code/session_01CqmCgU5RGDoJYhHUMVp2af
Co-authored-by: Claude <noreply@anthropic.com>

Copy link
Copy Markdown
Collaborator Author

needs:contract-review cleared from both carriers — provenance for the stroke, written 2026-09-17T18:32Z.

contract-review record card #18318, comment 5718801694 — ## Contract review, Served-tier: present, VERDICT: PASS
head judged by that record 2b053aebd7ba4bd24fe469c91790308d31333b28 — the head this PR still carries, ⛔ not a predecessor
carriers card #18318 cleared · PR #18736 cleared, one stroke, label-write.mjs with read-back on each

Pre-landing checks, all three re-taken at this stroke — ⛔ none carried over.

  • ① the in-seat clause-② review is in case, in the template's shape, at the tier the fuse reads — and it PASSED only after a patch round this seat demanded: the changeset now carries **BREAKING** plus one ADR-0087 disposition, which is how breaking-ness is declared while the launch window refuses major (pr-automation.yml:723-724).
  • ② check-clause2-carriers.mjs --pair 18736 → exit 0, and it reports that a review of record names this head.
  • ③ CI on 2b053aebd7: 34 names, 0 pending, 0 non-green; all seven required contexts green. The three skips — Build Docs · Console Pin Gate · Packed-tarball smoke (opt-in) — are each on check-expected-skips' roster with a declared reason this diff satisfies. ⭐ Note what is not skipped here: Check Changeset ran, because this PR carries no skip-changeset label — the changeset was judged by the gate, ⛔ not by the seat alone.

Governed-surface predicate, derived by the script from this PR's own file list: check-governed-merges.mjs --pr 18736 → 0 of 3 paths ⇒ ordinary queue landing.

⚠️ ⛔ Two things this provenance records against the seat rather than the dev.

  1. The needs:contract-review PR carrier was hung by the seat only after the dev reported row C1; the card side was hung at the claim stroke as it should be.
  2. ⭐ The patch order named the wrong ADR-0087 category (type-surface-only). The dev derived runtime-interface-only instead and proved it — the gate answers not-required (runtime-interface-only) -- verified: packages/formula/src/types.ts#EvalContext (interface), exit 0. ⇒ The record is right because the order also said 「⛔ do not take my spelling on trust; derive it from the gate and prove it」, and the dev did exactly that.

⇒ Going ready and into the merge queue (SQUASH). This seat follows it to MERGED.


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review September 17, 2026 18:32
@huangyiirene
huangyiirene added this pull request to the merge queue Sep 17, 2026
Merged via the queue into main with commit 9be2b59 Sep 17, 2026
39 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-18318-remove-evalcontext-api branch September 17, 2026 19:09
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…side is an instruction (objectstack-ai#18834)

Fixes objectstack-ai#18745

ADR-0087's `no-migration-prescription` detector missed a framed FROM →
TO line again, so
an exemption the categories inherit can be held by SILENCE rather than
by a finding. This
adds the one arm that was missing, answers the three questions the claim
could not, and
hands back the one reading that is not this PR's to act on.

This is ⛔ NOT a defect in PR objectstack-ai#18736. That PR's exemption rests on four
positively
verified predicates and the gate printed `-- verified`. What recurred is
the INSTRUMENT.

Clause-②: no — one repo gate script, no published byte moves (measured
below).

## Q1 — which spelling was missed, beside the branch that should have
caught it

Anchored on SYMBOLS and reported where they actually are. In
`scripts/check-adr-0087-registration.mjs`: `findMigrationPrescription`
is at **:1562** on
`origin/main` and at **:1685** on this branch. The card's `:204-214` for
the header
paragraph is NOT stale — `That argument is correct and it was accepted`
really does open
at **:204** on `origin/main`, and still does here (this PR appends its
cross-reference
after that paragraph rather than editing it).

```
MISSED:  **Migration — `api: { … }` → delete the property.**        .changeset/18318-evalcontext-no-query-api.md
BRANCH:  2, `framed-line`  (REWRITE_RE under MIGRATION_FRAMING_RE)
```

Predicate by predicate on that one line, each with its own non-zero
control:

| predicate | on the missed line | non-zero control |
|---|---|---|
| `MIGRATION_FRAMING_RE` | `true` | `false` on `plain prose` |
| FROM side is an `OPERAND` | `true` (a backticked span) | — |
| an `ARROW` is present | `true` | — |
| `REWRITE_RE` (needs an `OPERAND` on BOTH sides) | **`false`** | `true`
on the same line with `` `os.exists` `` as the TO side |

So it failed on exactly ONE predicate. `delete the property` is an
INSTRUCTION, not a
name — and `OPERAND` is a backticked code span or a dotted/slashed
identifier path. A
removal has no new name to put on the right, so the most careful
prescription an author
can write for one is invisible to every arm.

## Q2 — the objectstack-ai#8277 miss in the gate's header is a DIFFERENT hole

Measured on `fc71b84:.changeset/lucky-schools-smash.md` through the
exported detector.

| | objectstack-ai#8277 (the header's case) | objectstack-ai#18745 (this round) |
|---|---|---|
| the prescription | `read` `` `result.driverFault?.message` `` `where
you read` `` `result.error` `` | `` `api: { … }` `` → `delete the
property` |
| arrow on that line | no | yes |
| migration framing on that line | no | yes |
| FROM side an operand | n/a — no pair at all | yes |
| what it fails on | there is no rewrite PAIR to match | the TO side is
not an operand |
| after this PR | still `null` | `framed-removal` |

objectstack-ai#8277 is the **IMPERATIVE-SENTENCE** residue this gate's own header
already lists as
deliberate ("no arrow, no table, just …"), and this PR does not move it
one inch. RM11
and RM12 pin that in the self-test, so a later author widening toward it
argues with a
red test rather than with prose. Closing the second hole does not close
the first, and a
repair that claimed to would be the over-matching criterion the residue
paragraph says to
falsify before implementing.

## Q3 — does tightening red the existing changeset stock? ONE reading,
handed back

Measured over the 414 changesets in `.changeset/` at `origin/main`:

```
BEFORE  hits 28 / 414   {framed-line: 1, from-to-label: 25, header-framed-table: 2}
AFTER   hits 29 / 414   {framed-line: 1, from-to-label: 25, header-framed-table: 2, framed-removal: 1}
```

Exactly one row moves, and it is the card's own:
`18318-evalcontext-no-query-api.md`,
`null` → `framed-removal`. It holds `not-required
(runtime-interface-only …)`, a category
that INHERITS the prescription refusal, so it WOULD be refused if the
gate re-judged it.
It does not today: this gate judges only changesets a diff adds or
modifies, and this PR
touches none. `pnpm check:adr-0087-registration` is green on this
branch.

⚠️ **That reading is handed back, not acted on.** ⛔ No other author's
changeset is edited
here. The order is the seat's to decide.

## LIT — the behaviour flip, on the REAL missed text

Not "the gate is still green". The same 414-changeset verdict table,
before and after:

```
2,4c2,4
< HIT (prescription found): 28
< BY BRANCH: {"framed-line":1,"from-to-label":25,"header-framed-table":2}
---
> HIT (prescription found): 29
> BY BRANCH: {"framed-line":1,"from-to-label":25,"header-framed-table":2,"framed-removal":1}
191c191
< null	18318-evalcontext-no-query-api.md
---
> framed-removal	18318-evalcontext-no-query-api.md
```

End to end through `scan()`, the card's own scenario: the real missed
text carrying
`not-required (runtime-interface-only
packages/formula/src/types.ts#EvalContext)` is now
REFUSED with `Evidence (framed-removal)` (`RM-E2E1`), and the SAME claim
on the SAME
symbol with the removal sentence taken out is still ACCEPTED
(`RM-E2E1b`) — which is what
makes the red attributable to the prescription refusal rather than to a
hand-built
fixture failing some `runtime-interface-only` predicate for an unrelated
reason.

## DARK — the spellings it already recognised keep their verdict

The other **413** rows of that table are byte-identical before and
after, and every
pre-existing branch keeps its count (`framed-line` 1, `from-to-label`
25,
`header-framed-table` 2). That is structural, not
measured-and-remembered: the new hit is
held back and consulted LAST, so the function is exactly
`arrowBranches ?? framedTable ?? headerFramedTable ?? framedRemoval` and
no body an older
arm reads can change its branch or its evidence line.

Self-test case counts: **384 → 405** assertions, exit 0 both times. Two
new batteries
(`RM` end-to-end, 9 cases; `RM1-RM12` unit pins, 12) are declared in
`SELF_TEST_BATTERIES` with their own floors.

## The class is closed, and derived from the stock rather than invented

Measured over EVERY `.changeset/*.md` blob in this repository's history
— 5524 unique
blobs — the framed arrow lines whose right side is not an operand number
**three**. Every
candidate vocabulary tried (bare imperatives; plus `unset`/`omit`; plus
`none`/`nothing`;
plus the past participles; plus the Chinese arm) flags the **same two**
changesets and no
others — `18318-evalcontext-no-query-api.md` and
`drop-dead-env-template-flag.md`
(`**Migration.** …create --template… → drop the flag`), both genuine
removal
prescriptions on inspection. Zero false positives at any width, so the
NARROW class
ships: a wider one buys nothing measured and each extra word is a claim
somebody later
has to defend. The third non-operand right side,
`lookup-reference-target-gate.md`, is
running prose about resolution rungs, and this arm leaves it alone.

`none` / `nothing` in arrow position was measured and REFUSED: the stock
evidences that
spelling only as a table CELL, which branch 4 already reads, and in
arrow position it has
a competing reading an imperative verb does not (`… → none of this
changes what you
write` is a running sentence). RM8 pins the refusal.

## Reverse verification (ablation)

Committed first, then mutated on disk, then restored and proven:

Committed first, then mutated on disk, then restored and proven — the
mutation is
`return table ?? headerTable ?? removal;` back to `return table ??
headerTable;`, which
disables the arm and nothing else:

```
HEAD blob:                       84cce40
working-tree blob BEFORE:        84cce40
BEFORE: arm-live count=1   ablated count=0
AFTER MUTATION: arm-live count=0   ablated count=1
on-disk proof, blob AFTER:       6d8d731286f2d643873ce63658b55959bec9a3bc
ABLATED self-test exit: 1  -- 16 failure(s)
  RM-E2E1 ... : expected RED, got green
  RM-E2E2 ... : expected RED, got green
  RM1 / RM2 / RM3 ...
  and check-adr-0087-registration itself: 1 input problem -- the framing-scan-rot control
restored blob:                   84cce40
restore proven: byte-identical to HEAD, git diff HEAD empty
RESTORED self-test exit: 0  -- 405 assertions
```

The ablated run NAMES the RM cases, so they are wired to the arm rather
than passing for
some other reason, and the new framing-scan-rot control fires on the
live run too. The
script is read by `node` directly and no package builds it, so there is
no `dist/` leg to
this ablation.

## Changeset — measured, not inferred from the path

`skip-changeset`. The repo-root manifest is `"private": true`, so
nothing at repo root is
published; the edited file is tracked only at
`scripts/check-adr-0087-registration.mjs`
(plus its ADR anchor JSON), and **0 of the 70** workspace manifests
carrying a `files[]`
name a path that could ship it — non-zero control: 70 manifests were
read, e.g.
`packages/adapters/hono` ships `dist,README.md,CHANGELOG.md`.

## Acceptance notes

Gates derived with `scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` (32 commands) and all 32 run on this branch:
**31 exit 0**.
The 32nd, `pnpm check:pm-dispatch-gates`, is two batteries chained and
exceeded the 480s
window it was given (exit 124 = timeout, NOT MEASURED); re-run on its
own,
`node scripts/pm/check-dispatch-gates.mjs` is **exit 0, 1809 cases,
535.2s on this box**.
`check-widening-tells --declaration no` reports this file as NOT
MEASURED (no declared
surface covers `scripts/`), which is ⛔ not a clearance and is stated as
such. Repo-wide
scans (`pnpm lint` and the artifact-roster families the deriver lists as
silent) are CI's
run, not this PR's claim.

Noted, ⛔ not filed, and ⛔ not folded in — recorded so they are not lost:

- `check-adr-0087-registration`'s predicate-4 refusal text for
`type-surface-only` prints
"fix: register the migration" without naming `runtime-interface-only`.
Already recorded
on the card by the filing seat; the carrier is whoever next edits that
message. Not
touched here — it is a refusal-message improvement, not a wrong verdict.
- `.changeset/unscoped-multi-update-refusal.md` writes its TO side as a
BOLD-WRAPPED code
span (`** ` + backticked name + ` **`), which `OPERAND` does not reach
because `\s*`
after the arrow cannot cross the asterisks. It is caught today by
another line in the
same body, so no verdict rests on it, and widening `OPERAND` is a
different claim with
its own false-positive surface. Carrier: whoever next measures the
operand vocabulary.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…removal note (objectstack-ai#18905)

Fixes objectstack-ai#18842

Clause-②: no

One line of prose in one pending changeset. No code, no package, no
export, no
published surface moves.

## What was contradicting what

`.changeset/18318-evalcontext-no-query-api.md` carries an `adr-0087`
disposition
marker — an HTML comment, spelled out here in words because the platform
rewrites that
shape — declaring `not-required`, category `runtime-interface-only`,
naming the symbol
reference `packages/formula/src/types.ts` hash `EvalContext`. That
disposition inherits the `no-migration-prescription` refusal (objectstack-ai#8299).
After
PR objectstack-ai#18834 landed the RETIREMENT arm in
`scripts/check-adr-0087-registration.mjs`, the
body's own lead line reads as a framed migration prescription:

```
**Migration — `api: { … }` → delete the property.**
```

So the changeset's head and its foot now say opposite things, and the
gate refuses the
claim the moment a diff makes it judge the file.

**The refusal is right on its own terms and the detector is untouched
here.** What is
wrong is the disposition surface, so that is the only thing this PR
moves.

**In fairness to the author of objectstack-ai#18318 / PR objectstack-ai#18736: they did nothing
wrong.** All four
`runtime-interface-only` predicates were verified positively, and that
author handed
the detector miss up rather than letting it carry their claim. The
defect was in the
detector, closed by objectstack-ai#18745 / PR objectstack-ai#18834; what is left is that the new
reading
contradicts this one changeset's framing. That is why this diff edits
somebody else's
release note.

## The edit

```
- **Migration — `api: { … }` → delete the property.**
+ **Your fix — delete the `api: { … }` property.**
```

Five sentences follow it in the same paragraph and are byte-identical.

## Why the two sentences say the same thing to a consumer

Token for token:

| element | before | after |
|:---|:---|:---|
| the operand | `api: { … }` | `api: { … }` — unchanged, still a code
span |
| the verb | delete | delete |
| the object | the property | the property |
| the arrow | `→` | dropped |
| the label | Migration | Your fix |

The arrow is this repo's FROM/TO pair notation, and a pair notation
reserves its
right-hand place for a replacement NAME. There is no replacement name
here — which is
exactly why the author had to write an instruction in that position.
Dropping a
two-place notation that only ever had one place filled removes no
information; the
instruction that stood in the second place is still there, as the
sentence's own verb.

The label is the substantive half. In this repository "migration" names
what the
ADR-0087 ledger carries and what `objectstack migrate meta` performs,
and the
disposition two lines below states in writing that no such thing exists
here: no Zod
schema, no `packages/spec` declaration, no stored representation,
nothing to rewrite.
The paragraph was never a metadata migration; it is the one-line fix for
a TypeScript
consumer, delivered by the compiler at their own call site. AGENTS.md
calls that
element "the one-line fix", and the new label names it.

Everything a consumer needs is still on the page and unchanged: what to
delete, that
there is no replacement key, that nothing is re-pointed, that no
predicate result
changes, that TypeScript is where they hear about it, that reading a
related record's
field stays unexpressible, and that no schedule is implied.

## LIT — the refusal was live before, and the claim is positively
verified after

Both legs run the shipping gate's own `scan()`, with base set to the
parent of the
commit that ADDED this changeset (`09e16a5745`, parent of `9be2b594af` /
PR objectstack-ai#18736), so
the row arrives as `A` and is judged instead of inherited as stock.

```
BEFORE (84ba4a8)   LIT-TARGET-PROBLEMS  1   LIT-TARGET-JUDGED-OK  0
  `not-required (runtime-interface-only)` contradicts the changeset's own body,
  which carries a migration prescription.
  Evidence (framed-removal):
    **Migration — `api: { … }` → delete the property.** ...

AFTER  (66f12e4)   LIT-TARGET-PROBLEMS  0   LIT-TARGET-JUDGED-OK  1
  verdict: not-required, category: runtime-interface-only,
  ids: packages/formula/src/types.ts#EvalContext,
  detail: packages/formula/src/types.ts#EvalContext (interface)
```

The after leg is not silence: the gate judges the changeset and verifies
the four
`runtime-interface-only` predicates at HEAD.

## DARK — the whole stock, classified before and after

`findMigrationPrescription` imported from the gate and run over every
`.changeset/*.md` in the tree, both legs:

```
stock 428
BEFORE  {framed-line:1, framed-removal:1, from-to-label:26, header-framed-table:2, null:398}
AFTER   {framed-line:1,                   from-to-label:26, header-framed-table:2, null:399}
```

A row-by-row diff of all 428 rows shows exactly one line changed:

```
< 18318-evalcontext-no-query-api.md  framed-removal  **Migration — `api: { … }` → delete the property.** ...
> 18318-evalcontext-no-query-api.md  null
```

The other 427 rows are byte-identical, branch and evidence line
included.

## Gates

All 18 families derived by `node scripts/pm/dispatch-gates.mjs
--commands --repo
objectstack-ai/objectstack` were run and reconciled with `--ran` (18
derived, 18 run,
0 NOT-MEASURED), plus `node scripts/check-changeset-fixed.mjs`, whose
roster sits under
`.changeset/` and which the derivation flags as neither-evidence for a
path in that
directory. Green, exit 0, except the one below.

`node scripts/check-empty-changeset.mjs --base origin/main` — **exit 1,
and it cannot be
made green by this PR.** That gate refuses any diff that CHANGES a
changeset it did not
ADD, and it names the two shapes that produce the refusal. This is the
second one:

> DELIBERATE CORRECTION -- your change may have made this PENDING
release note false,
> and you rewrote it in the same stroke. Remedy: do NOT restore it --
say so on the PR
> and get it confirmed; restoring it from the base would put the false
sentence back.
> ... this gate stays red either way, and staying red is what puts the
decision in
> front of a person instead of routing around it.

So, saying so, as the gate asks: the note is
`.changeset/18318-evalcontext-no-query-api.md`; what changed under it is
its own
ADR-0087 disposition reading, not any behaviour; and the rewrite is the
lead line
quoted above. Restoring it from the base would put back the
contradiction this PR
exists to remove.

`Check Changeset` will therefore be red on two of its steps: this one,
and "Require a
changeset (or the skip-changeset label)", which counts `--diff-filter=A`
rows and sees
none. It is not one of the seven required contexts.

**The `skip-changeset` label is deliberately NOT applied**, and that is
a decision worth
a second pair of eyes rather than a default: the label exempts the WHOLE
job, so it
would clear the deliberate-correction refusal along with the
missing-changeset one —
the routing-around the gate's own text forbids. Leaving it off keeps the
decision
visible. If the maintainer or the dispatching seat judges otherwise, the
label is one
additive write away.

## Acceptance notes

Noted, not filed.

- The card frames the future refusal as arriving when "any PR touches
`packages/formula/src/types.ts`". That is not how the gate selects rows:
`scan()`
reads `git diff --name-status --diff-filter=AMR merge-base HEAD --
.changeset/*.md`,
so the source file a PR touches never enters the selection. The
reachable trigger is a
PR whose merge base predates `9be2b594af` and which has since merged
`main` — the
changeset then arrives as an `A` row and is judged, which is precisely
the leg this PR
uses as its LIT probe. The direction of the card is right and its
consequence is real;
  only the stated trigger is.
- `runtime-interface-only` inherits a refusal written for metadata
prescriptions, while
the class it serves delivers through the compiler. objectstack-ai#13080 recorded the
same mismatch
for `type-surface-only` and bought an exemption with predicate 4. No
card is filed:
this is an observation about a boundary, not a reproducible defect, not
a violation of
a declared contract, and not a trap that makes an author write metadata
the runtime
rejects. Prospective carrier: none — no open PR or queued issue touches
this seam.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

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

documentation Improvements or additions to documentation size/s tests tooling

Projects

None yet

2 participants