Uh oh!
There was an error while loading. Please reload this page.
fix(metadata-protocol): a package publish refused by a lock or a 409 leaves the inner verdict's own audit row (#8594) - #8627
Conversation
…rdict's own audit row (#8594)
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 1 package(s): 3 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also reference the affected code. These are read-only:
|
Uh oh!
There was an error while loading. Please reload this page.
Fixes#8594
The defect
publishPackageDraftspromotes every draft inside ONEengine.transaction()(ADR-0067 D2).
promoteDraftForPublishruns inside that closure, and it wroteits denial audit rows there —
assertLockAllowsWrite'sitem_locked(withits
lock_statecolumn) andrecordOptimisticConflictAudit'smetadata_conflict. On a transactional engine both rolled back with the batch:the refusal is what aborted the batch, so the row describing the refusal was
destroyed by the very rollback it caused. A compliance query filtering
code = 'item_locked'found nothing for a package publish refused by a lock.#8400's
batch_abortedrow gave a refused batch a trail, but it carries thebatch's fact, not the item-level verdict's vocabulary or its lock column — which
is the gap this card names.
Premise re-verified on the merged ref, and the triage assumption measured
The card and its triage comment predate #8400's landing (
fda61e41, PR #8605).Re-checked on
origin/main@60cbf9d45dwith #8400 in: both quoted assertionsstill pass, i.e. the write is genuinely made and genuinely undone. Baseline
confirmed before building on it.
Triage's mechanism assumption — "option 1 … the single-item route already
writes outside a transaction" — is the whole argument for option 1, so it was
measured, not assumed, and it holds:
repo.promoteDraft, so it is outsideany repository transaction by construction;
repo.put's ownwithTxnhas unwound andrethrown.
That measurement is now a permanent pin, not a note —
premise: the SINGLE-ITEM publish route audits outside a transaction — its item_locked row survives, runagainst the same really-rolling-back harness. It is green before and after
this change, which is exactly what a premise pin should be. Option 1 taken as
the card describes it; no fork.
The shape
promoteDraftForPublishno longer writes those rows. Each refusal carries itsrow out as data on the thrown error, and each of the two callers records it on
its own side of its own transaction:
publishMetaItemrecords it in acatch— that route has no transaction, sothe row lands where it always effectively landed, still filed under
source: 'protocol.publishMetaItem';publishPackageDraftsrecords it from the rollback handler, outside thetransaction, re-stamped
source: 'protocol.publishPackageDrafts'(the sharedPhase-1 helper names the single-item route for both of its callers, and filing
a batch publish under that name would make the trail lie about the door used).
The helper is now ignorant of whose transaction it is inside — it cannot know,
and guessing is what broke. The placement no longer depends on the engine's
capabilities either: an engine with no
transaction()lands the same row inthe same place, pinned.
Mechanically this is two extractions plus a hand-off, so the four other routes
that audit are byte-identical to before:
assertLockAllowsWriteis nowlockWriteRefusal(the verdict as a value) plus the write, andrecordOptimisticConflictAuditis nowoptimisticConflictAuditEntryplus thewrite.
save/rollback/deletecall the unchanged wrappers.Two rows, two facts, neither replacing the other. A batch refused by a lock
now leaves
item_locked(+lock_state) and#8400'sbatch_aborted. Arefusal that reached neither gate — driver fault,
NOT_OVERRIDABLE,INVALID_METADATA— still leaves exactly the onebatch_abortedrow it leftbefore. No new
codevalue is minted: ADR-0112 D6b keepssys_metadata_audit.codea closed persisted vocabulary and the values that landwere already in it. The one new gate hit was a
'batch_aborted'comparison in atest, marked per-site with the documented
adr0112-ok: D6b, following #8400'sprecedent rather than exempting the file.
The pin inversion is the deliverable's proof, not a test that broke
protocol.package-publish-audit-rows.test.tsended its locked-item case onThat assertion was measuring this defect rather than fixing it — a true
statement about
main, not a contract. It now reads.toBe(true), deliberatelyand with the reasoning written into the file's header and beside the line. Two
count-based assertions in the same file became membership assertions on
codefor the same reason: a lock refusal now leaves two denial rows, and alength assertion would have read the fix as a regression.
An inversion alone is not enough — a "fix" that simply stopped rolling
anything back would satisfy it. So every case asserting a surviving denial also
asserts ADR-0067 D2 in the same breath:
publishedCount: 0,published: [],the already-promoted sibling still a
draftwith no active row, nosys_metadata_commitrow, noallowedrow.Reverse verification — direction predicted before running, both ways
(a) Revert the production edits, keep the tests. Predicted RED on the
deliverable assertions, GREEN on the premise. Measured: 6 failed / 9 passed.
Red exactly where predicted —
The last one is the no-
transaction()case and it is red for a differentreason than the rest, as predicted: with nothing to roll back the row already
survived pre-fix, so what fails there is its
source, not its existence. Thepremise pin and the driver-fault guard stayed green.
(b) The over-broad direction, in both of its shapes.
inTxnforced to a passthrough.Predicted: denial assertions stay GREEN, ADR-0067 D2 controls go RED.
Measured: 3 failed / 12 passed, all three
expected [ 'active' ] to deeply equal [ 'draft' ]. The controls are load-bearing.recordPendingDenialAuditforced to mint aninner-vocabulary row when no gate reached a verdict. Predicted: the
driver-fault case goes RED. Measured: 1 failed / 14 passed,
expected [ … ] to have a length of 1 but got 2.Both ablations were applied on top of a committed fix and restored with
git checkout HEAD -- packages/metadata-protocol/src/protocol.ts; nogit stashanywhere.Tests
protocol.package-publish-audit-rows.test.ts— 15 passed. Inverted pin +membership conversions, and a new
[#8594]block: the premise pin, the lockverdict in full (
code,lock_state,source, scope) with its D2 control,the
/auditread door, themetadata_conflictverdict with its D2 control,and the no-
transaction()engine.metadata_conflictcase provokes a real repositoryConflictError— arival author advancing the active row between
promoteDraft's two reads(
advanceActiveOnRead, scoped to package-scoped active reads so the pre-flightcommitItemsscan does not consume the one-shot) — not an injected error.@objectstack/metadata-protocolfull suite: 87 files / 1278 tests passed.@objectstack/metadata-protocol, i.e. downstream), rebuilt first to dodge thedist-resolution trap:
objectql203/3579,rest115/1896,runtime155/2369 — all passed.
tsc --noEmitover the package: 63 errors, exactly the DEBT ledger'sfrozen count, unchanged; zero in either file I touched.
Gates run locally:
check:error-code-casing(red first, fixed with the per-siteD6b marker, then green),
check:nul-bytes(+ agrep -naPcontrol-byteself-scan of all three files),
check:cross-package-test-inputs,check:durability-log-level,check:filter-alias-parity,check:query-options-erasure,check:type-check-coverage,check:engine-double-contract,check:empty-changeset,check:changeset-gate-self-tests,check:objectui-changeset,check-changeset-no-major,check-adr-0087-registration. The list wasre-derived against the actual changed paths with
scripts/pm/dispatch-gates.mjs; that re-derivation added the.changesetfamily (
check:objectui-changeset,check-adr-0087-registration,check-empty-changeset,check-changeset-no-major), which a pre-change gatelist could not have named.
Scope
#8595 — the pre-flight refusal (the
preflightViolationsearly return abovethe transaction, which writes nothing at all) — is a hard same-file serial and is
not addressed here; that branch is untouched. This shape does make it
follow easily: the pre-flight sits outside the transaction already, so it needs a
row written, not a row relocated, and
recordMetadataAuditis right there.That is an observation for whoever takes it, not a change in this PR.
protocol.tsregion declared on the card before the first edit:promoteDraftForPublish/publishPackageDraftsaudit emission — checkedagainst the file's other live claims (the ADR-0070 D1 gate region, and
toRowApiErroraround lines 1491-1552); no overlap with either.Generated by Claude Code
Generated by Claude Code