fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL) - #14322

Merged
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native
Sep 2, 2026
Merged

fix(service-analytics): one verdict for a vacating read scope on all three analytics faces (echo + native SQL)#14322
os-sales merged 1 commit into
mainfrom
claude/issue-13926-read-scope-vacancy-echo-native

Conversation

@claude

@claudeclaudeBot commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes#13926

What this closes

#13640 guarded the ObjectQL ENGINE merges (withReadScope, resolveFkAttr) with assertReadScopeCannotVacate, and its dispatch deliberately did not move the two routes that consume compileScopedFilterToSql. That left one read scope with two answers on one strategy: ObjectQLStrategy.execute() REFUSED the $not-over-$in: [] family while ObjectQLStrategy.generateSql() — the /analytics/sql echo — still COMPILED it to a predicate admitting every row, and NativeSQLStrategy.applyReadScope built a real, executed WHERE from the same compiler.

The fix is the PM-suggested route, confirmed by measurement: the SAME exported guard, called at the two remaining getReadScope merge sites. Zero compiler change (see "What did NOT move").

Measured, not read from the compiler (the card asked)

The card flagged its row consequence as derived from the emitted predicate. read-scope-vacancy-three-faces.test.ts measures it end-to-end on one real engine (driver-sqlite-wasm over driver-sql; three fixture rows, NULL owner on r3), driving all three faces and EXECUTING the echo SQL rather than string-matching it. Pre-fix, on this harness:

