You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit c55d300
Browse filesBrowse the repository at this point in the historyBrowse files
refactor(metadata-protocol): the two overlay-write closes ask one fix-can-be-taken predicate (#29874)
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ897VUcPShHgSHgmfYBAj4Gx)_Fixes#29397
Clause-②: no
Metadata-protocol: none
## 维护者速读
-
改了什么:两个覆盖层写入关闭判定(`namedBaseCloseSubject`、`packagelessCloseSubject`)里逐步重复的「修复能否走通」四步(配对
id、ensure 门、基包版本、D2 门放行声明体)收成一个共享判定 `overlayPairFixOf` +
`overlayPairFixAdmits`,各自只保留本该不同的那部分。
- 为什么改:两份副本逐步相同,改其中一份时另一份会悄悄漂移;合同评审 S6 要求合并。
- 风险与回滚:纯搬移,拒绝文案、状态码、调用顺序都不变,两个关闭的既有测试原样通过;回滚即撤销本 PR。
-
先看:`packages/metadata-protocol/src/protocol-packaged-base-refusal.ts`(新判定与具名关闭)、`packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts`(无包关闭,行读取仍夹在两半之间)、`packages/metadata-protocol/src/protocol.named-base-overlay-write.test.ts`(补的三条控制用例)。
- 两分钟验证:`pnpm --filter @objectstack/metadata-protocol exec vitest run
src/protocol.named-base-overlay-write.test.ts`,预期 31/31
全绿,含三条新的「某一步失败则写入保持开放」控制用例。
- 需要你做的:无,席位落地。
## Why
The contract review of #29270 (record 6577747588, should-fix S6) found
that the two overlay-write closes judge "can the fix the refusal names
be taken" with the same four steps, written twice. A change to one copy
could drift from the other with every pin still green. This PR keeps one
copy.
## What changes
- **One predicate, two halves** (`protocol-packaged-base-refusal.ts`,
the module that owns the override write gate and both refusal
sentences):
- `overlayPairFixOf(host, base)`: the registry half. The base has a pair
id, the ensure door would answer or create the pair
(`customizationPairEnsureRefusal`), and the base records a
`manifest.version`. It answers `{ base, pair, basedOn }` or `null`.
- `overlayPairFixAdmits(host, fix, { type, name, body, member })`: the
gate half. The override write gate admits the declared body (`overrides:
{ package: base, basedOn }`) into the pair.
- It has two halves because the package-less close reads the item's
stored rows between them, and only for a name the registry has not
already left open. Folding the row read in front would change the call
order and which failure wins (a store-read failure before a registry
`null`).
- **`namedBaseCloseSubject`** keeps only its own steps: a named package,
a body carrying `overrides` stays the D2 gate's, the named package among
the shippers, and the declared body from the save (no row read).
- **`packagelessCloseSubject`** keeps only its own steps: exactly one
shipper, the item's row read, and `classifyPackagelessRow`.
- **Unchanged:** `closedOverlayWriteClauses`, both refusal sentences,
`protocol.ts` (no line touched; its two host methods and their eight
call sites are as on `main`), and the return shape `{ base, pair }`. The
new functions are internal: `src/index.ts` names its exports, and
neither is added there.
- **Changeset:** `@objectstack/metadata-protocol` patch (see Acceptance
notes for why it is not `skip-changeset`).
### Measured first, on `main` at 42ff86f
Every step of the two predicates, common or intended-different (line
numbers are `main`'s):
| Step | `namedBaseCloseSubject` (`protocol-packaged-base-refusal.ts`) |
`packagelessCloseSubject` (`protocol-packageless-overlay-migration.ts`)
| Verdict |
|:---|:---|:---|:---|
| Which write | `:394-395` a non-empty package | `:729` no package |
intended-different |
| `overrides` in the body | `:399` left to the D2 gate | (classified at
`:764`, `foreign_declaration`) | intended-different (both docblocks say
so) |
| Shippers | `:400` the named one among them | `:731-733` exactly one |
intended-different (the card) |
| Pair id | `:401-403` | `:734-736` | common |
| Registry present | `:404-405` | `:737-738` | common |
| Ensure door | `:406` | `:739` | common |
| Base version | `:407-408` reads `registry.getPackage(base)` |
`:740-741` `baseVersionOf` re-reads `engine.registry` | common. Same
value: `ObjectQL#registry` is a getter over one never-reassigned field,
and `getPackage` was already proved a function at the step above |
| Row read and classification | none | `:743-768` | intended-different
(the card) |
| Member of the declared body | `:411-415` keyed on
`isPlainRecord(item)` | `:754-756` keyed on `item !== undefined` |
spelled differently, same answer for every JSON body: for a non-record
body both read the canonical member. Left in each close, not folded, so
the fold stays move-only |
| Declared body and gate | `:416-423` | `:770-778` | common |
No step differs by accident, so the card's stop condition did not fire.
## Tests
- **No existing pin was edited.**
`protocol.named-base-overlay-write.test.ts` gained fixtures (three
packages and two shipped dashboards that no existing case addresses) and
one new top-level `describe`. The package-less close's pins
(`packages/objectql/src/protocol-packageless-overlay-migration.test.ts`,
`environment-overlay-email-locale-restart.test.ts`,
`environment-overlay-five-types.test.ts`) were not touched.
- **Step coverage before this PR:**
| Step | named close | package-less close |
|:---|:---|:---|
| pair id | none | `no_pair` case |
| ensure door | `a read-only base whose pair cannot be ensured` | `an
unensurable pair` cases |
| base version | none | none |
| gate refuses the declared body | none | the `email_template`
unshipped-locale reset |
- **Three new pins, all through the named close, all green on `main`
before the fold** (commit 0dc436c on 42ff86f), and each shown able to
fail: a write naming a read-only base whose id forms no pair, a base
that records no version while its pair is installed, and an
`email_template` at a locale the base does not ship. Each stays open, as
on `main`. One `basedOn` pin covers both closes now, because the step
lives once.
## Acceptance notes
- **The version step is not separately observable today.** Delete only
the `basedOn` check and the no-version pin stays green: the gate then
sees `overrides` with no `basedOn`, which `OverrideDeclarationSchema`
rejects, so the gate judges the body as an undeclared same name in the
pair and refuses it. The pin still pins the outcome: a fallback version
(`?? '0.0.0'`) reds it (Verification, ablation 4). The check is kept as
it was.
- **Changeset, not `skip-changeset`.** No published behaviour changes,
but the shipped `dist/` of `@objectstack/metadata-protocol` does (the
build carries the new functions). AGENTS.md keeps `skip-changeset` for a
diff that publishes nothing, so this is a `patch`. The order allowed
`skip-changeset` if no behaviour changed; AGENTS.md wins.
- The predicate guard `registry present with a getPackage function` is a
type guard reached only by an engine without a registry. It is not one
of the card's four steps and is not pinned. That is unchanged.
## Verification
<details><summary>Unit tests at ba4aaf1 (the head, main merged in at
3a856f6, after #29797's 5f99582): @objectstack/metadata-protocol
typecheck and full suite, and the package-less close's three objectql
files against the rebuilt dist</summary>
```
pnpm --workspace-concurrency=2 --filter '@objectstack/metadata-protocol...' build
VERDICT command-exit 0 (dist/index.js carries overlayPairFixOf)
pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 src/protocol.named-base-overlay-write.test.ts
Test Files 1 passed (1)
Tests 31 passed (31) (25 of this PR, with #29797's six; the three new controls among them)
VERDICT command-exit 0
pnpm --filter @objectstack/metadata-protocol typecheck && pnpm --filter @objectstack/metadata-protocol test
Test Files 311 passed | 4 skipped (315)
Tests 22320 passed | 26 skipped (22346)
VERDICT command-exit 0
pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 src/protocol-packageless-overlay-migration.test.ts src/environment-overlay-email-locale-restart.test.ts src/environment-overlay-five-types.test.ts
Test Files 3 passed (3)
Tests 105 passed (105)
VERDICT command-exit 0
```
Earlier heads: at e1c8a10 the suite read 22314/22314 and the objectql
files 105/105; at 408cc35, 22308/22308 and 105/105.
</details>
<details><summary>New pins on main first: 25/25 at 0dc436c (the pins on
42ff86f, before the fold)</summary>
```
✓ CONTROLS: every step the fix takes, failing, leaves the write naming the base open (ADR-0146 D1, D11) > a read-only base whose id forms no pair (an id ending in `.custom`) keeps the write naming it: there is no pair to name
✓ … > a read-only base that records no version, its pair installed, keeps the write naming it: there is no `basedOn` to declare
✓ … > an email template at a locale the base does not ship keeps the write naming the base: the override write gate refuses its declared body
Test Files 1 passed (1)
Tests 25 passed (25)
```
</details>
<details><summary>Ablation on main: four mutations of
namedBaseCloseSubject through scripts/ablation-replace.mjs, each landed
on disk and was restored to the HEAD blob</summary>
The file was `protocol-packaged-base-refusal.ts`. The blob was 410bf52
before, and it was 410bf52 after every restore, equal to HEAD.
1. `if (!pairId.ok) return null;` replaced with a no-op: the no-pair-id
pin goes red, 1 failed | 24 passed.
2. The `basedOn` check deleted: 25 passed. The step is masked by the
gate (see Acceptance notes).
3. The gate's `refuse` return deleted: the unshipped-locale pin goes
red, 1 failed | 24 passed.
4. `basedOn` given the fallback `?? '0.0.0'`: the no-version pin goes
red, 1 failed | 24 passed.
</details>
<details><summary>Derived gates at e1c8a10: 59 derived, 58 run green, 1
NOT MEASURED; pnpm lint exit 0. Re-run at ba4aaf1: the ratchet and text
families, all exit 0</summary>
At ba4aaf1 (the merge brings no change to this PR's four files; `git
diff --stat origin/main HEAD` still reads 4 files, +177 / -53), a
declared narrowing to the ratchet and text families:
`check:type-check-coverage`, `check:type-check-debt`,
`check:durability-log-level`, `check:doc-authoring`,
`check:closed-ceremony-text`, `check:nul-bytes`,
`check:issue-citations`, `check-empty-changeset.mjs --base origin/main`,
and eslint (`--no-inline-config`) on the three changed source files:
every one exit 0. CI runs the full set on this head.
The full union, at e1c8a10:
`node scripts/pm/dispatch-gates.mjs --commands` (change set: the four
files above, vs merge base on `origin/main`), each run with its exit
code captured before any pipe, then `--ran`:
```
Run reconciliation — 59 derived, 58 run, 1 NOT-MEASURED, 0 UNRUN.
NOT-MEASURED · DERIVED (1):
- node scripts/check-plugin-teardown-shape.mjs --self-test [exit 3 — PREREQUISITE NOT MET]
✓ dispatch-gates --ran: 59 derived famil(ies) accounted for — 58 run, 1 NOT-MEASURED
pnpm lint :: exit 0
```
NOT MEASURED, reason: that self-test's fixture is pinned to a commit
outside this shallow clone (it asks for `git fetch --unshallow`). CI
runs it on a full checkout. Every other derived family exited 0,
including `check:durability-log-level`, `check:doc-authoring`,
`check:closed-ceremony-text`, `check:type-check-coverage` and
`check:type-check-debt`.
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; `pnpm lint` passes.
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset.
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (Not touched.)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbZz2XXsYuaJz9sPuGU
---
_Generated by [Claude
Code](https://claude.ai/code/session_01JQwxbZz2XXsYuaJz9sPuGU)_
<!-- signed: domain:server#2 · os-warren ·
session_01JQwxbZz2XXsYuaJz9sPuGU -->
---------
Co-authored-by: Claude <noreply@anthropic.com>
refactor(metadata-protocol): the two overlay-write closes ask one "the fix can be taken" predicate
6
+
7
+
Clause-②: no
8
+
Metadata-protocol: none
9
+
10
+
The close of a write naming the read-only base itself and the close of the package-less write each judged, step for step, whether the fix their refusal names can be taken: the base has a pair id, the ensure door would answer or create the pair, the base records a version, and the override write gate admits the declared body into the pair. Those four steps now live once (`overlayPairFixOf` and `overlayPairFixAdmits`, internal to the package), and each close keeps only its own difference: the named close reads no row and accepts the named package among several shippers; the package-less close requires exactly one shipper and classifies the stored row. No refusal, status, code or sentence changes, and no write that was open or closed before changes side.
it('an email template at a locale the base does not ship keeps the write naming the base: the override write gate refuses its declared body',async()=>{
0 commit comments