Symptom
Staging, publishing and rolling back a metadata item all answer 200, and the audit trail records only one of the three.
- After 3 publishes and 2 rollbacks (all 200, all sent with
X-Actor), GET /api/v1/meta/<type>/<name>/audit returns Counter({'save': 5}) — no publish rows, no rollback rows.
- The 409
METADATA_CONFLICT denial is likewise absent.
Reproduced twice, on two actors. Everything else in the lifecycle passes: draft staging, the publish flip, OCC (409 with a passing control), package-wide publish-drafts atomicity, the #4463 draft→active gate, the Studio unpublished badge and Changes counter, /history carrying both publish events, and rollback flipping the served body. So the operations happen — they are simply not written down.
Root cause (source-confirmed)
packages/metadata-protocol/src/protocol.ts writes an allowed-outcome audit row at exactly two sites:
- line 10220 —
operation: 'save', outcome: 'allowed' (in the save path, commented "ADR-0010 — success audit (best-effort)")
- line 12848 —
operation: 'delete', outcome: 'allowed'
publishMetaItem and rollbackMetaItem reach recordMetadataAudit only through assertLockAllowsWrite() (called at lines 10783 and 12414). That helper records on the deny path and returns early on allow:
const refusal = evaluateLockForWrite(state.lock);
if (!refusal) return null; // ← allowed: returns before any audit write
…
await this.recordMetadataAudit({ …, outcome: 'denied', … });
So a refused publish is audited and a successful one is not — the inverse of what an audit trail is for. The same asymmetry explains the missing 409: the conflict is refused outside the lock helper, and nothing on that route writes a row either.
Stale-premise check: re-verified on objectstack origin/main (00e9196). Both allowed-outcome sites and both assertLockAllowsWrite call sites are unchanged in substance; only line numbers moved from the run's build (save 9833 → 10220, delete 12429 → 12848).
Reproduction
- Boot the showcase with writable runtime packages.
- Stage a draft (
PUT …?mode=draft), publish it, roll it back — repeat to 3 publishes and 2 rollbacks, sending X-Actor on every call. All answer 200.
GET /api/v1/meta/<type>/<name>/audit → only save rows (Counter({'save': 5})).
- Force a 409
METADATA_CONFLICT (stale If-Match), then re-read the audit — the denial is absent too.
Scope
The actor stamped on the rows that are written is a second, independent defect with a different located file — filed separately as #7749 so it survives this fix.
Source
Extracted from the QA run #7695 (framework 92f26f7, console 09987b680).
Symptom
Staging, publishing and rolling back a metadata item all answer 200, and the audit trail records only one of the three.
X-Actor),GET /api/v1/meta/<type>/<name>/auditreturnsCounter({'save': 5})— nopublishrows, norollbackrows.METADATA_CONFLICTdenial is likewise absent.Reproduced twice, on two actors. Everything else in the lifecycle passes: draft staging, the publish flip, OCC (409 with a passing control), package-wide
publish-draftsatomicity, the #4463 draft→active gate, the Studio unpublished badge and Changes counter,/historycarrying both publish events, and rollback flipping the served body. So the operations happen — they are simply not written down.Root cause (source-confirmed)
packages/metadata-protocol/src/protocol.tswrites an allowed-outcome audit row at exactly two sites:operation: 'save', outcome: 'allowed'(in the save path, commented "ADR-0010 — success audit (best-effort)")operation: 'delete', outcome: 'allowed'publishMetaItemandrollbackMetaItemreachrecordMetadataAuditonly throughassertLockAllowsWrite()(called at lines 10783 and 12414). That helper records on the deny path and returns early on allow:So a refused publish is audited and a successful one is not — the inverse of what an audit trail is for. The same asymmetry explains the missing 409: the conflict is refused outside the lock helper, and nothing on that route writes a row either.
Stale-premise check: re-verified on
objectstackorigin/main(00e9196). Both allowed-outcome sites and bothassertLockAllowsWritecall sites are unchanged in substance; only line numbers moved from the run's build (save 9833 → 10220, delete 12429 → 12848).Reproduction
PUT …?mode=draft), publish it, roll it back — repeat to 3 publishes and 2 rollbacks, sendingX-Actoron every call. All answer 200.GET /api/v1/meta/<type>/<name>/audit→ onlysaverows (Counter({'save': 5})).METADATA_CONFLICT(staleIf-Match), then re-read the audit — the denial is absent too.Scope
The actor stamped on the rows that are written is a second, independent defect with a different located file — filed separately as #7749 so it survives this fix.
Source
Extracted from the QA run #7695 (framework 92f26f7, console 09987b680).