Repository navigation
feat(cli): os meta register takes --force, forwarded as the SDK's force (#23480) - #23519
Conversation
The save door refuses a change that drops or transforms existing data with 409 DESTRUCTIVE_CHANGE and names ?force=true as the remedy, which this command had no way to send. --force (boolean, off by default, no short alias) maps to SaveMetaItemOptions.force only when given, and a refusal on a run without it gains one CLI-side line, as text and as `hint` in the --format json envelope, naming the flag. The server's message is passed through verbatim. Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
A destructive re-register without --force is refused 409 DESTRUCTIVE_CHANGE with the server's message verbatim, a hint naming the flag, and nothing written; with --force it sends ?force=true and lands; a non-destructive save with --force lands as one without it. Adds the @objectstack/cli minor changeset (Clause-② yes, widening). Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
The save door's author-time rule security-owd-unset refuses an object without one, so the seed saves never reached the destructive gate. Claude-Session: https://claude.ai/code/session_0121ooBYRmS8ziFXm9ai5YHm Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check6 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
Coarse fallback — 28 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin bb9105904ea68f84317de52bfd414714a5c6b8b9 && git checkout bb9105904ea68f84317de52bfd414714a5c6b8b9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 222293f60a7fff2779269b20bb4e5ce28a34c935 a56f6340e7cfcee06b114a7af4aa8c4c7644ea7c && git checkout -B drift-repro 222293f60a7fff2779269b20bb4e5ce28a34c935 && git merge --no-ff a56f6340e7cfcee06b114a7af4aa8c4c7644ea7c
node scripts/docs-audit/affected-docs.mjs --json 222293f60a7fff2779269b20bb4e5ce28a34c935 |
席位核对
Verdict: ACCEPT. Not governed (
|
Fixes #23480
Clause-②: yes
What this does
os meta registertakes--force, so a CLI user can send the acknowledgement that the save door's409 DESTRUCTIVE_CHANGErefusal prescribes ("re-submit with ?force=true to proceed").--forceis a boolean, off by default, with no short alias, in the house shape of this CLI's other--forceflags (migrate apply,migrate audit-metadata-bodiesand the othermigratecommands: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 with409 DESTRUCTIVE_CHANGEwithout it.metaRegisterOptionsmaps--forcetoSaveMetaItemOptions.force: trueonly when the flag is given. When it is absent (oclif handsfalse), no member is added, so an unflagged run still hands the SDK no options bag and sends the same request as before.--forcecombines with--package. The empty---packagerefusal still runs first.DESTRUCTIVE_CHANGErefusal from a run without--force, one CLI-side line now names the flag:--format json: a newhintkey besidesuccess,error,codeandhttpStatus, following thehintkey themigratecommands already put in their refusal envelopes (hint: OCCUPANCY_HINT,hint: 'pass --yes').hint. A run that did send--forcegets no hint, because it has nothing left to re-run with.metaRegisterOptionsdocblock no longer namesforceas deliberately unmapped. It now lists the two mapped members and says thatifMatchandmoderemain unmapped.Measurements (the dispatch's mechanism assumptions)
All taken in this worktree, branched from
origin/main1418de4c7.metaRegisterOptionsis the only place the command buildsSaveMetaItemOptions, and its docblock namedforceas unmapped ("this command declares no flag for them"). That docblock is updated.forceas?force=true: confirmed onpackages/client/src/index.ts.metaSaveQueryrunsif (options.force) params.set('force', 'true'), behind a truthy guard, sofalseandundefinedsend nothing. BothsaveItemtwins call it. On the real door,--forceputs?force=trueon the wire and the protocol receivesforce: true(pin 2 below).register.ts(1418de4c7) through the same real-door harness: it savedprobe_objwith fieldstitleandnote, then re-registered it withoutnote.--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}.--force: exit 2,Nonexistent flag: --force. The stored fields stayed["note", "title"].README.mdcarries no flag table for this command. No page undercontent/docs/**outside the release-ownedreleases/tree names this command's flags.skills/objectstack-platform/references/operations.mdnames the command without flags.Where the edits sit
All inside the claim's file surface:
packages/cli/src/commands/meta/register.ts: the flag, themetaRegisterOptionsmapping and docblock,DESTRUCTIVE_CHANGE_FORCE_HINT,metaRegisterRefusalHint, one example, and the hint in thecatch.packages/cli/src/commands/meta/register-force.test.ts: new. It copies the real-door harness ofregister-package.test.tsrather 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/climinor,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 realRestServerroute, the realObjectStackProtocolImplementationand real SQLitesys_metadatatables. Each pin has a negative control.code: 'DESTRUCTIVE_CHANGE',httpStatus: 409. The server's message still carries?force=true, andhintnames--force. The stored body is deep-equal to the one before. Noforcereaches 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.--force, the same re-register: exit 0, and the stored fields are["title"](the field is dropped). The protocol receivesforce: true, and the wire reads?force=true.--forceon two identical items: both exit 0, and the stored bodies are identical apart from the name. The probe seesforceonly on the flagged run.DESTRUCTIVE_CHANGE(the empty---packagerefusal) carries nohint, and nothing reaches the door.Unit cases cover the declared flag (boolean,
default: false, nochar, noallowNo, help names409andDESTRUCTIVE_CHANGE), themetaRegisterOptionsmapping, and themetaRegisterRefusalHintguard. The guard returnsundefinedfor--force, for another code and for a local error.Runs, each under
scripts/pm/os-verify-lock.sh, ata56f6340e:pnpm turbo run build --filter='@objectstack/cli^...' --concurrency=2gaveTasks: 60 successful, 60 total. Thenpnpm --filter @objectstack/cli buildexited 0.pnpm --filter @objectstack/cli typecheck: exit 0,check:test-typecheck: OK.tsc --noEmit --listFileslistsregister-force.test.ts.vitest run --project unit --maxWorkers=2:Test Files 288 passed (288),Tests 4314 passed (4314).vitest run --project integrationonregister-force.test.tsandregister-package.test.ts:Test Files 2 passed (2),Tests 15 passed (15).register-force.test.tsalone measuredtests 4.94s.os meta register.Ablations (one-off, no file kept), each through
node scripts/ablation-replace.mjsin wrap mode. The subject is imported relatively fromsrc(./register.js), so no build ordistleg applies.if (flags.force === true) options.force = true;withvoid flags.force; /* ABLATION-23480: force forwarding removed */.ab3eeab33107toba60503f0913.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).? DESTRUCTIVE_CHANGE_FORCE_HINTwith? undefined /* ABLATION-23480-HINT: hint removed */.ab3eeab33107to49fccded87ff.Tests 2 failed | 5 passed (7). The two red cases were the hint-guard case and pin 1, as predicted.ab3eeab33107, equal to HEAD, andgit diff HEADis empty.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackata56f6340e: 64 commands. Each was run, and its exit code was captured before any pipe.--ranreconciliation: "64 derived famil(ies) accounted for — 62 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3)".check:dual-build-cjs-loadsandcheck:i18n-coverage. Both reportPREREQUISITE NOT METbecause packages outside this card are not built in this checkout.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/mainhas moved 6 commits since the base (96a7f434a..222293f60). None of them touchespackages/cli/src/commands/meta/,packages/clientor 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--packageon an object. Somainwas not merged before opening, and CI runs the merge ref.Acceptance notes
Observations, not filed.
security-owd-unsetrefuses an object with nosharingModel, so the probe object here declaressharingModel: 'private'. Without it the seed saves never reach the destructive gate.object-only on the protocol side.--forceon any other type is sent, and the save lands exactly as it would without the flag.--forcesave 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.delete-reset-carriers,register-packageandregister-force. Sharing it would be a refactor outside this card.Generated by Claude Code