docs(changeset): scope two pending service-analytics BREAKING banners to the SQL echo on either strategy, and anchor the nested-relation note to its measured base - #21012
Conversation
…SQL echo on either strategy, and anchor the nested-relation note's base The field-level gate and the relationship-path admission both run in the call context generateSql() shares, ahead of the strategy choice, and the ObjectQL strategy's echo never reached the engine's guards: the echo moved from a printed statement to 403 on both strategies, not only on a SQL deployment. The nested-relation note's Why paragraph describes the base measured before those two gates landed; it now says so. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
…two pending analytics notes to the doors they hold for The ObjectQL strategy already refused these queries on the query and the dataset doors, through the engine; on the SQL echo it printed the statement, because its generateSql renders without an engine call. Each sentence now names the two doors and says the echo printed. Claude-Session: https://claude.ai/code/session_01XY5uCwTjZj7884yYtyur4H Co-authored-by: Claude <noreply@anthropic.com>
|
Generated by Claude Code |
Contract reviewServed-tier: Review of PR #21012 (Part of #20917, #20933 and #20887) at its head ① Derived judgmentsThe diff. Three files, +9/−5, two commits ( The five rewritten sentences, each judged on (i) true, (ii) no new claim, (iii) backed by the cited reading.
What is deliberately not changed, and whether it should have been. The two ADR-0087 markers of the #20917 and #20933 notes are byte-identical (the #20917 marker's rationale still says the engine "already refuses" such queries "on the ObjectQL strategy", without the echo's scope). The marker is the gate's disposition input; The ADR-0087 reading and the Disclosure. The rewritten sentences name doors (route paths already present in all three notes) and strategies; no field spelling, no value, no request shape. The PR body and both dev reports state the probe by class, door and role only. Holds. ② Semver level
③ Boundary flagsRound 0's report (
Implemented-by: VERDICT: PASS |
Part of #20917
Clause-②: no
Part of #20933 · Part of #20887
A follow-up on three landed cards. It corrects one sentence in each of three release notes of
@objectstack/service-analyticsthat are still pending onmain, before release PR #20639 consumes them. The order is this lane's dispatch note on #20917. The reasons are thedomain:specseat's pointer5922364600on #6021 (two banners) and the named follow-up in section ② of the delta review5922352898on PR #20916 (one paragraph). PR #20991 is the precedent: it makes the same banner correction to #20935's note, and that note is not touched here. No code, test or other file changes.The three sentences
.changeset/20917-analytics-field-permission-gate.md, the BREAKING bannerPOST /api/v1/analytics/sqlwhichever strategy serves the cube.".changeset/20933-analytics-relationship-path-admission.md, the BREAKING bannerPOST /api/v1/analytics/sqlwhichever strategy serves the cube.".changeset/20887-analytics-nested-relation-engine-answer.md, the first sentence of WhyThe parenthesis in the third row is unchanged and elided here. Both banners keep their bold. Everything else in the three files is byte-identical: front matter, summary line,
Clause-②line, ADR-0087 disposition marker and every other paragraph. Each file's diff is one line (+1/−1).The readings behind each sentence
Banner of the #20917 note
Before, by source, at
95555e71(the parent of #20931's landing1571aedc):packages/services/service-analytics/src/analytics-service.ts:2305-2333:generateSql()runscallCtx(2329), resolves a strategy (2330) and returns that strategy'sgenerateSql(2333).analytics-service.ts:1283-1327:callCtxruns the object-level admission (1308) and the read-scope pre-pass. It has no field-level gate. No non-test source in the package namesgetReadableFieldsat that commit (git grepexit 1). The control is the landing1571aedc, where it is named in three files.strategies/objectql-strategy.ts:372-638: the ObjectQL strategy'sgenerateSqlrenders the statement from the cube definition and returns it (638). It makes no engine call, so it never reaches the engine's field guards. That strategy'sexecute()reachesengine.aggregate, which is why the query door already refused on it.After, on
mainata5bce40888:analytics-service.ts:1453-1515:callCtxruns the field-level gate (1487) after the object admission (1481).generateSql()callscallCtxbefore it resolves a strategy (2580).Banner of the #20933 note
Before, by source, at
83480c6a(the parent of #20962's landing5f6b63a6):analytics-service.ts:1452-1507:callCtxadmits objects overqueryObjects(1477). That set iscubeObjects(1584-1588, 1596-1608): the base object and the declared joins only. An object reached through an undeclared relationship path is not in it.getReadableFields. That answer is field-level only and never asks about object-level read (packages/plugins/plugin-security/src/security-plugin.ts:5410-5412,computeReadableFields5440-5461 overresolveProjectionFieldMask5517 ff.). Object-level read is the separatecanReadObject(5641). So the field gate does not refuse a readable field on an unreadable related object.objectql-strategy.tsis byte-identical at95555e71,83480c6aand5f6b63a6(git diff --quiet, exit 0 for both ranges).generateSqlpasses a one-hop cross-object dimension throughplanCrossObject(895 ff.). That function throws only for the out-of-envelope shapes: a cross-object time dimension, measure or filter, a multi-hop dimension, a non-recombinable measure. The one-hop dimension renders a LEFT JOIN (461-466), and the statement is returned (638).After, on
mainata5bce40888:queryObjects(1607-1616) adds every object a named member reads.generateSql()resolves a strategy (2580).Measured after, on both strategies
The measurement was a scratch probe at
a5bce40888, copied intopackages/rest/srcfor one run and removed by an EXIT trap. It was never committed;git status --porcelainwas empty afterwards.os-verify-lock.shfirst:turbo run build --concurrency=1over@objectstack/service-analytics...,@objectstack/plugin-security...,@objectstack/objectql...and@objectstack/driver-sql.... That is 19 tasks,VERDICT command-exit 0.pnpm --filter @objectstack/rest exec vitest run --maxWorkers=2on the one file gave 1 file / 2 tests passed,VERDICT command-exit 0.SecurityPluginoverObjectQLonSqlDriver(SQLite), andAnalyticsServicePluginover the same engine. There are two compositions:native(the plugin's own capabilities) andobjectql(narrowed to the engine-aggregate path).AnalyticsQueryRequestSchema). It then went togenerateSql, the callPOST /api/v1/analytics/sqlmakes (packages/runtime/src/domains/analytics.ts:141-159). The thrownstatusis the HTTP status (packages/runtime/src/dispatcher-plugin.ts:646-649).nativeobjectql403 PERMISSION_DENIED403 PERMISSION_DENIED403 PERMISSION_DENIED403 PERMISSION_DENIED403 PERMISSION_DENIED403 PERMISSION_DENIEDNativeSQLStrategy)ObjectQLStrategy)NativeSQLStrategy)ObjectQLStrategy)Before and after both hold for both banners. So each banner is scoped the way PR #20991 scoped #20935's. The note's own pins in
packages/rest/src/analytics-field-permission-gate.test.tsandanalytics-relationship-path-admission.test.tsassert the same echo refusals on both compositions. The probe adds the system-caller and readable controls, and it ran the bodies through the door's schema.The #20887 Why paragraph
The dev measured on the branch point of PR #20916,
00a92e18da. That is the parent of its first commit,5687276296.The base predates both gates.
git merge-base --is-ancestor 00a92e18da Xexits 0 for X =95555e71,1571aedc,83480c6aand5f6b63a6, so both landings descend from it. The reverse,1571aedcagainst00a92e18da, exits 1. Its control leg,00a92e18da~200against00a92e18da, exits 0, and the repository is not shallow.Clause one, "answered rows for a condition on a field the caller cannot read", held on that base:
getReadableFields(git grepexit 1; control: six hits inanalytics-service.tsonmain).callCtx(analytics-service.ts:1283ff. at00a92e18da) runs only the object admission (1308) and the read-scope pre-pass.strategies/filter-normalizer.ts:1296-1299). The native strategy joined the declared include.5916988260on #20802 analytics half (domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887) records the measured rows.Clause two, "without the declared join it named a table that does not exist (500)", held on that base:
strategies/native-sql-strategy.ts:771-783falls back to the relationship name as the joined table when no join is declared.analytics-service.ts:1266-1268;native-sql-strategy.ts:575-576).analytics-service.ts:1404,1416ff.). So nothing refused before the statement ran.The paragraph is anchored to that base in the delta review's own words; the clauses are kept.
The ADR-0087 gate's reading
breakingDeclarationandreadDispositionwere re-run on each note ata5bce40888and at this head. All three notes read the same on both:breaking: true;BREAKING,bangandclause-②-narrowing;not-required (no-migration-prescription).The gate itself counts the three as inherited, not introduced: it skips a changeset already breaking at the branch point.
Changeset gate: no
skip-changeset, andCheck Changesetstays redThis PR edits three pending changesets and adds none, so
Check Changesetgoes red by design.check-empty-changeset.mjs --base origin/mainexits 1 and refuses all three files as the DELIBERATE CORRECTION class. Ruling D on #18375 saysskip-changesetis never applied to a PR that edits an existing changeset, so no label is applied.Check Changesetis not a required context.Confirmation: this lane's at-tier contract review record on this PR's head judges each rewritten sentence above. The sentences are:
POST /api/v1/analytics/sqlon either strategy;domain:services): the cube read and the analytics read scope answer{ relation: { field: value } }as the engine seam now serves it — as the caller, capped, one answer on every face #20887 note's Why, now anchored to the base before the field-level gate and the relationship-path admission landed.Clause-②: nowas checked againstscripts/pm/clause2-line.mjs. A wording edit to pending notes widens no accept set and adds no public surface, so the value isno. With no arm, the line declares no direction.Verification at
56fc0e77e3node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack(exit 0) derived 19 commands from the three changed paths. Each ran on this head, with its exit code captured before any pipe.check-adr-0087-registration.mjs --base origin/mainand--self-testcheck-changeset-no-major.mjs --base origin/mainand--self-testcheck-closing-keyword-parity.mjsand--self-testcheck-comment-mask-corpus.mjscheck-empty-changeset.mjs --self-testpm/release-rehearsal-clone.mjs --self-testcheck:changeset-gate-self-tests,check:driver-memory-census,check:gitlink-declared,check:nul-bytes,check:objectui-changeset,check:pm-changeset-deadline-census,check:published-files,check:refd-timer-probe,check:watch-hint-literalcheck-empty-changeset.mjs --base origin/main, the DELIBERATE CORRECTION class above.check-changeset-fixed.mjs(its roster lives under.changeset/) exited 0.dispatch-gates.mjs --ranwith the recorded exit codes exited 0. It read "19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN".origin/maingainedf8178ffeceafter the branch point. It touches none of the three notes, and all three are still present there.Acceptance notes
Patch rounds (the seat's append from the dev's report on #20917; the dev writes a body only once)
Patch round 1
At
db64c37690, on the seat's note5923016038on #20917, which takes the dev's class (a) finding into this PR. Two sentences beside the corrected banners are qualified to the doors they hold for. Nothing else changes: each is still one sentence, rewrapped in place, and both ADR-0087 disposition markers are byte-identical..changeset/20917-analytics-field-permission-gate.md, What changed, last sentencePOST /api/v1/analytics/queryandPOST /api/v1/analytics/dataset/query, as the data API did, but printed the statement onPOST /api/v1/analytics/sql.".changeset/20933-analytics-relationship-path-admission.md, Refusals that change form, first sentencePOST /api/v1/analytics/queryandPOST /api/v1/analytics/dataset/query, thoughPOST /api/v1/analytics/sqlprinted the statement; on those two doors it now answers the analytics door's refusal rather than the engine's, the same one a declared join gets."Readings:
95555e71. On the query door, the ObjectQL strategy'sexecute()reaches the engine (objectql-strategy.ts:301) before it renders its own echo (350-354). The dataset door runs the sameexecute(), so both refused through the engine. The SQL echo'sgenerateSql(372-638) renders with no engine call, andcallCtxhad no field gate (analytics-service.ts:1283-1327). SoPOST /api/v1/analytics/sqlprinted the statement.83480c6a. On the query and dataset doors, a one-hop dimension through a related object goes throughexecuteCrossObject. ItsresolveFkAttr(1176 ff.) reads that object through the engine as the caller (1211-1215). The echo renders the join (461-466) and returns the statement (638), with no engine call, and the object was outside the admitted set (analytics-service.ts:1584-1608). After the change, onmain, the echo answers403 PERMISSION_DENIEDon both strategies (round 0's probe).ADR-0087 reading at
db64c37690: for both notes,breakingDeclarationreadsbreaking: truewith the signalsBREAKING,bangandclause-②-narrowing, andreadDispositionreadsnot-required (no-migration-prescription). Both are unchanged froma5bce40888, and the #20887 note reads the same.Gates at
db64c37690:dispatch-gates.mjs --commandsderived the same 19 commands. 18 exited 0.check-empty-changeset.mjs --base origin/mainexited 1 by design: the DELIBERATE CORRECTION class, with all three notes named.check-changeset-fixed.mjsexited 0.--ran: 19 derived, 19 run, 0 not measured.origin/main8055ff2279, which had changed one roster file, and gave the same 19 commands. None of the three notes changed onmainover that range.Acceptance note, wording kept: both ADR-0087 disposition markers are HTML comments, and they stay byte-identical. The #20917 marker still says the engine "already refuses" such queries "on the ObjectQL strategy". Like the two sentences above, that wording holds for the query and dataset doors, not for the SQL echo.
Generated by Claude Code