Repository navigation
Commit 2811a63
Fixes #23480
Clause-②: yes
## What this does
`os meta register` takes `--force`, so a CLI user can send the
acknowledgement that the save door's `409 DESTRUCTIVE_CHANGE` refusal
prescribes ("re-submit with ?force=true to proceed").
- **The flag.** `--force` is a boolean, off by default, with no short
alias, in the house shape of this CLI's other `--force` flags (`migrate
apply`, `migrate audit-metadata-bodies` and the other `migrate`
commands: `Flags.boolean({ description, default: false })`). Its help
text says it applies a save that drops or transforms existing data, and
that the server refuses such a change with `409 DESTRUCTIVE_CHANGE`
without it.
- **The mapping.** `metaRegisterOptions` maps `--force` to
`SaveMetaItemOptions.force: true` only when the flag is given. When it
is absent (oclif hands `false`), no member is added, so an unflagged run
still hands the SDK no options bag and sends the same request as before.
`--force` combines with `--package`. The empty-`--package` refusal still
runs first.
- **The refusal the user sees.** The command already passed the server's
message through verbatim (measured below), and it still does. On a
`DESTRUCTIVE_CHANGE` refusal from a run without `--force`, one CLI-side
line now names the flag:
- text output: a warning line under the error line;
- `--format json`: a new `hint` key beside `success`, `error`, `code`
and `httpStatus`, following the `hint` key the `migrate` commands
already put in their refusal envelopes (`hint: OCCUPANCY_HINT`, `hint:
'pass --yes'`).
- Every other failure is reported exactly as before, with no `hint`. A
run that did send `--force` gets no hint, because it has nothing left to
re-run with.
- The `metaRegisterOptions` docblock no longer names `force` as
deliberately unmapped. It now lists the two mapped members and says that
`ifMatch` and `mode` remain unmapped.
## Measurements (the dispatch's mechanism assumptions)
All taken in this worktree, branched from `origin/main` `1418de4c7`.
- **A1, the one mapping place:** confirmed. `metaRegisterOptions` is the
only place the command builds `SaveMetaItemOptions`, and its docblock
named `force` as unmapped ("this command declares no flag for them").
That docblock is updated.
- **A2, the SDK forwards `force` as `?force=true`:** confirmed on
`packages/client/src/index.ts`. `metaSaveQuery` runs `if (options.force)
params.set('force', 'true')`, behind a truthy guard, so `false` and
`undefined` send nothing. Both `saveItem` twins call it. On the real
door, `--force` puts `?force=true` on the wire and the protocol receives
`force: true` (pin 2 below).
- **What the command printed before this change,** from a throwaway
probe (deleted afterwards). The probe drove the BASE `register.ts`
(`1418de4c7`) through the same real-door harness: it saved `probe_obj`
with fields `title` and `note`, then re-registered it without `note`.
- `--format json`: exit 1, `{"success": false, "error":
"object/probe_obj would drop or transform existing data: Field 'note'
removed — existing data in this column will become inaccessible. —
re-submit with ?force=true to proceed.", "code": "DESTRUCTIVE_CHANGE",
"httpStatus": 409}`.
- Text: exit 1, the same message on one error line.
- `--force`: exit 2, `Nonexistent flag: --force`. The stored fields
stayed `["note", "title"]`.
- So the server's message already passed through verbatim, and the
change adds one line beside it rather than rewriting it.
- **Generated reference:** nothing to regenerate. The package commits no
oclif manifest, and `README.md` carries no flag table for this command.
No page under `content/docs/**` outside the release-owned `releases/`
tree names this command's flags.
`skills/objectstack-platform/references/operations.md` names the command
without flags.
## Where the edits sit
All inside the claim's file surface:
- `packages/cli/src/commands/meta/register.ts`: the flag, the
`metaRegisterOptions` mapping and docblock,
`DESTRUCTIVE_CHANGE_FORCE_HINT`, `metaRegisterRefusalHint`, one example,
and the hint in the `catch`.
- `packages/cli/src/commands/meta/register-force.test.ts`: new. It
copies the real-door harness of `register-package.test.ts` rather than
adding to that file, so each file keeps its own wall time under the 10 s
line.
- `.changeset/23480-cli-meta-register-force.md`: `@objectstack/cli`
`minor`, `Clause-②: yes (widening)`. It needs no ADR-0087 disposition,
because it is not a breaking changeset (`check-adr-0087-registration`:
"this PR adds no declared-breaking changeset").
There is no SDK change and no server change.
## Tests
**The pins** run through the real oclif command, the real
`ObjectStackClient`, the real `RestServer` route, the real
`ObjectStackProtocolImplementation` and real SQLite `sys_metadata`
tables. Each pin has a negative control.
1. Unflagged, a field removed: exit 1, `code: 'DESTRUCTIVE_CHANGE'`,
`httpStatus: 409`. The server's message still carries `?force=true`, and
`hint` names `--force`. The stored body is deep-equal to the one before.
No `force` reaches the protocol, and the wire search is empty. The text
output shows the same two things. This pin is the negative control for
pin 2.
2. `--force`, the same re-register: exit 0, and the stored fields are
`["title"]` (the field is dropped). The protocol receives `force: true`,
and the wire reads `?force=true`.
3. Non-destructive (a field added), with and without `--force` on two
identical items: both exit 0, and the stored bodies are identical apart
from the name. The probe sees `force` only on the flagged run.
4. A failure that is not `DESTRUCTIVE_CHANGE` (the empty-`--package`
refusal) carries no `hint`, and nothing reaches the door.
Unit cases cover the declared flag (boolean, `default: false`, no
`char`, no `allowNo`, help names `409` and `DESTRUCTIVE_CHANGE`), the
`metaRegisterOptions` mapping, and the `metaRegisterRefusalHint` guard.
The guard returns `undefined` for `--force`, for another code and for a
local error.
**Runs,** each under `scripts/pm/os-verify-lock.sh`, at `a56f6340e`:
- Dependency closure: `pnpm turbo run build
--filter='@objectstack/cli^...' --concurrency=2` gave `Tasks: 60
successful, 60 total`. Then `pnpm --filter @objectstack/cli build`
exited 0.
- `pnpm --filter @objectstack/cli typecheck`: exit 0,
`check:test-typecheck: OK`. `tsc --noEmit --listFiles` lists
`register-force.test.ts`.
- `vitest run --project unit --maxWorkers=2`: `Test Files 288 passed
(288)`, `Tests 4314 passed (4314)`.
- `vitest run --project integration` on `register-force.test.ts` and
`register-package.test.ts`: `Test Files 2 passed (2)`, `Tests 15 passed
(15)`. `register-force.test.ts` alone measured `tests 4.94s`.
- The rest of the integration tier is declared to CI. No spawn test
covers `os meta register`.
**Ablations** (one-off, no file kept), each through `node
scripts/ablation-replace.mjs` in wrap mode. The subject is imported
relatively from `src` (`./register.js`), so no build or `dist` leg
applies.
- **Forwarding.** The mutation replaced `if (flags.force === true)
options.force = true;` with `void flags.force; /* ABLATION-23480: force
forwarding removed */`.
- The anchor count went 1 to 0, and the blob went `ab3eeab33107` to
`ba60503f0913`.
- Result: `Tests 3 failed | 4 passed (7)`. The three red cases were the
mapping case, pin 2 (`expected 1 to be +0`, the 409) and pin 3
(`expected undefined to be true`).
- Pin 1 (the negative control), pin 4 and the hint guard stayed green,
as predicted.
- **Hint.** The mutation replaced ` ? DESTRUCTIVE_CHANGE_FORCE_HINT`
with ` ? undefined /* ABLATION-23480-HINT: hint removed */`.
- The anchor count went 1 to 0, and the blob went `ab3eeab33107` to
`49fccded87ff`.
- Result: `Tests 2 failed | 5 passed (7)`. The two red cases were the
hint-guard case and pin 1, as predicted.
- Both restores were proven by the tool: the blob is back to
`ab3eeab33107`, equal to HEAD, and `git diff HEAD` is empty.
## Gates
- Derived with `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` at `a56f6340e`: 64 commands. Each was run,
and its exit code was captured before any pipe.
- `--ran` reconciliation: "64 derived famil(ies) accounted for — 62 run,
2 NOT-MEASURED (2 DERIVED from a recorded exit 3)".
- NOT MEASURED, declared to CI: `check:dual-build-cjs-loads` and
`check:i18n-coverage`. Both report `PREREQUISITE NOT MET` because
packages outside this card are not built in this checkout.
- Verdict lines of note:
- `check-adr-0087-registration`: "this PR adds no declared-breaking
changeset";
- `check-empty-changeset`: "1 declaring changeset(s) added";
- `check:doc-authoring`: clean;
- `check:nul-bytes`: OK;
- `check:test-source-alias`: OK;
- `check:cross-package-test-inputs`: OK.
- `origin/main` has moved 6 commits since the base
(`96a7f434a`..`222293f60`). None of them touches
`packages/cli/src/commands/meta/`, `packages/client` or the REST save
door. The one on this test's path, `222293f60`, touches the protocol's
package-bound object save, and these pins send no `--package` on an
object. So `main` was not merged before opening, and CI runs the merge
ref.
## Acceptance notes
Observations, not filed.
- The save door's author-time rule `security-owd-unset` refuses an
object with no `sharingModel`, so the probe object here declares
`sharingModel: 'private'`. Without it the seed saves never reach the
destructive gate.
- The destructive gate is `object`-only on the protocol side. `--force`
on any other type is sent, and the save lands exactly as it would
without the flag.
- A `--force` save that the gate would otherwise have refused prints the
same success line as any save. The door's 200 answer does not say
whether the gate was bypassed, so the command cannot either.
- The real-door harness is now copied in three test files:
`delete-reset-carriers`, `register-package` and `register-force`.
Sharing it would be a refactor outside this card.
---
_Generated by [Claude
Code](https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm)_
---------
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1f3ac4f commit 2811a63
3 files changed
Lines changed: 509 additions & 9 deletions
File tree
- .changeset
- packages/cli/src/commands/meta
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
0 commit comments