scopeexecute()echo (SQL executed)native execute()
{ owner: { $nin: [] } }refused (guard)refused (compiler #13571 arm)refused (compiler #13571 arm)
{ $not: { owner: { $in: [] } } }refusedALL THREE ROWSALL THREE ROWS
{ $not: { owner: [] } }refusedrefused (bare-array arm)refused (bare-array arm)
{ $not: { owner: { $in: [], $ne: 'u_other' } } }refusedALL THREE ROWSALL THREE ROWS
{ $or: [ { $not: { owner: { $in: [] } } }, { owner: 'u_me' } ] }refusedALL THREE ROWSALL THREE ROWS
{ $not: { $not: { owner: { $nin: [] } } } }refusedrefused (compiler arm)refused (compiler arm)

Post-fix, all six spellings refuse on all three faces in the module's one envelope — READ_SCOPE_COMPILE_FAILED / 500 — asserted by code plus status in every refusal case, never a bare throw assertion.

The PM's question: latent execution leak, or echo truthfulness?

Both, split by route. NativeSQLStrategy.execute() runs generateSql()'s output through ctx.executeRawSql — real rows, with no other read-scope door in front of it. So the native half was a LATENT EXECUTION LEAK for any out-of-repo getReadScope producer (the spec contract in packages/spec/src/contracts/analytics-service.ts exists exactly for those), held shut in-repo only by #13570's producer-side polarity guard. The ObjectQL half was an echo-truthfulness defect only: execute() refuses at withReadScope before the engine, and the echo is a display string. The pre-fix native rows above are the leak measured, not inferred.

Also verified while counting doors: the third getReadScope reader in the package, NativeSQLStrategy.crossFieldComparisonIn, is a read-only routing probe (nothing it reads reaches a compiler or an engine), and analytics-service.ts's callCtx wrapper only pre-resolves scopes into the same StrategyContext the strategies consume. The card's count of exactly two unguarded merge sites holds.

Where the guard stands — AFTER the compiler, deliberately

At both new sites the guard runs after compileScopedFilterToSql returns. The shapes the compiler already refuses ($nin: [] at any depth, the bare-array comparand) keep their #13571 refusal messages on these routes — read-scope-empty-nin-refusal.test.ts's end-to-end message pin is green UNEDITED — so each door stays distinguishable in the operator's log (the property #13640's review called out), and the guard closes exactly the shapes that compile-but-vacate. Same verdict everywhere; door-specific messages preserved.

What did NOT move (controls)

Ablation (direction predicted before the run)

Fix committed first. Mutation replaced the two guard call lines with markers, confirmed ON DISK before measuring (per file: anchor grep count 1 to 0, marker grep count 0 to 1; editor exit codes not used as evidence). No rebuild leg is owed: the suites import the mutated sources by relative path inside their own package, so vitest transforms src directly and no dist is in the resolution path. Result matched the prediction exactly: 5 tests reddened — the three compiling spellings, the guard-message pin, and the joined-object door, with echo and native again admitting all three rows — while the other 34, including every control and the whole #13924 suite, stayed green. Restore proven by STATE, not exit code: git checkout HEAD -- with absolute paths under an EXIT/INT/TERM trap, then git diff HEAD empty, blob hashes equal to the HEAD blobs (verified non-empty before mutating), zero markers left.

Verification (all on c8ac3e7, the head of this branch)

  • pnpm --filter @objectstack/service-analytics test — Test Files 87 passed (87), Tests 1871 passed (1871)
  • pnpm --filter @objectstack/service-analytics exec tsc --noEmit --listFiles — zero error-TS lines; the listing shows all five touched files inside the program, so the green covers the edits
  • node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (no paths passed; the script derived the changeset itself, and its first line names this repo at commit c8ac3e7): 29 commands — 28 green by their own verdict lines; check-test-completeness exits 3 with its own first line "PREREQUISITE NOT MET" (it grades a CI-produced turbo log) = NOT MEASURED, neither green nor red
  • pnpm lint (repo-wide eslint . --no-inline-config) ran to completion, exit 0, clean — no narrowing claimed or needed
  • node scripts/check-nul-bytes.mjs OK over 7815 files
  • Changeset: patch for @objectstack/service-analytics — no new exported symbol and no new payload key (reusing the exported guard was chosen over exporting anything new)

Out of scope, deliberately: the compiler-internal residue (#13571's ruled-first design fork) — this PR shrinks its reachability to direct consumers of the exported compiler and does not touch the lowering; the #5322 reductions and the #13570 producer guard are unmoved.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8


Generated by Claude Code

…ative-SQL merge sites
The #13640 guard (assertReadScopeCannotVacate) covered only the ObjectQL
ENGINE merges; ObjectQLStrategy.generateSql (the /analytics/sql echo) and
NativeSQLStrategy.applyReadScope still compiled the $not-over-$in-empty
family to a constant-TRUE predicate — the echo rendered, and the native
strategy actually executed, a whole-table WHERE for a scope execute()
refused. Call the same guard at both merge sites, after the compiler so
its own #13571 refusals keep their messages; compileScopedFilterToSql
itself (the ruled #13571 residue included) is unchanged.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AUF1NoViznQK32gqpK8wS8
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Sep 2, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/service-analytics, touching 2 documentable anchor(s). ⚠️1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

2 release-owned page(s) name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))
  • content/docs/releases/v17.mdx(via generateSql (symbol, a method of class ObjectQLStrategy))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/services/service-analytics/src/read-scope-sql.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • 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 — 8 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 c5a9a437dd881df2766df8f8c44055b366bb6d57packageMentionDocs.

Which tree this was computed on

This run read content/docs from 11d84ffd864fdea7f3501749ebb615c063d41b83 — the merge of head c8ac3e7b30a098e0da83226ef114163ee0932a81 into base c5a9a437dd881df2766df8f8c44055b366bb6d57, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 11d84ffd864fdea7f3501749ebb615c063d41b83 && git checkout 11d84ffd864fdea7f3501749ebb615c063d41b83
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c5a9a437dd881df2766df8f8c44055b366bb6d57 c8ac3e7b30a098e0da83226ef114163ee0932a81 && git checkout -B drift-repro c5a9a437dd881df2766df8f8c44055b366bb6d57 && git merge --no-ff c8ac3e7b30a098e0da83226ef114163ee0932a81
node scripts/docs-audit/affected-docs.mjs --json c5a9a437dd881df2766df8f8c44055b366bb6d57

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs c5a9a437dd881df2766df8f8c44055b366bb6d57 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-salesClaude

Copy link
Copy Markdown
Collaborator

Landing provenance (domain:services seat, session session_01AUF1NoViznQK32gqpK8wS8): flipped ready and enqueued via auto-merge on head c8ac3e7b after the three landing checks — ① in-seat Clause-② contract review PASS on the card (13926#issuecomment-5503343895, isolated reviewer at the contract-review tier, transcript-verified 50/50 stamps, verdict adopted verbatim); ② needs:contract-review cleared on both carriers and check-clause2-carriers --pair 14322 reads consistent; ③ all 40 check runs on this head completed success or path-filter skipped (Lint & Repo Gates green 02:25:22Z). Path surface: packages/services/service-analytics/** + one changeset — no governed surface, so the merge queue is the landing path. Follow-up from the review's §5 filed as #14329.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-sales@claude