') + ')', 'gi');
if (regex.test(text)) {
found = true;
var frag = document.createDocumentFragment();
var parts = text.split(regex);
parts.forEach(function(part, i) {
if (i % 2 === 0) {
frag.appendChild(document.createTextNode(part));
} else {
var span = document.createElement('span');
span.className = 'userscript-highlight';
span.textContent = part;
frag.appendChild(span);
}
});
node.parentNode.replaceChild(frag, node);
}
});
} else if (node.nodeType === 1 && node.childNodes) { // element
var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];
if (!skipTags.includes(node.tagName)) {
Array.from(node.childNodes).forEach(highlight);
}
}
}
highlight(document.body);
// Re-highlight on dynamic content
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1 || node.nodeType === 3) highlight(node);
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); }
})();
(function(){
try {
var __m = "*";
var __re = new RegExp('^' + ".*" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Strip utm_, fbclid, gclid, etc. from all links on page
(function() {
var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',
'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',
'ref', 'ref_src', 'source', 'medium', 'campaign'];
function cleanUrl(url) {
try {
var u = new URL(url, window.location.origin);
var changed = false;
trackingParams.forEach(function(p) {
if (u.searchParams.has(p)) {
u.searchParams.delete(p);
changed = true;
}
});
return changed ? u.toString() : url;
} catch (e) {
return url;
}
}
function cleanLinks() {
document.querySelectorAll('a[href]').forEach(function(a) {
var clean = cleanUrl(a.href);
if (clean !== a.href) a.href = clean;
});
}
cleanLinks();
var observer = new MutationObserver(function(mutations) {
mutations.forEach(function(m) {
m.addedNodes.forEach(function(node) {
if (node.nodeType === 1) {
if (node.tagName === 'A') cleanLinks();
node.querySelectorAll('a[href]').forEach(function(a) {
var clean = cleanUrl(a.href);
if (clean !== a.href) a.href = clean;
});
}
});
});
});
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); }
})();
(function(){
try {
var __m = "youtube.com";
var __re = new RegExp('^' + "youtube\\.com" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Auto-enable theater mode on YouTube
(function() {
function tryTheater() {
var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]');
if (btn && !btn.classList.contains('activated')) {
btn.click();
}
}
// Try immediately
tryTheater();
// Try after navigation (SPA)
var lastUrl = location.href;
setInterval(function() {
if (location.href !== lastUrl) {
lastUrl = location.href;
setTimeout(tryTheater, 500);
}
}, 1000);
// Also try on player load
var observer = new MutationObserver(tryTheater);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); }
})();
(function(){
try {
var __m = "*";
var __re = new RegExp('^' + ".*" + ', 'i');
if (__m === '*' || __re.test(location.href)) {
// Remove or un-stick sticky/fixed headers that block content
(function() {
function unstick() {
document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) {
if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {
el.style.position = 'static';
el.style.top = 'auto';
el.style.zIndex = 'auto';
}
});
}
unstick();
var observer = new MutationObserver(unstick);
observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });
})();
}
} catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); }
})();
})();
fix(rest): `/meta/:type/:name/history` and `/diff` state the org partition they read (#13406) by os-steve · Pull Request #13756 · objectstack-ai/objectstack · GitHub
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
GET /api/v1/meta/:type/:name/history answered { events: [] }, and GET /api/v1/meta/:type/:name/diff answered an all-empty diff, for metadata whose overlay was authored org-scoped — while sys_metadata_history held the full log. Both doors named no organization.
sys_metadata_history is a per-org table. SysMetadataRepository.history() and diffMetaItem both filter organization_id by strict equality (no $or), and event_seq is declared on the object as a "Per-organization monotonic event log cursor". So a door that states no organization does not read "everything" — it reads the env partition (request.organizationId ?? null). The write door has stated the org since #8805; only these two read doors had not.
Direction is fail-closed: the caller's OWN org data was under-served. There is no cross-org read, and the controls below keep it that way.
Call-side only.packages/spec and protocol.ts are untouched — organizationId was already declared on the request contract, and request.organizationId ?? null is the legitimate expression of env scope that every correct caller depends on.
Generated by Claude Code · session session_01UngCYXF98BVpYA9hfz6NYk
⭐ The escalation gate, answered FIRST — p2 stands, no regrade
Triage made this the executor's first mandatory item: if any of these doors is a compliance / audit evidence surface, "an org cannot see its own history" stops being a functional gap. Measured answer: no. Three independent in-repo classifications agree, and all name the audit table, never the history table:
ADR-0010 §3.6, explicitly: "The audit table is independent of sys_metadata_overlay and sys_metadata_history (ADR-0008) — the latter store state, the former stores intent and provenance. Compliance reports read from the audit table." §8 repeats it: the compliance-grade trail is the audit one.
ADR-0057 §3.1 classifies the audit / compliance-ledger lifecycle class as sys_audit_log and sys_metadata_audit. sys_metadata_history appears nowhere in that ADR.
The object declarations, which is the mechanical half: sys-metadata-audit.object.ts carries lifecycle: { class: 'audit' }; sys-metadata-history.object.ts carries no lifecycle block at all and describes itself as a "Durable event log of metadata overlay changes (per-org, append-only)".
And the compliance-evidence door in this family — /audit — already forwards the org; it was scoped in #8747. So the surface the gate protects is not one of the affected doors.
⇒ Grade proposal: p2 unchanged, bug, no security label. Stated rather than chosen silently.
⭐ Premise re-measurement — symptom 3 was already delivered (a good run, not a scope cut)
The card names three doors. On origin/main today, two were open. The third — "single-item GET /meta/dashboard/:name ignores an org-scoped overlay" — is already correct, threaded by #9454 / #9727 before this branch. The uncached arm that dashboard takes (isDashboardType bypasses the cache) carries readOrganizationId, hoisted above the fork precisely so the two arms cannot disagree about scope, and dashboard is allowOrgOverride: true so the registry predicate returns the tenant.
Rather than assert that in a report, this branch pins the disputed fact next to the two doors that were genuinely open, so the falsification is auditable: the card's third symptom, RE-MEASURED on today's main → single-item dashboard read ALREADY serves the org overlay. It authors a real org-scoped overlay, asserts the persisted row carries organization_id, then asserts the door serves it.
The card's line numbers were all dead too (#13521 de-cast the history door after triage wrote its note). Everything here was located by symbol.
⭐ The design decision: organizationIdForMetaRead, not /audit's expression
The card calls /audit "zero-tradeoff prior art". It is prior art for the intent; copying its expression is wrong twice, and both halves are measured rather than argued.
1. The predicate.auditMetaItem reads with $or: [{organization_id: org}, {organization_id: null}] — a union, so a raw ctx?.tenantId there can only add rows. These two doors read with strict equality. Under strict equality a raw tenant id asks the org partition for the history of every allowOrgOverride: false type that is still runtime-writable — object, hook, page, app, dataset — because organizationIdForMetaWrite deliberately lands those env-wide under the #6190 ruling. That answers { events: [] } for them: this card's own defect, newly minted one type family over. Gating the read on the same registry predicate the write uses is what keeps the two sides incapable of drifting.
2. The spelling.HistoryMetaItemRequestSchema declares organizationId: z.string().optional() — optional plain string, not nullable, mirroring the implementation's organizationId?: string. The key is therefore spread, never organizationId: x ?? null.
Both were confirmed by ablation, with the direction predicted before running:
Ablation
Prediction
Measured
A — swap the registry predicate for a raw ctx?.tenantId on the history door
exactly one control reddens: serves a NON-overridable type's env-wide history to an org session; every org-overridable case stays green
1 failed / 19 passed, and the failure is that control and only that control
B — write ?? null on the history door instead of the omit-spread
tsc --noEmit reds naming organizationId and null; TS2322 (null not assignable to string | undefined), not TS2353 — the latter is the code for an undeclared member, and this member is declared
src/rest-server.ts(6238,25): error TS2322: Type 'string | null' is not assignable to type 'string | undefined'. — 1 error, exactly as predicted, code included
Both legs were run from a committed implementation and restored with git checkout HEAD -- against the target's ABSOLUTE path under an EXIT INT TERM trap; each mutation was proven on disk by injected/deleted occurrence counts plus a git hash-object comparison against the HEAD blob, and each restore proven the same way plus an empty git diff HEAD. Neither leg needed a rebuild: the pins import ./rest-server.js inside their own package, so the mutation reaches source directly — and ablation A changing exactly one verdict is itself the proof that it did.
⚠️ Worth recording for the next author: the diff door reaches diffMetaItem through (p as any), so the compiler checks nothing about that literal. There, ?? null type-checks and is a silent runtime no-op (null ?? null is null) — a fix-shaped non-fix. The two doors are not identical and were measured separately.
⭐ The mandatory answer, BY ENUMERATION
Every meta read door in rest-server.ts, against /audit's parameter passing. Located by symbol; line numbers omitted deliberately because this file changed three times in a day.
Door (GET)
Forwards the caller's org?
How / why
/meta/types
n/a
registry type listing; reads no metadata document
/meta/diagnostics
❌ NO — real gap
getMetaDiagnostics declares organizationId and passes it into getMetaItems; the door supplies none. Out of scope → #13753
So the enumeration found two doors beyond this card's scope with the identical omission. They are filed unassigned as #13753, not fixed here. The references half is the sharper one: it backs the admin "Used by" panel whose empty case reads "Nothing in the metadata graph points at this item. Safe to delete.", shown to an operator about to delete — the exact false-negative class that door's own 501 refusal was added to prevent.
Tests — positive controls, not just green
packages/rest/src/rest-server-meta-history-diff-org-scope.test.ts drives real REST routes against a real ObjectStackProtocolImplementation over a stub engine, one boot, write-then-read round trips.
The load-bearing difference from the sibling rest-server-meta-read-org-scope.test.ts: its stub returns every sys_metadata_history row unfiltered, so over that engine both doors pass with or without this change — there is no partition to miss. This harness honours the where, organization_id included.
Every read assertion is gated on a fixture proof first (historyRowsFor), because "the read is org-scoped" is worthless if the fixture never created an org-scoped row — the card's own repro bar was "confirm the pg rows exist before hitting the read door":
fixture first — for all five allowOrgOverride: true types, a PUT under an active org appends exactly 1 history row in the org partition and 0 env-wide. Both halves asserted: the second is why an org-blind door missed them.
/history — two authored revisions come back as two events, version[1, 2].
/diff?from=1&to=2 — resolves both org revisions and reports the real change ({ path: 'label', from, to }), not the card's echoed-bounds/empty-buckets shape.
the env-scoped control — the same harness still serves env-scoped rows to an env-scoped caller: an org-less session writes and reads its own history unchanged.
the registry control (ablation A's target) — an object write under an active org lands env-wide, and the org session still reads it.
cross-tenant controls — org B is served neither org A's change log nor a diff of its revisions; an org-less caller is served neither.
Commands, all at 0df058f2 (the implementation commit; see the patch round below for d73a967d)
Command
Verdict line
pnpm --filter @objectstack/rest test
Test Files 164 passed (164) · Tests 2760 passed (2760)
pnpm --filter @objectstack/rest typecheck
exit 0 — check:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.json
pnpm --filter @objectstack/dogfood exec vitest run --shard=1/3
Test Files 43 passed (43) · Tests 313 passed (313)
pnpm --filter @objectstack/dogfood exec vitest run --shard=2/3
The typecheck really does cover the new test file, rather than excluding it: tsc -p tsconfig.test.json --listFiles counts it 1, and the package's main program counts it 0.
Not measured, reported separately from the passes — pnpm --filter '@objectstack/rest^...' build fails on @objectstack/verify, and it is a filter artefact, not a defect in this diff: @objectstack/verify depends on @objectstack/rest, and the ^ excludes rest itself from its own dependency closure, so verify's dts build can never resolve it under that spelling. Building as CI does — turbo run build --filter='./packages/*' --filter='./packages/*/*' — is 70/70 tasks successful. The first dogfood attempt on the unbuilt tree was PREREQUISITE NOT MET (Failed to resolve entry for package "@objectstack/runtime"), not a red; the shards above are the runs on the built tree.
Ratchets moved, and why
execctx-consumer-census.test.ts — two new resolveExecCtx sites, both locally caught on the continuation line: 73 → 75 sites, 92 → 95 mentions (+2 calls and +1 prose mention — the two numbers move by different amounts on purpose), 20 → 22 caught, 16 same-line and 53 bare unchanged. Counts re-derived from the tree, not hand-edited.
content/docs/permissions/system-context.mdx — re-anchored by regeneration (check-system-context-census.mjs --fix), never by hand, twice: once for the implementation (:6382→:6432, :6575→:6625) and again for the comment-only patch round below (:6432→:6450, :6625→:6643). Each run rewrote exactly 2 anchors, matching the 2 it flagged — so unlike the sibling that found 13 silently stale extras, there was no hidden staleness in either pass.
scripts/engine-double-contract.pinned.json — 3 new pinned rows for the new file's double (delete, findOne, update), all routed through the assertEngine*Dispatch helpers; registered with --write as the gate's own message prescribes, and the DEBT baseline did not grow.
The new stub double refuses any WHERE combinator other than $or rather than reading it as a field name — check:where-matcher executes an $and battery against every discovered matcher, and refusing loudly is the convention 201 of the 320 already follow.
Patch round — d73a967d, comment-only
PM review caught a real defect in the written record: two comments I landed named TS2353 where the measured code is TS2322.
organizationIdis declared on HistoryMetaItemRequestSchema, so ?? null there is an assignability failure (Type 'string | null' is not assignable to type 'string | undefined'), not an unknown-property one. TS2353 is the undeclared-member code. My own ablation leg B predicted TS2322, reasoned about why TS2353 would be wrong, and measured TS2322 — and then the comments said TS2353 anyway.
Mechanism, named in the fix so the next reader does not repeat it: the pre-existing paragraph nine lines above the history door's org comment correctly says "an undeclared member here is now TS2353" — right in its context, which is about undeclared members. Mine sits directly beneath it and describes a declared member. Comment drift by adjacency.
Both sites now say TS2322, and the test-file header keeps the two doors explicitly apart, because this is the half that matters most: /history reddens with TS2322; /diff reddens with NOTHING. The diff door reaches diffMetaItem through (p as any), so ?? null type-checks there and is a silent runtime no-op — the guard is weakest exactly where "the compiler catches this" is easiest to assume. A reader who trusted "TS2353" and went looking for unknown-property behaviour would have concluded the opposite.
Re-run on d73a967d — scoped to what a comment edit can actually disturb, deliberately not the whole union:
Command
Verdict
pnpm lint (repo-wide)
exit 0, no findings
node scripts/check-system-context-census.mjs
rotted as predicted (the added comment lines shifted rest-server.ts), re-anchored by --fix, then OK — 145 anchors resolve
pnpm check:doc-authoring
exit 0
pnpm check:doc-anchors
exit 0
pnpm check:nul-bytes
exit 0
execctx-consumer-census counts, re-derived from the patched file
75 / 95 / 22 / 16 / 53 — all five unchanged, because the new prose contains no resolveExecCtx occurrence. Measured, not assumed: a mention count is exactly the ratchet prose can move
Deliberately NOT re-run, and why: the @objectstack/rest suite, the three dogfood shards, check:type-check-debt, and the rest of the path-derived union. The diff is comment-only — verified mechanically, every changed line in both TS files begins with // — so no test behaviour, no type, and no gate population changes. The one gate a comment edit did disturb (the census, via line-number shift) was caught and repaired.
⚠️ Platform reading taken on this PR: a body PATCH DELETES the attribution footer outright
Recorded because the standing note says a PATCH merely downgrades the session-URL footer to the bare form, and that the bare form then survives. Measured here, two trials on this body:
CREATE kept it, in session-URL form — read back verbatim.
PATCH (the update carrying this section) removed the entire trailing block — both the --- rule and the _Generated by …_ line. Read back: _Generated by occurrences 0, claude.ai/code occurrences 0. Not a downgrade; a deletion.
Mitigation, and it held: the session id was deliberately also written into prose before that PATCH, and it survived at 1 occurrence. The footer above has been moved out of the trailing position into the header for the same reason. ⇒ Never rely on a trailing footer for attribution across an edited body; put the session id in the body text.
Reading requested by review — the sibling harness, measured
Does rest-server-meta-read-org-scope.test.ts assert anything about org scoping of /history or /diff today? No. Its drive helper exposes exactly three routes and its tests reach no others:
PUT /meta/:type/:name
GET /meta/:type/:name
GET /meta/:type
Every occurrence of "history" in that file is stub-engine plumbing for sys_metadata_history (the table SysMetadataRepository.put() appends to during the PUT — without it the write throws). No /history or /diff route is registered, driven, or asserted on. ⇒ No vacuous claim exists there today. Its name is broader than its content, but every assertion it makes is about the single-item and list read doors, and its stub genuinely keeps all of them.
Is the stub nonetheless a trap for anyone who later adds one? Yes — and on both seams:
asyncfindOne(table,opts){assertEngineFindOnePredicate(table,opts);if(table==='sys_metadata_history')returnnull;// where DISCARDED
...
asyncfind(table,opts){if(table==='sys_metadata_history')returnhistoryRows;// where DISCARDED
Both discard opts.where for that table. historyMetaItem (via repo.history()) and diffMetaItem both filter organization_id by strict equality, so over this stub that filter is a no-op: every history row comes back whichever partition was asked for. An org-scoping assertion for /history or /diff added to that file would therefore pass with or without the org being forwarded — vacuously green. That is precisely why the new file carries its own partitioned stub rather than extending this one.
What this does not do
⛔ No packages/spec edit is owed and none is made. The spec anticipated this card by name: HistoryMetaItemRequestSchema's describe text already records that the door "currently sends no organization at all (whether it should is a tenant-scoping question measured separately for that door — declaring the member records the implementation contract, it does not answer that question)", and protocol.test.ts calls it "the #8747-family measurement the card fences to a future issue". This is that issue. Clause-② is no.
⛔ protocol.ts is unmodified. ⛔ #13753 is not addressed here and remains open.
⚠️1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files. Nothing else in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 1 changed package(s)).
What this run could not see
1 changed file(s) yielded no anchor (packages/rest/src/rest-server.ts) — pages documenting those are invisible to this run
a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json c42bc8ee68833297f0273f14b0f6f6e9357ab293 → packageMentionDocs.
Merge ordering — this PR goes SECOND, behind #13763
Both PRs touch content/docs/permissions/system-context.mdx. Ruling ① makes same-file hard-serial with no region exemption, and #13763 (priority:p1) goes first. Recorded here rather than in the body so the note is additive and the reviewed text is untouched.
⭐ The part that matters for whoever merges this one: that file is NOT text-merged
.gitattributes:109 registers it merge=os-regen, and the driver (merge.os-regen.driver → scripts/git-merge-regen.mjs) defers instead of merging. Measured with a simulated merge that touches no worktree — git merge-tree --write-tree against #13763's head exits 0 for the tree, and emits for this file:
⟳ content/docs/permissions/system-context.mdx
not text-merged — it is generated. Regenerate from the merged tree:
pnpm gen:system-context-census
The pre-commit hook will not let this commit through until you do.
⇒ After #13763 lands, do not hand-resolve this file and do not reason about line ranges. The sequence is:
Merge main, and commit the merge first. Regenerating while the merge is still uncommitted reads the pre-merge tip and silently rolls the anchors back to the old fork point — a well-formed, gate-green, wrong answer.
Run pnpm gen:system-context-census (which ischeck-system-context-census.mjs --fix).
That third step is why "regenerate with --fix" and "don't blindly re-run --fix" are not in conflict: the driver requires the regeneration, and the discipline is on the word blindly.
The two PRs are independent in both dimensions anyway
Neither PR's source edit shifts the file the other cites, so neither invalidates the other's anchor values. The collision is nominal — same artifact, disjoint content. (Note the cited files: not http-dispatcher.ts, and never rest-server.ts.)
State at ddcf40f195
origin/main merged — 8 commits, no conflicts, merge committed before anything else ran. #13763 had not landed at that point, and system-context.mdx had not moved on main since this branch's base, so there was nothing for the driver to reconcile.
The census check was re-run without--fix, deliberately: green, 145 anchors resolve. The 2 anchors did not move. They stand at :6450 and :6643 — the values after this PR's comment-only patch round, which advanced them from the :6432/:6625 an earlier note cites.
Re-verified on the merged head, since the merge pulled in 38 files of other people's work:
Gate union re-derived on ddcf40f195: 53 → 62 families, 9 newly named, none dropped. All nine run green — check:corpus-claim-drift (new, arrived in this merge), check:merge-driver, check:entry-guard, check:parse-guard, check:bash32-floor, check:pnpm-filter-targets, check:cli-command-ids, check:agent-test-spelling, check:watch-hint-literal.
Ratchet family green: census, check:engine-double-contract, check:where-matcher ("baseline key set verified against adf4bf4: no files added"), check:nul-bytes (7579 files), check:cross-package-test-inputs.
execctx-consumer-census counts re-derived from the merged tree, not assumed: 75 / 95 / 22 / 16 / 53 — all five still at their pins.
pnpm lint repo-wide on the merged tree: exit 0, no findings.
Not re-run, and named as such: the @objectstack/rest suite, the three dogfood shards, and check:type-check-debt. This branch's own diff is unchanged since 0df058f2 — the merge added no line to any file it touches — and CI runs those on the merged head regardless.
⛔ Still draft. Nothing flipped ready, nothing armed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes#13406
GET /api/v1/meta/:type/:name/historyanswered{ events: [] }, andGET /api/v1/meta/:type/:name/diffanswered an all-empty diff, for metadata whose overlay was authored org-scoped — whilesys_metadata_historyheld the full log. Both doors named no organization.sys_metadata_historyis a per-org table.SysMetadataRepository.history()anddiffMetaItemboth filterorganization_idby strict equality (no$or), andevent_seqis declared on the object as a "Per-organization monotonic event log cursor". So a door that states no organization does not read "everything" — it reads the env partition (request.organizationId ?? null). The write door has stated the org since #8805; only these two read doors had not.Direction is fail-closed: the caller's OWN org data was under-served. There is no cross-org read, and the controls below keep it that way.
Call-side only.
packages/specandprotocol.tsare untouched —organizationIdwas already declared on the request contract, andrequest.organizationId ?? nullis the legitimate expression of env scope that every correct caller depends on.Generated by Claude Code · session
session_01UngCYXF98BVpYA9hfz6NYk⭐ The escalation gate, answered FIRST — p2 stands, no regrade
Triage made this the executor's first mandatory item: if any of these doors is a compliance / audit evidence surface, "an org cannot see its own history" stops being a functional gap. Measured answer: no. Three independent in-repo classifications agree, and all name the audit table, never the history table:
sys_metadata_overlayandsys_metadata_history(ADR-0008) — the latter store state, the former stores intent and provenance. Compliance reports read from the audit table." §8 repeats it: the compliance-grade trail is the audit one.audit/ compliance-ledger lifecycle class assys_audit_logandsys_metadata_audit.sys_metadata_historyappears nowhere in that ADR.sys-metadata-audit.object.tscarrieslifecycle: { class: 'audit' };sys-metadata-history.object.tscarries nolifecycleblock at all and describes itself as a "Durable event log of metadata overlay changes (per-org, append-only)".And the compliance-evidence door in this family —
/audit— already forwards the org; it was scoped in #8747. So the surface the gate protects is not one of the affected doors.⇒ Grade proposal: p2 unchanged,
bug, nosecuritylabel. Stated rather than chosen silently.⭐ Premise re-measurement — symptom 3 was already delivered (a good run, not a scope cut)
The card names three doors. On
origin/maintoday, two were open. The third — "single-itemGET /meta/dashboard/:nameignores an org-scoped overlay" — is already correct, threaded by #9454 / #9727 before this branch. The uncached arm thatdashboardtakes (isDashboardTypebypasses the cache) carriesreadOrganizationId, hoisted above the fork precisely so the two arms cannot disagree about scope, anddashboardisallowOrgOverride: trueso the registry predicate returns the tenant.Rather than assert that in a report, this branch pins the disputed fact next to the two doors that were genuinely open, so the falsification is auditable:
the card's third symptom, RE-MEASURED on today's main→single-item dashboard read ALREADY serves the org overlay. It authors a real org-scoped overlay, asserts the persisted row carriesorganization_id, then asserts the door serves it.The card's line numbers were all dead too (
#13521de-cast the history door after triage wrote its note). Everything here was located by symbol.⭐ The design decision:
organizationIdForMetaRead, not/audit's expressionThe card calls
/audit"zero-tradeoff prior art". It is prior art for the intent; copying its expression is wrong twice, and both halves are measured rather than argued.1. The predicate.
auditMetaItemreads with$or: [{organization_id: org}, {organization_id: null}]— a union, so a rawctx?.tenantIdthere can only add rows. These two doors read with strict equality. Under strict equality a raw tenant id asks the org partition for the history of everyallowOrgOverride: falsetype that is still runtime-writable —object,hook,page,app,dataset— becauseorganizationIdForMetaWritedeliberately lands those env-wide under the #6190 ruling. That answers{ events: [] }for them: this card's own defect, newly minted one type family over. Gating the read on the same registry predicate the write uses is what keeps the two sides incapable of drifting.2. The spelling.
HistoryMetaItemRequestSchemadeclaresorganizationId: z.string().optional()— optional plainstring, not nullable, mirroring the implementation'sorganizationId?: string. The key is therefore spread, neverorganizationId: x ?? null.Both were confirmed by ablation, with the direction predicted before running:
ctx?.tenantIdon the history doorserves a NON-overridable type's env-wide history to an org session; every org-overridable case stays green?? nullon the history door instead of the omit-spreadtsc --noEmitreds namingorganizationIdandnull; TS2322 (null not assignable tostring | undefined), not TS2353 — the latter is the code for an undeclared member, and this member is declaredsrc/rest-server.ts(6238,25): error TS2322: Type 'string | null' is not assignable to type 'string | undefined'.— 1 error, exactly as predicted, code includedBoth legs were run from a committed implementation and restored with
git checkout HEAD --against the target's ABSOLUTE path under anEXIT INT TERMtrap; each mutation was proven on disk by injected/deleted occurrence counts plus agit hash-objectcomparison against the HEAD blob, and each restore proven the same way plus an emptygit diff HEAD. Neither leg needed a rebuild: the pins import./rest-server.jsinside their own package, so the mutation reaches source directly — and ablation A changing exactly one verdict is itself the proof that it did.diffMetaItemthrough(p as any), so the compiler checks nothing about that literal. There,?? nulltype-checks and is a silent runtime no-op (null ?? nullisnull) — a fix-shaped non-fix. The two doors are not identical and were measured separately.⭐ The mandatory answer, BY ENUMERATION
Every meta read door in
rest-server.ts, against/audit's parameter passing. Located by symbol; line numbers omitted deliberately because this file changed three times in a day./meta/types/meta/diagnosticsgetMetaDiagnosticsdeclaresorganizationIdand passes it intogetMetaItems; the door supplies none. Out of scope → #13753/meta/_draftsorganizationId: ctx?.tenantId ?? undefined/meta/:type(list)organizationIdForMetaRead(canonicalMetaUrlType(type), ctx?.tenantId)(#9454)/meta/:type/:name/referencesfindReferencesToMetadeclaresorganizationIdand spreads it intogetMetaItems; the door supplies none. Out of scope → #13753/meta/:type/:name/layers/meta/book/:name/treebookanddocareallowOrgOverride: false; naming an org would resurrect #6190's phantom rows/meta/:type/:name(single item)/meta/:type/:name/historyorganizationIdForMetaRead, omit-spread/meta/:type/:name/auditorganizationId: ctx?.tenantId ?? null(#8747)/meta/:type/:name/difforganizationIdForMetaRead, omit-spread/meta/object/:name/state/:fieldobjectisallowOrgOverride: false/meta/:type/:name/published...(ctx?.tenantId ? { organizationId: ctx.tenantId } : {})So the enumeration found two doors beyond this card's scope with the identical omission. They are filed unassigned as #13753, not fixed here. The
referenceshalf is the sharper one: it backs the admin "Used by" panel whose empty case reads "Nothing in the metadata graph points at this item. Safe to delete.", shown to an operator about to delete — the exact false-negative class that door's own 501 refusal was added to prevent.Tests — positive controls, not just green
packages/rest/src/rest-server-meta-history-diff-org-scope.test.tsdrives real REST routes against a realObjectStackProtocolImplementationover a stub engine, one boot, write-then-read round trips.The load-bearing difference from the sibling
rest-server-meta-read-org-scope.test.ts: its stub returns everysys_metadata_historyrow unfiltered, so over that engine both doors pass with or without this change — there is no partition to miss. This harness honours thewhere,organization_idincluded.Every read assertion is gated on a fixture proof first (
historyRowsFor), because "the read is org-scoped" is worthless if the fixture never created an org-scoped row — the card's own repro bar was "confirm the pg rows exist before hitting the read door":allowOrgOverride: truetypes, aPUTunder an active org appends exactly 1 history row in the org partition and 0 env-wide. Both halves asserted: the second is why an org-blind door missed them./history— two authored revisions come back as two events,version[1, 2]./diff?from=1&to=2— resolves both org revisions and reports the real change ({ path: 'label', from, to }), not the card's echoed-bounds/empty-buckets shape.objectwrite under an active org lands env-wide, and the org session still reads it.Commands, all at
0df058f2(the implementation commit; see the patch round below ford73a967d)pnpm --filter @objectstack/rest testTest Files 164 passed (164)·Tests 2760 passed (2760)pnpm --filter @objectstack/rest typecheckcheck:test-typecheck: OK — @objectstack/rest's test layer compiles under packages/rest/tsconfig.test.jsonpnpm --filter @objectstack/dogfood exec vitest run --shard=1/3Test Files 43 passed (43)·Tests 313 passed (313)pnpm --filter @objectstack/dogfood exec vitest run --shard=2/3Test Files 43 passed (43)·Tests 288 passed | 1 skipped (289)pnpm --filter @objectstack/dogfood exec vitest run --shard=3/3Test Files 42 passed | 1 skipped (43)·Tests 379 passed | 2 skipped (381)pnpm lint(repo-wide,eslint . --no-inline-config)node scripts/check-system-context-census.mjsOK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-readpnpm check:engine-double-contractOK — 727 pinned, 134 in the DEBT ledger, 3 exemptpnpm check:where-matcher320 matcher(s) discovered, 320 answer the combinator battery correctly or refuse it loudly (201 refuse)pnpm check:type-check-debtOK — 29 ledger entr(ies) re-measured in 255.0s, 1542 raw tsc error(s) total, none above its recorded numberpnpm check:authz-resolver·check:route-envelope·check:nul-bytes·check:cross-package-test-inputs·check:query-options-erasure·check:type-check-coverage·check:dispatcher-error-vocabulary·check:test-source-aliasThe typecheck really does cover the new test file, rather than excluding it:
tsc -p tsconfig.test.json --listFilescounts it 1, and the package's main program counts it 0.Not measured, reported separately from the passes —
pnpm --filter '@objectstack/rest^...' buildfails on@objectstack/verify, and it is a filter artefact, not a defect in this diff:@objectstack/verifydepends on@objectstack/rest, and the^excludesrestitself from its own dependency closure, soverify's dts build can never resolve it under that spelling. Building as CI does —turbo run build --filter='./packages/*' --filter='./packages/*/*'— is 70/70 tasks successful. The first dogfood attempt on the unbuilt tree wasPREREQUISITE NOT MET(Failed to resolve entry for package "@objectstack/runtime"), not a red; the shards above are the runs on the built tree.Ratchets moved, and why
execctx-consumer-census.test.ts— two newresolveExecCtxsites, both locally caught on the continuation line:73 → 75sites,92 → 95mentions (+2calls and+1prose mention — the two numbers move by different amounts on purpose),20 → 22caught,16same-line and53bare unchanged. Counts re-derived from the tree, not hand-edited.content/docs/permissions/system-context.mdx— re-anchored by regeneration (check-system-context-census.mjs --fix), never by hand, twice: once for the implementation (:6382→:6432,:6575→:6625) and again for the comment-only patch round below (:6432→:6450,:6625→:6643). Each run rewrote exactly 2 anchors, matching the 2 it flagged — so unlike the sibling that found 13 silently stale extras, there was no hidden staleness in either pass.scripts/engine-double-contract.pinned.json— 3 new pinned rows for the new file's double (delete,findOne,update), all routed through theassertEngine*Dispatchhelpers; registered with--writeas the gate's own message prescribes, and the DEBT baseline did not grow.$orrather than reading it as a field name —check:where-matcherexecutes an$andbattery against every discovered matcher, and refusing loudly is the convention 201 of the 320 already follow.Patch round —
d73a967d, comment-onlyPM review caught a real defect in the written record: two comments I landed named TS2353 where the measured code is TS2322.
organizationIdis declared onHistoryMetaItemRequestSchema, so?? nullthere is an assignability failure (Type 'string | null' is not assignable to type 'string | undefined'), not an unknown-property one. TS2353 is the undeclared-member code. My own ablation leg B predicted TS2322, reasoned about why TS2353 would be wrong, and measured TS2322 — and then the comments said TS2353 anyway.Mechanism, named in the fix so the next reader does not repeat it: the pre-existing paragraph nine lines above the history door's org comment correctly says "an undeclared member here is now TS2353" — right in its context, which is about undeclared members. Mine sits directly beneath it and describes a declared member. Comment drift by adjacency.
Both sites now say TS2322, and the test-file header keeps the two doors explicitly apart, because this is the half that matters most:
/historyreddens with TS2322;/diffreddens with NOTHING. The diff door reachesdiffMetaItemthrough(p as any), so?? nulltype-checks there and is a silent runtime no-op — the guard is weakest exactly where "the compiler catches this" is easiest to assume. A reader who trusted "TS2353" and went looking for unknown-property behaviour would have concluded the opposite.Re-run on
d73a967d— scoped to what a comment edit can actually disturb, deliberately not the whole union:pnpm lint(repo-wide)node scripts/check-system-context-census.mjsrest-server.ts), re-anchored by--fix, thenOK — 145 anchors resolvepnpm check:doc-authoringpnpm check:doc-anchorspnpm check:nul-bytesexecctx-consumer-censuscounts, re-derived from the patched file75 / 95 / 22 / 16 / 53— all five unchanged, because the new prose contains noresolveExecCtxoccurrence. Measured, not assumed: a mention count is exactly the ratchet prose can moveDeliberately NOT re-run, and why: the
@objectstack/restsuite, the three dogfood shards,check:type-check-debt, and the rest of the path-derived union. The diff is comment-only — verified mechanically, every changed line in both TS files begins with//— so no test behaviour, no type, and no gate population changes. The one gate a comment edit did disturb (the census, via line-number shift) was caught and repaired.Recorded because the standing note says a PATCH merely downgrades the session-URL footer to the bare form, and that the bare form then survives. Measured here, two trials on this body:
---rule and the_Generated by …_line. Read back:_Generated byoccurrences 0,claude.ai/codeoccurrences 0. Not a downgrade; a deletion.Mitigation, and it held: the session id was deliberately also written into prose before that PATCH, and it survived at 1 occurrence. The footer above has been moved out of the trailing position into the header for the same reason. ⇒ Never rely on a trailing footer for attribution across an edited body; put the session id in the body text.
Reading requested by review — the sibling harness, measured
Does
rest-server-meta-read-org-scope.test.tsassert anything about org scoping of/historyor/difftoday? No. Itsdrivehelper exposes exactly three routes and its tests reach no others:PUT /meta/:type/:nameGET /meta/:type/:nameGET /meta/:typeEvery occurrence of "history" in that file is stub-engine plumbing for
sys_metadata_history(the tableSysMetadataRepository.put()appends to during the PUT — without it the write throws). No/historyor/diffroute is registered, driven, or asserted on. ⇒ No vacuous claim exists there today. Its name is broader than its content, but every assertion it makes is about the single-item and list read doors, and its stub genuinely keeps all of them.Is the stub nonetheless a trap for anyone who later adds one? Yes — and on both seams:
Both discard
opts.wherefor that table.historyMetaItem(viarepo.history()) anddiffMetaItemboth filterorganization_idby strict equality, so over this stub that filter is a no-op: every history row comes back whichever partition was asked for. An org-scoping assertion for/historyor/diffadded to that file would therefore pass with or without the org being forwarded — vacuously green. That is precisely why the new file carries its own partitioned stub rather than extending this one.What this does not do
⛔ No
packages/specedit is owed and none is made. The spec anticipated this card by name:HistoryMetaItemRequestSchema's describe text already records that the door "currently sends no organization at all (whether it should is a tenant-scoping question measured separately for that door — declaring the member records the implementation contract, it does not answer that question)", andprotocol.test.tscalls it "the #8747-family measurement the card fences to a future issue". This is that issue. Clause-② is no.⛔
protocol.tsis unmodified. ⛔ #13753 is not addressed here and remains open.