Repository navigation
[Decision] analytics field gate (#20917): an authored cube member whose sql is an expression — keep the stand-down, judge its identifiers, refuse it, or retire expressions #20943
Description
Activity
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actions决策请求 ·
domain:servicesseat (#6021) ·session_01XY5uCwTjZj7884yYtyur4H· 2026-09-30T21:09Z⚠️ 披露纪律:本评论只按类别描述,不含任何请求、字段名或返回值。背景
- security(analytics): the native-SQL strategy answers a query naming a field the caller has no field-level read permission for, where the engine and the ObjectQL strategy refuse 403 #20917(p0 安全)已由 PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931 落地(
1571aedc)。分析层入口现在有一道字段级读取闸:在对象级准入之后、任何策略运行之前,把查询点名的每个成员解析成它读的字段,用安全服务的getReadableFields判定;不可读就回引擎原文的403。 - 有一类成员解析不到单个字段:作者手写 cube 里,
sql是表达式的成员,例如CASE WHEN …,或两个聚合相除的比率。闸对它放行(已钉死),这是改动前的原状,不是裁决。 - 后果:这类表达式如果读了调用方无权读的字段,原生 SQL 策略照样给出答案;ObjectQL 策略则直接拒绝表达式度量。
- dev 在报告
5919006124里提了这个分叉,达档复核5919318329把它升级。它落在安全与权限边界上,属于人工地板,所以呈给您。
Governing text
- security(analytics): the native-SQL strategy answers a query naming a field the caller has no field-level read permission for, where the engine and the ObjectQL strategy refuse 403 #20917 分诊方向
5917636106:「闸判定成员解析到的底层字段,而不是 cube 的别名」。表达式解析不到单个字段,这句没有覆盖。 packages/spec/src/data/analytics.zod.ts:度量的sql是「SQL expression or field reference」,维度的是「SQL expression or column reference」。契约今天允许表达式。- ADR-0021(已接受)设计原则:「Zero raw SQL / zero raw expressions」与「Safety enforced by the engine, not the author」。D1 把作者面收窄到
dataset:比率这类派生度量用derived: { op: 'ratio', of: [...] }按名字引用其他度量,不写原始表达式。它管的是dataset,没有明文退役analytics_cube成员上的表达式。
前提(每条带复核命令)
- P1 放行已在 main:
git grep -n "Anything else is an EXPRESSION" -- packages/services/service-analytics/src/analytics-service.ts。 - P2 契约允许表达式:
git grep -n "SQL expression or field reference" -- packages/spec/src/data/analytics.zod.ts。 - P3 仓内用量:
git grep -n "CASE WHEN" -- examples/app-showcase/src/data/analytics/showcase.cube.ts。全仓只有一个表达式成员done_rate,读的是可读字段;sql: '*'只用于计数,不读字段值。 - P4 结构化替代存在:
git grep -n "DerivedMeasureOp" -- packages/spec/src/ui/dataset.zod.ts。dataset的度量有自带filter和derived比率,所以done_rate可以写成「带筛选的计数 ÷ 计数」。
Q1 作者 cube 里的表达式成员,字段闸怎么对待?
一句话问题: 一个表达式成员可能读到调用方无权读的字段,但闸看不出它读了哪个字段。今天闸放行。要不要改?
选项 × 真实代价:
选项 做什么 客户可感知的后果 A 维持放行 把表达式视为 cube 作者声明的派生值,类似公式字段 零改动。表达式读了隐藏字段时,原生路径静默给出答案 B 解析表达式 在分析层对表达式里的标识符做分词,逐个按字段判定 分析层多出第二个 SQL 解析器;标识符碰巧与隐藏字段同名时会误拒 C 一刀切 调用方在该对象上只要有一个不可读字段,就拒绝所有表达式成员 不碰隐藏字段的表达式也会被拒;受限用户的看板大面积报错 D 契约退役表达式 在 spec 契约层把 cube 成员的 sql收窄为字段引用;派生值改用dataset的filter与derived声明;done_rate随之迁移作者无法再写绕过权限的成员;已有表达式要迁移(仓内一个,云端与客户部署未知) 业务直译:
- A:会计手写的公式单元格照常显示,即使公式引用了他看不到的列。
- B:在报表层再写一个公式解析器,逐个检查引用。
- C:只要你有一列看不到,所有公式单元格都不给你看。
- D:不再允许手写公式,只能从一张「已认证公式」清单里选。
四轴(业务立场):
- 长远: ADR-0021 已定方向:作者面零原始表达式,安全由引擎保证而不是作者。D 与之一致,在契约层结构性堵住;A 把一个已知例外长期留在权限闸里;B 是在消费端补丁式再解析一次 SQL,违背「单一派生、契约优先」;C 过宽。
- 业务拉动: 实测拉动为零:仓内唯一的表达式成员读的是可读字段,也没有读隐藏字段的表达式。云端与客户部署的作者 cube 本席读不到。D 的迁移成本在仓内只有一处,而且已有结构化替代。
- 防 AI 犯错: D 最强:AI 根本写不出绕过权限的成员。B 次之,能响亮拒绝但会误报。A 最弱,AI 写出一个读隐藏字段的表达式,没有任何提示。
- 不扩散: A 零改动。D 是能力退役,属于「从紧」而非扩张,但要动 spec 契约和一次迁移。B 新增一个解析器。C 新增一条粗粒度规则。
推荐:D;D 落地之前维持 A。 回退是 A。
- 理由以长远轴领起:这就是 ADR-0021 已接受原则在 cube 层的收尾,而且结构化替代已经存在。
- 置信缺口:云端与客户部署里的作者 cube 表达式用量不可读;维度上的
CASE分桶有没有结构化等价物,未实测。仓内没有这种用法。 - 自检:只看①选 D;②③④ 是否翻转:②零拉动偏向 A 但不翻转(D 本身是收窄);③ 支持 D;④ 在 A 与 D 之间持平。
裁后执行
- 选 D: 本卡转为 spec 车道的退役卡(
domain:spec,由分诊定级),带 ADR-0087 处置与done_rate迁移。闸在迁移落地前维持 A;退役后,闸的放行分支变成不可达代码,一并删除。 - 选 A: 本卡以「维持现状」关闭,原文写进
analytics.zod.ts的文档说明。 - 选 B 或 C: 本卡回到本车道,改动闸的实现(
service-analytics),钉子按所选选项写。
os-decision-facets
- ① 项目长远合理性:D 在契约层落实 ADR-0021「零原始表达式、安全由引擎保证」,不在分析层再解析 SQL;A 留一个长期例外。
- ② 实际业务拉动:实测零拉动:仓内唯一的表达式成员读可读字段;云端与客户部署不可读。
- ③ 防 AI 犯错:D 让 AI 写不出绕过字段权限的成员;A 下这类成员被静默放行。
- ④ 创业阶段不扩散:D 是能力退役(从紧),不新增门禁;B 新增解析器,C 新增粗粒度规则。
Prior rulings read: expression,stand-down,formula,raw,cube,field-level → 42 hits; ADR-0021 D1 (the dataset author surface: no raw SQL, derived measures by name — read, does not retireanalytics_cubemember expressions), ADR-0032 Decision §2, ADR-0058 D8, ADR-0120 D3, ADR-0005 Decision §5, ADR-0030 Decision §6, ADR-0031 Decision §2, ADR-0032 Decision §1 — each other a term match on another subject, none answers Q1; thread: none; repo: objectstack-ai/objectstack
推荐:Q1 D(落地前维持 A)。自检:只看①选 D;②③④ 是否翻转:否。
置信缺口:云端与客户部署的作者 cube 表达式用量不可读;维度上的 CASE 分桶有无结构化等价物未实测。
回批一行即可,例如
Q1 D或Q1 A。Release: not applicable — this card was filed as a decision card and was never claimed. The landed code (PR #20931,
1571aedc) keeps the stand-down until the ruling.
Generated by Claude Code
- security(analytics): the native-SQL strategy answers a query naming a field the caller has no field-level read permission for, where the engine and the ObjectQL strategy refuse 403 #20917(p0 安全)已由 PR fix(service-analytics)!: one field-level read gate at the analytics door, before either strategy (#20917) #20931 落地(
- addedarea:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsPermissions that actually hold — RLS/FLS, sharing model, write-path guardspriority:p1High: required for production / M2High: required for production / M2
on Sep 30, 2026 objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsRuling: batch #259 item 2 · letter D · maintainer 「同意」 2026-09-30T22:57Z
Director seat, summon #31,
session_01PGMD6TRDaZY8Ubyo7Ukm66(GitHubos-litant; written asobjectstack-fleet[bot]via the relay). Batch #259 was presented in the live director chat (four cards; this one item 2, director recommendation D with A held until D lands); the maintainer answered the batch with one word, 「同意」. The seat's analysis 5919778230 reached the same letter; this ruling was derived from axis ① first and the letters coincide.The ruling
D — retire raw expressions in a cube member's
sqlat the contract. A cube member'ssqladmits a field or column reference only (and'*'for a count); a derived value is declared in a form the platform can judge — thedatasetform (derived: { op, of: [...] }over named measures, and a filtered count). A stays as the interim: the gate's stand-down on an expression member remains exactly as PR #20931 landed it until the retirement is onmain, and then that branch is deleted as unreachable. B (a second SQL parser in the analytics layer) and C (refuse every expression member for a partially restricted caller) not taken.Readings (this summon,
origin/main013f97d)- P1 stand-down:
packages/services/service-analytics/src/analytics-service.ts:409("Anything else is an EXPRESSION the cube's author wrote") — verified. - P2 contract:
packages/spec/src/data/analytics.zod.ts:250measuresql"SQL expression or field reference";:305dimensionsql"SQL expression or column reference" — verified. - P3 in-repo usage:
examples/app-showcase/src/data/analytics/showcase.cube.ts:41done_rate(SUM(CASE WHEN status = 'done' THEN 1 ELSE 0 END) * 100.0 / COUNT(*)) is the only expression member in the tree;git grep -l 'analytics_cube|defineCube|AnalyticsCubeSchema' -- examples packages/apps packages/pluginsnames that file alone;sql: '*'at:26is the count — verified. - P4 structural replacement:
packages/spec/src/ui/dataset.zod.ts:136DerivedMeasureOp = ['ratio', 'sum', 'difference', 'product'],:79a derived measure combines other measures by name — verified; a measure-levelfilterondatasetmeasures — the seat's reading, unverified this summon. - ADR-0021
docs/adr/0021-analytics-dataset-semantic-layer.md:62,:118("Zero raw SQL / zero raw expressions"),:133,:164-167(derived measures reference other measures by name only) — verified. The ADR governsdatasetand does not itself retireanalytics_cubemember expressions, which is why this ruling exists. - Prior rulings read:
raw expression|zero raw|expression measure|cube member|member.*sqloverAGENTS.md docs/adr packages/spec/src→ 18 hits; deciding: ADR-0021 (above); the rest are the cube inner-nameretirement (ADR-0049, spec(analytics): retire the innernameon cube measures and dimensions; the record key is the identity (2 keys) #20300) and the single-granularity default — term matches on other subjects; thread: 5919778230.
四棱(本裁决新记录)
- ① 长远:D 在契约层落实 ADR-0021「零原始表达式、安全由引擎保证」,不在分析层再解析 SQL;LookML 式的表达式靠成员级授权补,本平台没有也不新建。
- ② 拉动:仓内零拉动(唯一表达式成员读可读字段);云端与客户部署不可读。
- ③ 防错:D 让 AI 写不出绕过字段权限的成员;A 下静默放行。
- ④ 不扩散:D 是能力退役(从紧),不新增门禁。
- 只看①选 D;②③④ 是否翻转:否。
Execution parameters (ruled here; no further decision card)
- Lane / state: this card leaves the decision box to
pm:queueindomain:spec(domain:servicesreplaced in the same stroke);security,priority:p1andarea:accessstay. Thedomain:specseat claims it as a retirement card: level L,mode:cloudormode:subagentat the seat's call,Clause-②: yes (narrowing)— BREAKING, with the ADR-0087 disposition in the changeset and the full kit of thespec-property-retirementskill (refusal guidance on an expression value naming thedatasetform, liveness ledger row, regenerated baselines, docs, pins). - The shape after:
AnalyticsCubeSchemameasure and dimensionsqladmit an identifier (a field or column reference) and'*'; any other value is refused at parse with the prescription.examples/app-showcasedone_ratemigrates to thedatasetform in the same PR, and the showcase dashboards that read it are re-pinned. - Governed half: ADR-0021 gets a dated note under its decision naming the spec symbol (
analytics.zod.tsmeasure / dimensionsql) — a separate Tier H docs PR (four-piece), ⛔ never mixed into the code PR. - Follow-up in
domain:services: once the retirement is onmain, the stand-down branch atanalytics-service.ts:409is unreachable and is deleted with its pin. The spec seat files that as a sub-issue at dispatch (Blocked-by:this card). Until then the interim is A and the gate is not touched. - Fork clause: the premise is that every authored expression member has a structural equivalent. If the dev finds a dimension
CASEbucket, or another expression shape, thatdatasetcannot express, it reports the fork on this card and stops; ⛔ no widening ofdatasetin the same PR, ⛔ no quiet fallback to B or C. A cloud or customer cube expression that reads a hidden field, if one turns up, is reported the same way and is not patched at the gate.
Generated by Claude Code
- P1 stand-down:
objectstack-fleet commented
on Sep 30, 2026 ContributorAuthorMore actionsClaim: PM loop round 32 · 2026-09-30T23:10Z
Session:session_01Sfe5YjBLwB9J3y8fvm2xq1
Account:os-justin(the seat's linked user asGET /useranswers it; the card's assignee from this act)
Branch:claude/issue-20943-cube-sql-identifiers
Worktree:objectstack-issue-20943
Domain:domain:spec(ruling D5921156712, execution parameters: a retirement card)
Seat:domain:spec#5(seat post #19357)
Clause-②: yes (narrowing)
Scope: ruling D's execution parameters, taken whole. It is a retirement with the fullspec-property-retirementkit.AnalyticsCubeSchemameasuresql(packages/spec/src/data/analytics.zod.ts≈:250) and dimensionsql(≈:305) admit an identifier (a field or column reference) and'*'for a count. Any other value is refused at parse, with a prescription that names thedatasetform (derived: { op, of: [...] }over named measures, and a filtered count).- The kit: the ADR-0087 disposition in the changeset (BREAKING), a D3 semantic entry and the regenerated migration registry, the liveness ledger row, regenerated baselines and docs, and pins, each ablated.
examples/app-showcasedone_rate(src/data/analytics/showcase.cube.ts≈:41, the only expression member in the tree, as ruling D read it) migrates to thedatasetform in the same PR. The showcase dashboards and tests that read it are re-pinned:examples/app-showcase/test/gap-fill.test.tsandpackages/services/service-analytics/src/__tests__/cube-authored-format-granularity.test.tsname it.⚠️ Fork clause: if a dimensionCASEbucket, or another expression shape, has no structural equivalent indataset, the dev reports the fork on this card and STOPS. ⛔ No widening ofdatasetin this PR. ⛔ No fallback to B or C.⚠️ The disclosure discipline holds: no request recipe, field spelling or returned value from any private measurement.
File surface (stop on breach and explain it in the report):packages/spec(the schema, its tests, the D3 entry, the registry, liveness, generated projections and docs, regenerated only);- the showcase cube, its dashboards and tests;
- the one service-analytics test named above, only where the migration moves its assertion;
.changeset/.
⛔ The gate's stand-down branch (analytics-service.ts≈:409) is not touched. It is service-analytics: delete the analytics field gate's stand-down on an authored cube expression member once #20943 retires raw expressions in a cube member'ssql(the branch becomes unreachable) #20965's, filed in this act,Blocked-by: #20943. ⛔ ADR-0021's dated note is the separate Tier H docs PR the ruling names, and it is never mixed into this PR.
Container & model:S,mode:subagent,model: opus.
Review: an at-tier contract review is owed before enqueue.
Thread-read: 5921156712
Serial constraints cleared: read at this stamp.- The only open PR on this surface is fix(spec): the field → name respelling reaches a form view's subform columns as its own ADR-0087 step, and two step-18 entries read true (#20901) #20953 (the seat's own, in the merge queue), on
packages/spec/src/migrations/registry.ts. - Registry serial condition: the dev writes
migrations/registry.tshunks only after merging amainthat contains fix(spec): the field → name respelling reaches a form view's subform columns as its own ADR-0087 step, and two step-18 entries read true (#20901) #20953, regenerating with the repo's generator (never by hand). packages/spec/src: 1,277 comment lines still cite 170 deleted tracker numbers (1,295 sites) — the staged remainder of ruling C+D on #19123, measured by PR #20226 #20234 stage 11 waits behind this card on the same file. - No open PR touches
data/analytics.zod.ts,ui/dataset.zod.ts, the showcase analytics or dashboards, or the analytics liveness ledger.
Generated by Claude Code
9 remaining items
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20943,
"status": "done",
"branch": "claude/issue-20943-adr0021-note",
"pr": "#21071",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (mode:subagent, the dispatching session's id)",
"premise_still_valid": true,
"summary": "Ruling D's governed half, opened as draft PR #21071 (Part of #20943, Clause-②: no). Head 68593d4, base 6073bb9, three files, +18 / -11, no packages/** change. ADR-0021: a dated note at the end of D1 records ruling D and names MetricSchema / DimensionSchema sql in packages/spec/src/data/analytics.zod.ts, with symbol anchors. Dashboards skill: the Level A table drops "or any custom-SQL metric". The iron rule now escalates to a stored field (a summary rollup, or a field holding the computed value or bucket) or app code, not to a Cube, because a cube member's sql is a column reference. It also no longer names a formula field (declared below). Checklist: dashboards.cube-query revision 3 drops done_rate. The skill file has the same line and token counts as before, and nothing else in ADR-0021 needed an edit.",
"changes": [
"docs/adr/0021-analytics-dataset-semantic-layer.md (+2): one blockquote "Note (2026-10-01) — cube members", placed after D1's RLS paragraph and before D2. Rests on: ruling 5921156712 (letter D, maintainer 2026-09-30); PR #20998 = 5d5e679; analytics.zod.ts at head: CUBE_MEMBER_SQL :240 (the accept set: identifier, dotted path, ''), MetricSchema :282 with sql :330, DimensionSchema :367 with sql :394 (refused at parse: cube-member-sql-column-reference.test.ts :129-160 and :180-232); the measure refusal CUBE_METRIC_SQL_EXPRESSION_REFUSED :248-258 names the dataset measure filter and derived { op, of }; the dimension refusal CUBE_DIMENSION_SQL_EXPRESSION_REFUSED :260-265 says to keep the bucket as a field of the object. The links use #MetricSchema / #DimensionSchema / #CUBE_MEMBER_SQL symbol anchors: check:adr-symbol-anchors exit 0, check:adr-links exit 0.",
"skills/objectstack-ui/rules/dashboards.md (5 lines rewritten, 0 net): (1) :70, "or any custom-SQL metric" removed from the escalate column. The one such metric in the tree (showcase done_rate) is now a dataset measure-filter count plus a derived ratio (chart-gallery.dataset.ts :34-38), so the row was false. (2) :75-81 iron rule: "escalate to a hand-authored Cube (raw SQL / explicit joins), a stored rollup or formula field on the object" became "escalate to a stored field on the object (a summary rollup, or a field holding the computed value or bucket) or app code, not to a Cube: a cube member's sql is a column reference". Rests on: MetricSchema/DimensionSchema sql regex :330/:394; summary has a numeric column (driver-sql sql-driver.ts :19438); the dimension refusal text :264. Declared beyond the retirement: "formula field" is dropped because the code stores no formula column (sql-driver.ts :19561-19562 "Virtual — no column"; aggregate-field-type-compatibility.ts :80 refuses sum/avg/min/max over formula, enforced by dataset-compiler.ts assertAggregateFieldTypeCompatible :389 / :713). The sibling published skill objectstack-formula SKILL.md :407-411 already says to denormalise onto a stored field. Left as they were, after reading: the "Computed column" bullet :91-94 (still true, names no escalation target) and the Analytics Cubes section :433-468 (its defineCube example uses identifiers and ''; Rule 1 concerns the cube-level sql; nothing there is false). No Cube rule was added, because the parse refusal carries the prescription.",
"docs/qa/platform-checklist/areas/dashboards.json, item dashboards.cube-query: revision 2 to 3. The fixture line lists the measures count / total_estimate_hours / avg_estimate_hours (showcase.cube.ts :22-44; the cube comment :38-43 says done_rate moved). The meta clause now reads "exactly its measures (namespaced showcase_delivery.count / .total_estimate_hours / .avg_estimate_hours)", with no count written. The verify text says "names each measure and dimension the clause lists" instead of "all four measures". The variant "measure done_rate (type: number, computed CASE expression)" is removed, and a history entry (revision 3, 2026-10-01, ref #20943) says what moved and that the steps, the reconciliation clauses and the four dimensions are unchanged. This follows README.md Lifecycle "Change" (bump revision, append history), and the validator's revision-equals-last-history check passes."
],
"adr0021_read": "Read whole (446 lines before the note; line numbers below are at base 6073bb9). Nothing else is made false by this retirement. :16 Correction: dated, and about joins and RLS. :118 the "zero raw expressions" principle is now enforced one layer lower. :123 "expressible four ways (Cube DSL, ...)" is dated decision-time context. :140-143 (the D1 sketch comment, "Cube Dimension/Metric ABSORBED") and :379 (D-A: "the author-facing Cube DSL is retired/absorbed"; "smaller than both Cube-with-raw-SQL and QuerySchema") are decision-time rationale. They were already at odds with an authorable analytics_cube before ruling D, so the drift is pre-existing (below, not filed). :358-362 Finding 2 (dated scan 2026-05-31) describes the Cube layer's joins and RLS and says nothing about member expressions. :442 open follow-ups: unrelated.",
"tests": "All on head 68593d4 (git rev-parse --short HEAD after the final commit; no later commit). dispatch-gates --commands --repo objectstack-ai/objectstack, no paths given: exit 0, 31 commands for 3 paths (23 by path, 8 whole-tree). All 31 were run with exit codes recorded before any pipe. --ran reconciliation: "31 derived, 31 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. 29 exited 0, including check:adr-links, check:adr-symbol-anchors ("2166 anchors across 140 records resolve"), check:adr-anchors, check:doc-authoring, check:nul-bytes, check:skills-token-ratchet (dashboards.md 6211 tokens, ceiling 6252), check:skill-identifier-liveness, check:skill-frame-sync, check:skill-compatibility, check:pm-governed-merges, check:closing-keyword-parity, check:cross-package-test-inputs and check:comment-mask-corpus. check:doc-formula-expressions first answered exit 3 (PREREQUISITE NOT MET, formula and lint unbuilt). It answered exit 0 after "turbo run build --filter=@objectstack/formula --filter=@objectstack/lint" under os-verify-lock (VERDICT command-exit 0, 4/4 tasks). pnpm check:platform-checklist exit 1 with 1 problem, the areas/identity-auth.json anchor packages/plugins/plugin-auth/src/auth-plugin.ts#twoFactor (ABSENT SYMBOL). The same single problem gives exit 1 on the untouched base 6073bb9 and on a detached origin/main 5dbeb7d worktree (clean status, removed after). It is main's, not fixed here, and the dashboards.json edit adds no problem. node scripts/pm/check-skill-line-ratchet.mjs exit 0 and check-skill-id-lint.mjs exit 0 (34 files clean); neither scans skills/** by its own header. Skill readings: dashboards.md 468 lines / 24842 bytes / 6211 tokens before, 468 / 24843 / 6211 after. Sum of every skills//SKILL.md: 4395 lines before and after (none touched). Lint: eslint --no-inline-config --format json over the 3 changed files gave 3 results, each 0 errors and 1 warning "File ignored because no matching configuration was supplied". The eslint.config.mjs population is /*.{ts,tsx,mts,cts,js,jsx,mjs,cjs} (:971) and the diff has no file in it. skip-changeset criterion measured: 69 non-private package.json files[], 0 cover the three paths. Positive control: the same matcher hits service-analytics README.md and spec json-schema. skills/ reach customers through npx skills add from GitHub, not npm. Not run: check:skill-examples, which the derivation did not name; no fenced code block was touched. No ablation: prose and JSON only, with no gate or test added.",
"ci": "Head 68593d4, check-runs deduped by name (latest started_at), read once at report time: 29 distinct, 13 success, 7 skipped, 9 in_progress. In progress: Check Changeset; Lint & Repo Gates; Test Core 1/6, 2/6, 3/6; Type Check consumer gates, debt ledger, source gates, workspace. Successes include Governed Surface Queue Guard, Dogfood Regression Gate, Part-of PR must not also close its card, and Test Core 4/6, 5/6, 6/6. Not awaited. Check Changeset is expected to go red after its label window: there is no changeset and no skip-changeset label, and the dispatch limited my writes to three.",
"governed": "check-governed-merges --pr 21071 (paths read off the PR's file list): exit 3 GOVERNED, landing tier H. docs/adr/ has 1 path and skills/ has 1. docs/qa/platform-checklist/areas/dashboards.json is not on the register. 29 changed lines, under the 5000-line threshold. --branch claude/issue-20943-adr0021-note gives the same verdict. PR read-back: draft true, assignee os-justin, labels documentation and size/s (both from the labeler), 0 requested reviewers, 3 files, and the body is byte-identical to what was sent (930 bytes).",
"seat_actions": [
"Apply skip-changeset on #21071 (criterion measured above), or Check Changeset stays red.",
"The PR body has the dashboards.md file reading but not the package reading (sum of skills SKILL.md: 4395 lines before and after), and it has no "## 维护者速读(草稿)" section. My contract asks for both on a governed PR; the dispatch said to keep the body very short because it is the seat's. I did not PATCH. Draft for the seat to insert: 改了什么:三个文档文件。ADR-0021 在 D1 末尾加一条带日期的注记,记录裁决 D(分析 cube 的度量与维度 sql 只能是列引用,表达式在解析时被拒);对外发布的看板技能不再教「超出 dataset 就去手写带原始 SQL 的 Cube」,也不再把公式字段当出口;QA 清单的 cube 条目去掉已迁到 dataset 的 done_rate。为什么改:代码半边 #20998 已落地,这三处文字随之失真;技能是 AI 写元数据的语料。风险与代价(含回滚):纯文字、无代码、无发版;技能文件行数与 token 数不变;回滚即 revert。席位意见:(留空)。你要做的:审阅 ADR 注记与技能那一段措辞,同意即批准(Tier H)。"
],
"mcp_calls": "0",
"api_writes": "3 relay strokes, each a POST /repos/objectstack-ai/objectstack/dispatches executed as objectstack-fleet[bot]: (1) pr_create → POST /repos/objectstack-ai/objectstack/pulls (draft #21071, run 36815673169, read-back 930 bytes identical); (2) label-write --assign os-justin → POST /repos//issues/21071/assignees (run 36815717811, read-back matches, zero label writes); (3) this os-dev-report → POST /repos//issues/20943/comments. git push is not counted: one empty-branch probe push and one commit push. Reads were REST GETs: the card and its comments, 31 open PRs' file lists (none touches the three files), check-runs and rate_limit.",
"open_questions": [
{
"question": "Conflict, not resolved silently: the dev contract says a governed-surface PR body carries a "## 维护者速读(草稿)" section and, for skills/**, both size readings. The dispatch said the rest of the body is very short and belongs to the seat. Which one governs this PR?",
"options": [
"A: the seat inserts the drafted section and the package reading (text in seat_actions)",
"B: leave the body as opened"
],
"recommendation": "A, because the contract makes both due on every governed PR, and the seat owns the body edit."
}
],
"out_of_scope_findings": [
"carrier: 承接者:无 · noted, not filed: ADR-0021 D-A (the Resolved decisions table) says "the author-facing Cube DSL is retired/absorbed", and the D1 sketch comment says the Cube Dimension/Metric are absorbed into the dataset. On main analytics_cube is still authored (defineCube, defineStack analyticsCubes, examples/app-showcase showcase.cube.ts), and ruling D treats it as a live authored surface. This is decision-time text that predates ruling D and is not made false by it; it is a read-only inference, and no door was measured."
]
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsRuling on report
5924841117· 2026-10-01domain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1), holder of claim5921298734.- Verified against GitHub: PR docs: a cube member's sql is a column reference, in ADR-0021, the dashboards skill and the cube checklist item #21071 is at head
68593d46d8, the head the report names.- It has 3 files (+18 / -11), is a draft, is assigned to os-justin, and says
Part of #20943. check-governed-merges --pr 21071: GOVERNED, landing tier H (docs/adr/**,skills/**). The maintainer merges.
- It has 3 files (+18 / -11), is a draft, is assigned to os-justin, and says
- Delivered as dispatched:
- ADR-0021 has a dated note after D1, naming
MetricSchema/DimensionSchemasql. - The dashboards skill no longer escalates to a raw-SQL cube.
- Checklist item
dashboards.cube-queryis at revision 3, withoutdone_rate.
- ADR-0021 has a dated note after D1, naming
- Beyond the retirement, declared by the dev: the skill sentence also drops "formula field" as an escalation target. The dev gives the code lines it rests on: a formula field stores no column, and an aggregate over one is refused. Accepted pending the at-tier review, which judges it.
- Q1: A. The seat inserts the
## 维护者速读(草稿)section and the skills-package size reading into the PR body in this act. 席位意见 stays blank until ACCEPT. - No changeset:
skip-changesetis added to the PR in this act. The dev measured that no package'sfiles[]covers the three paths. check:platform-checklistexit 1: it fails on theidentity-auth.jsontwoFactoranchor. The dev reproduced it on a detachedorigin/main, so it is not this PR's.- Out of scope: ADR-0021's D-A row and its D1 sketch say the Cube DSL is absorbed, while
analytics_cubeis still authored. That text predates ruling D. Noted, not filed. - Next:
needs:contract-reviewgoes on the PR, and it is subscribed to this session.- Then the CI wait and the at-tier review.
- At ACCEPT, the Tier H hand-off: the 速读 comment,
needs-user-decision, and review requests. The PR stays draft.
Generated by Claude Code
- Verified against GitHub: PR docs: a cube member's sql is a column reference, in ADR-0021, the dashboards skill and the cube checklist item #21071 is at head
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsos-dev-report
{
"issue": 20943,
"status": "done",
"branch": "claude/issue-20943-adr0021-note",
"pr": "#21071",
"session": "session_01Sfe5YjBLwB9J3y8fvm2xq1 (mode:subagent, the dispatching session's id)",
"premise_still_valid": true,
"summary": "Cut round for review FAIL 5925160445 (finding E). New head 5ed7f87, one commit appended to 68593d4 with no merge or rebase. The diff stat over 68593d4 is 2 files, +3 / -3, and the PR is now 5 files, +21 / -14. Eval 2 now escalates to a stored or materialized rolling field or app code, not a Cube, and its must_contain drops "Cube". The evals README names a stored field or app code. Both files shrink.",
"sentences": [
"skills/objectstack-ui/evals/analytics-inline-vs-dataset.json :18, eval 2 expected_output. Before: "Escalates to a hand-authored Cube (raw SQL) or a stored/materialized rolling field". After: "Escalates to a stored/materialized rolling field or app code, not a Cube" (82 to 72 characters). File: 4406 bytes / 1102 tokens before, 4388 / 1097 after (ceiling 1102). must_contain changed from ["window", "Cube"] to ["window"]. Reason: with Cube now named only as a non-target, a correct answer need not name a Cube at all, so requiring "Cube" would fail correct answers; it also cannot tell "not a Cube" from "a Cube". "Cube" was not added to must_not_contain, because that would flag a correct "not a Cube". No positive escalation term was added, because must_contain is AND and an answer may name either target. must_not_contain ["dateGranularity"] is unchanged.",
"skills/objectstack-ui/evals/README.md :10. Before: "must escalate to a Cube or a stored rollup field". After: "must escalate to a stored field or app code". File: 1148 bytes / 287 tokens before, 1143 / 286 after (ceiling 289). I wrote "a stored field", not the dispatch's "a stored rollup field", for two reasons. "a stored rollup field or app code" is 2 bytes longer than the old text, against "no longer than the current text". And the iron rule at dashboards.md :78 says "a stored field on the object (a summary rollup, or a field holding the computed value or bucket) or app code", which covers both eval 2 (rolling field) and eval 3 (rollup)."
],
"tests": "Head 5ed7f87. dispatch-gates --commands --repo objectstack-ai/objectstack over the two paths: exit 0, 23 commands. The tool warned STALE TREE because origin/main 2821e9f changed scripts/sdui-manifest.record.json. The same two-path derivation from a detached origin/main 2821e9f worktree (removed after) printed an identical list of 23, so the warning does not move it. No PR path changed on main since base 6073bb9, so no merge was needed. All 23 ran (formula and lint built first under os-verify-lock, VERDICT command-exit 0), and every one exited 0. --ran: "23 derived, 23 run, 0 NOT-MEASURED, 0 UNRUN", exit 0. check-skills-token-ratchet exit 0: README 286 of 289, analytics-inline-vs-dataset.json 1097 of 1102, dashboards.md 6211 of 6252. Eval-fixture validator: none in the repo; git grep for must_contain outside skills/ hits only a CHANGELOG, and check-skills-token-ratchet is the one script that reads evals. The whole-branch derivation is 31 commands; its 8 extra (adr links and symbol anchors, adr-anchors, future-spec-major, platform-checklist, pm-prior-rulings) cover round-1 paths this commit does not touch and were run green in round 1, apart from main's platform-checklist twoFactor red. check-governed-merges --pr 21071: exit 3 GOVERNED, tier H, 5 paths (docs/adr x1, skills x3, plus dashboards.json off the register), 35 changed lines.",
"grep": "Three git grep runs over skills/ at the head. (1) The word cube on a line that also says escalat, raw sql, custom sql, hand-authored or expression: 2 hits. evals/analytics-inline-vs-dataset.json :18 is the new "not a Cube". rules/dashboards.md :466 is Cube Rule 1, "do not put raw SQL there", a prohibition. (2) escalat across skills/: in objectstack-ui, rules/dashboards.md :62 / :66 / :78 (the dataset envelope, now a stored field or app code), :373 (the drawer "Open in list" escape hatch, not analytics), evals/README.md :10 (new) and evals json :3 / :18. navigation.md :68 is unrelated, and the rest are support-case "escalated" domain examples in other skills. (3) raw sql, custom sql or CASE WHEN: dashboards.md :76 (a dataset has no raw SQL), dashboards.md :466 (Rule 1) and objectstack-data rules/indexing.md :158 (a partial index issued as raw SQL from a runtime migration, unrelated). Result: no sentence under skills/ still names a Cube or raw SQL as an escalation target, so no out-of-file hit is left to report.",
"ci": "Head 5ed7f87, check-runs read once, deduped by name to the latest started_at: 30 distinct, 20 success, 8 skipped, 2 in_progress (Lint & Repo Gates, Test Core 1/6). Not awaited. PR read-back: draft true, labels documentation, size/s, skip-changeset and needs:contract-review. The body and labels were not touched.",
"mcp_calls": "0",
"api_writes": "1 relay stroke, this comment: POST /repos/objectstack-ai/objectstack/dispatches executed as POST /repos//issues/20943/comments. git push is not counted (one push, 68593d4 to 5ed7f87). Reads were REST GETs: review comment 5925160445, check-runs and the PR object.",
"open_questions": [],
"out_of_scope_findings": []
}
Generated by Claude Code
objectstack-fleet commented
on Oct 1, 2026 ContributorAuthorMore actionsLanded: PR #21071 →
d19da5d620(the governed half); the card is donedomain:specseat 5 (session_01Sfe5YjBLwB9J3y8fvm2xq1) · 2026-10-01. Holder of claim5921298734.- Landed: os-zhuang, an authorized approver, approved PR docs: a cube member's sql is a column reference, in ADR-0021, the dashboards skill and the cube checklist item #21071 at
5ed7f87157and enqueued it. It went through the merge queue asd19da5d620, with one parent (9b81314c29, read withgit rev-list --parents). All 5 files have the same per-filegit patch-id --stableas the reviewed head5ed7f87157against the merge base6073bb96b8. Review records: the FAIL5925160445, then the delta PASS5925512466. 维护者速读:5925525242. - Ruling D is complete:
- Code half: PR feat(spec)!: an analytics cube member's sql is a column reference, and the showcase done rate moves to its dataset (#20943) #20998 →
5d5e679873. - Governed half: ADR-0021 has the dated note, the dashboards skill and its evals no longer name a raw-SQL Cube as an escalation, and the checklist cube item is at revision 3.
- Code half: PR feat(spec)!: an analytics cube member's sql is a column reference, and the showcase done rate moves to its dataset (#20943) #20998 →
- Follow-ups, with triage: service-analytics: delete the analytics field gate's stand-down on an authored cube expression member once #20943 retires raw expressions in a cube member's
sql(the branch becomes unreachable) #20965 (delete the gate's stand-down branch) and spec+service-analytics: retire the cube metric typesnumber/string/boolean— they existed to carry a SQL expression, which #20943 retired from a member'ssql#21000 (the metric typesnumber/string/boolean). Both had pointers that their blocker landed (5924626421,5924627040). - Closed in this act as completed. The seat removes
pm:dispatchedand the assignee in the same act.
Generated by Claude Code
- Landed: os-zhuang, an authorized approver, approved PR docs: a cube member's sql is a column reference, in ADR-0021, the dashboards skill and the cube checklist item #21071 at
- added 6 commits that reference this issue
on Oct 7, 2026
Ruled: 5921156712 · letter D · 2026-09-30T23:01Z
Filed by the
domain:servicesseat (#6021, sessionsession_01XY5uCwTjZj7884yYtyur4H) as a decision card. #20917's dev left anopen_questionsentry that triage's direction does not settle: report5919006124on #20917, accepted in5919159814, and escalated by the at-tier review5919318329. #20917 has closed, so this card is the question's anchor. ⛔ Not a claim.The question
PR #20931 (landed as
1571aedc) judges every member an analytics query names against the caller's readable fields, at the analytics door and before either strategy runs. A member of an authored cube whosesqlis an expression (for example aCASE WHEN …or a ratio of aggregates) names no single field the gate can attribute.This is a permission-boundary question, so it is the maintainer's. Four options follow; the analysis is in Chinese in the next comment.
sqlat the contract (thespeclane), per ADR-0021's "zero raw expressions". A derived value then has to be declared in a form the platform can judge.Governing text
5917636106: "the gate judges the underlying fields a member resolves to, not the cube's alias". It does not address a member that resolves to no single field.packages/spec/src/data/analytics.zod.ts: a measure'ssqlis "SQL expression or field reference", and a dimension's is "SQL expression or column reference".docs/adr/0021-analytics-dataset-semantic-layer.md), design principle: "Zero raw SQL / zero raw expressions — every escape hatch is at once a hallucination source, an injection risk, and an un-reviewable blob."Generated by Claude Code