Skip to content

fix(service-analytics): a dataset refuses a measure currency that contradicts its field's fixed code, and a derived measure across two different fixed codes - #27991

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-2w318c
Oct 30, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/project-thread-2w318c

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Elon · project thread
Fixes #27579

Clause-②: yes (narrowing)
Metadata-protocol: narrowed
Skills-docs: follow-up owed — skills/objectstack-data/rules/field-types.md, skills/objectstack-ui/rules/dashboards.md, content/docs/data-modeling/currency.mdx, content/docs/api/data-api.mdx

维护者速读

  • 改了什么:数据集编译时新增两条只针对 fixed 币种的拒收。度量写的 currency 和被汇总字段的 fixed 币种不同,拒收;派生度量(任何运算)的两个金额操作数落在两个不同的 fixed 币种,拒收。都返回 DATASET_INVALID / 400,并给出改法。
  • 为什么改:这两种写法今天会算出不报错的错数(EUR 字段被标成 USD,或 USD 减 EUR 不带币种)。维护者裁决 6486427200(ADR-0144 Open item 12,字母 A)。
  • 风险与回滚:这是收窄,以前能编译的这两种写法现在被拒。仓内数据集元数据实测 0 处命中;已部署租户存的数据集无法从仓库调查,它们会在注册与请求时被拒,错误信息写明改法。回滚即撤本 PR。
  • 先看:packages/services/service-analytics/src/measure-currency-coherence.ts(两个 fixed 分支与 fixed 标记),packages/services/service-analytics/src/__tests__/record-currency-analytics.test.ts(三道门的 pin 与对照)。
  • 两分钟验证:pnpm --filter @objectstack/service-analytics exec vitest run src/__tests__/record-currency-analytics.test.ts,预期 42 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

assertMeasureCurrenciesCoherent (measure-currency-coherence.ts), called once from compileDataset, gains the fixed arm of each guard. The record-mode arms from #27338 are unchanged.

  • Shape 1. An aggregated measure whose currency differs from its field's fixed code refuses with DATASET_INVALID / 400. The message names the measure, the field, the object and both codes, and the fix: delete the measure's currency, and the column takes the field's code. A currency that restates the fixed code is accepted.
  • Shape 2. A derived measure, for every op (sum, difference, ratio, product), whose monetary operands resolve to two different fixed codes refuses with DATASET_INVALID / 400, naming both operands, their codes and the fix (derive it from measures in one currency, or drop the operand in the other). A derived sum / difference over one fixed code resolves to that code, so a derived measure built on it is judged at the next level. A count operand is not compared.
  • What a fixed code is. A resolved code now carries fixed: true when it is the field's fixed code, restated by the measure or not. A derived sum / difference is fixed when every operand is. Shape 2 compares only fixed codes.
  • Not judged, unchanged: a fixed code against the tenant currency; a measure currency over a dynamic field. Because the ruling lists the second as legal, an operand whose code is such an explicit one is not a fixed code, and Shape 2 does not compare it (see Acceptance notes).
  • Moved pins. Starting from feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies #27338's first draft (80b98fb8, dropped at 507ed8f9): currency-mode-relay.test.ts's m_explicit_fixed restates the fixed field's EUR instead of contradicting it with JPY, and query-dataset.test.ts's chain pin moves to a dynamic field.
  • Changeset: @objectstack/service-analytics minor, with a BREAKING paragraph, both declaration lines, and the ADR-0087 marker not-required (no-migration-prescription).

Tests

record-currency-analytics.test.ts, guard block (real engine over SQLite, through AnalyticsServicePlugin):

  • Shape 1 refused at registration, naming the fix; restated code accepted (control); a currency over a dynamic field not judged (control).
  • Shape 2 refused for each of the four ops, naming the fix; refused one level up through a derived sum of one fixed code; a fixed operand restated by its measure code compares by that one code (control); an explicit code over a dynamic field against a fixed one is not judged (control); amount per invoice over each fixed code, and a fixed amount with the tenant currency, accepted (controls).
  • The request door and the Studio draft preview: queryDataset, with and without previewDrafts, refuses both shapes with DATASET_INVALID / 400 naming the measure; a restated code is served with currency: 'EUR' on both (control).
  • The old "not judged" controls for the two fixed shapes are replaced by the pins above.

Acceptance notes

  • The in-tree survey: 0 of either shape. I searched dataset metadata in examples/**, packages/platform-objects/**, packages/runtime/src/seed-datasets.ts, ui/examples/** and the skills evals. 16 files declare measures:, and none sets a measure currency. The two derived measures in the tree (chart-gallery.dataset.ts, revenue-pulse.dataset.ts) are both ratios of two counts. A deployed tenant's stored datasets cannot be surveyed from the repository: a stored dataset with either shape now refuses at registration and at query, and its message names the edit.
  • One reading the ruling leaves open, for the contract review. The ruling says Shape 2 compares operands that "resolve to two different fixed codes", and lists "a measure currency over a dynamic field" as legal. feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies #27338's first draft compared any two resolved codes, so it also refused a derived measure over an explicit JPY on a dynamic field and a fixed USD. This PR follows the ruling's words: only fixed codes are compared, and a CONTROL pins that pair as accepted. Refusing that pair too would narrow further, and would need its own ruling.
  • Skills and docs: follow-up owed. skills/objectstack-data/rules/field-types.md says "Under 'fixed'/'dynamic' a measure's own currency wins", which this PR makes false for a contradicting fixed code. skills/objectstack-ui/rules/dashboards.md (the currency chain comment), content/docs/data-modeling/currency.mdx (Roll-ups and datasets) and content/docs/api/data-api.mdx (the currency chain) describe the record-mode refusals only. None is in this card's file surface. Grep: git grep -n -iE "measure.{0,40}currency|currency.{0,40}measure" -- 'skills/**' 'content/docs/**'.
  • dataset-compiler.ts. The comment above the assertMeasureCurrenciesCoherent call names only the record-mode arms. It is outside the file surface and is incomplete rather than false, so this PR leaves it.
  • The ADR-0144 Open item 12 dated note stays with the epic PM, as the ruling says.

Verification

Head c053f4ca (base bacb3e6a).

Red on the base, ablations, restore
  • The new and moved pins against the unchanged measure-currency-coherence.ts of bacb3e6a: 10 red, 83 green over the three files. Red: Shape 1 at registration, Shape 2 for each of the four ops, Shape 2 one level up, and both shapes at queryDataset and at the draft preview. Every control was green (restated code, dynamic field, count operand, tenant currency, explicit code over a dynamic field).
  • Through scripts/ablation-replace.mjs (anchor hit 1 → 0, restored blob == HEAD, git diff HEAD empty after each). The tests import the source, so no build was needed between legs.
    • Shape 1 arm disabled: 3 red (registration, queryDataset, preview), 39 green.
    • Shape 2 fixed pair disabled: 7 red (four ops, one level up, queryDataset, preview), 35 green.
    • An explicit code over a dynamic field marked fixed: 1 red (the CONTROL that pins it as not judged), 41 green.
Package suite, gates, dogfood
  • pnpm --filter @objectstack/service-analytics test: 196 files, 4752 passed, 282 skipped. typecheck exit 0 (its include is src, tests included).
  • node scripts/pm/dispatch-gates.mjs --commands: 59 derived families, all run. --ran reconciliation: "59 derived, 58 run, 1 NOT-MEASURED, 0 UNRUN". 58 exited 0. NOT MEASURED: check:dual-build-cjs-loads, exit 3 (prerequisite: a whole-tree build; this run built only the dogfood closure and @objectstack/service-analytics). CI runs it.
  • pnpm lint: exit 0.
  • Full dogfood battery (the (narrowing) arm), pnpm --filter @objectstack/dogfood test under the verify lock at c053f4ca: VERDICT command-exit 0, Test Files 357 passed and 3 skipped (360), Tests 2875 passed and 20 skipped (2895), no failures.

Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_016WV4cyL63KrU8pu6fycLHW

…cting its field's fixed code, and a derived measure across two different fixed codes

Both compile-time guards in measure-currency-coherence.ts gain their fixed
arm (ADR-0144 open item 12, ruled letter A). The two chain pins move off the
contradicting fixed shape: currency-mode-relay restates the fixed code, and
query-dataset's chain pin moves to a dynamic field.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016WV4cyL63KrU8pu6fycLHW
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 30, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

3 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 58 of 210 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 10 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 5e1342a6e07b287e0b829332ac565872f568666f → packageMentionDocs.

Which tree this was computed on

This run read content/docs from d937ded5d5f41b036e4ae14b521762e6d643a3b4 — the merge of head c053f4cabaa4929a80aabba4320f406846a74d12 into base 5e1342a6e07b287e0b829332ac565872f568666f, 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 d937ded5d5f41b036e4ae14b521762e6d643a3b4 && git checkout d937ded5d5f41b036e4ae14b521762e6d643a3b4
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e1342a6e07b287e0b829332ac565872f568666f c053f4cabaa4929a80aabba4320f406846a74d12 && git checkout -B drift-repro 5e1342a6e07b287e0b829332ac565872f568666f && git merge --no-ff c053f4cabaa4929a80aabba4320f406846a74d12

node scripts/docs-audit/affected-docs.mjs --json 5e1342a6e07b287e0b829332ac565872f568666f

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract-review-claim: session_016WV4czuTWNgKR22fKv7jia · head c053f4cabaa4929a80aabba4320f406846a74d12 · 2026-10-30T06:40Z

The lane seat runs the CONTRACT_REVIEW_TIER review this PR owes (Clause-②: yes (narrowing)).

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c053f4cabaa4929a80aabba4320f406846a74d12
Local-runs: none
Scope: packages/services/service-analytics/src/measure-currency-coherence.ts, packages/services/service-analytics/src/tests/record-currency-analytics.test.ts, packages/services/service-analytics/src/tests/currency-mode-relay.test.ts, packages/services/service-analytics/src/tests/query-dataset.test.ts, .changeset/27579-fixed-currency-measure-guards.md + packages/services/service-analytics/src/analytics-service.ts (compile, registerDataset, queryDataset, enrichResultColumns currency chain), packages/services/service-analytics/src/plugin.ts (sourceFieldMeta relay), packages/services/service-analytics/src/dataset-compiler.ts:860, packages/spec/src/ui/dataset.zod.ts (DatasetMeasureSchema, DerivedMeasureOp), PR #27338 fea128ce / 80b98fb8 / 507ed8f9

Face: narrowing (record-recognisers --paths returns no hits; the changeset's Clause-②: yes (narrowing) decides). Read: card #27579, ruling 6486427200 on #8345 (letter A), triage 6515345174, dispatch-27579.md, PR body, os-dev-report 6516665481, the 5-file diff, PR #27338 and its dropped first draft.

① Derived judgments

  • Shape 1 (guard 1, fixed arm) — field.defaultCurrency !== undefined && field.defaultCurrency !== m.currency, aggregated measures only (m.derived ⇒ field undefined). defaultCurrency is relayed by sourceFieldMeta ONLY under currencyMode === 'fixed' (plugin.ts:1447), so a dynamic field (schema-materialised default included) cannot trip it. Exactly the ruled shape. Message names dataset, measure, declared code, field, object, the fixed code and the fix (delete currency; column takes the field's code). Pinned red → green at registration, queryDataset, previewDrafts.
  • Shape 2 (guard 2, fixed arm) — pairRefused adds a.fixed && b.fixed && a.code !== b.code to the unchanged record pair. Every op (sum/difference/ratio/product, the whole DerivedMeasureOp enum) pinned. fixed is set only when the code IS the field's fixed code (restated or not); a derived sum/difference is fixed iff every operand is. Message names derived measure, op, both operands and their codes, and the fix. Exactly the ruled shape in the ruled cases.
  • Controls, all pinned and all unchanged in behaviour: restated fixed code accepted (registration + both request doors, column prints EUR); measure currency over a dynamic field not judged; count operand (count(*)) not compared — per_invoice, usd_per_invoice, eur_per_invoice; fixed vs tenant (usd_and_tenant) not judged; record-mode arms — the record tests in record-currency-analytics.test.ts are untouched by the diff and provablyDiffer / the record half of pairRefused are byte-equivalent to recordPairDiffers.
  • Q2, explicit currency over a dynamic field in Shape 2 — the PR marks it fixed: false so sum(tenant_as_jpy, usd) compiles (pinned as CONTROL); fea128ce refused it. The ruling's Shape 2 text is "operands resolve to two different fixed codes" and it lists "a measure currency over a dynamic field … stays legal" among the not-judged. An explicit JPY over a dynamic field is not the field's fixed code under any reading of the words, so the PR follows the ruling's literal text and the dev was right not to widen. I do not FAIL for this. But the ruling's reason (Prime Directive 12, "an answer that is not a number in any currency") applies to JPY+USD exactly as to EUR+USD, and the first draft the ruling names as the starting point refused it — so the maintainer may have meant "two different known codes". needs_decision (not blocking): whether Shape 2 should also compare an explicit measure currency (over a dynamic field) against a fixed code. If yes, it is a further narrowing on its own card; this PR's CONTROL pin is the one line that would flip.
  • Q3, fixed propagation — ratio/product resolve to undefined (common = []), so a derived measure built on one has no monetary operand there: no false refusal (sum(usd_per_invoice, eur) compiles, matching the column chain, where a derived measure carries no currency unless explicit). sum/difference propagate code + fixed, pinned one level up (usd_twice → mixed). No silent miss the ruling's text requires.
  • Q4, both doors — registerDataset (analytics-service.ts:2588) and queryDataset (:2688) both call the one compile() → compileDataset → assertMeasureCurrenciesCoherent (dataset-compiler.ts:860); the previewDrafts branch (:2698) and the engine query come after. Refusal precedes any query. Pinned for both shapes × {queryDataset, previewDrafts}.
  • Q6, moved pins — git diff fea128ce pr-27991 on currency-mode-relay.test.ts and query-dataset.test.ts is empty: byte-identical to the draft the ruling names as the starting point. m_explicit_fixed JPY→EUR (restates) and the chain pin moves to a dynamic field: both are the exact shape the ruling now refuses, so the old pins could not stand; the dynamic-wins and tenant-ctx assertions they carried are kept. Not hiding a regression.
  • Q7, in-tree callers — independent survey: 16 files declare measures:; 0 set a measure currency (the one textual hit is a prose example in skills/objectstack-ui/rules/dashboards.md:299, the owed follow-up); the only derived measures (revenue-pulse, chart-gallery) are ratio of two count(*). 0 of either shape; dev's count confirmed.
  • Q9, dogfood — dev reports the full battery at c053f4ca: 357/360 files (3 skipped), 2875 passed / 20 skipped, 0 failed, under the verify lock. Package suite 196 files / 4752 passed, red-on-base 10/83 and three ablations reported with restore. Check-runs at the head at review time: 21 success, 13 skipped, 12 in_progress (Lint & Repo Gates, Test Core 1–8, Type Check ×3), 0 failures.

② Semver level

@objectstack/service-analytics minor with a BREAKING paragraph — as the ruling fixes (the fixed group refuses major before GA). Clause-②: yes (narrowing) and Metadata-protocol: narrowed on both PR body and changeset, both recognised spellings (clause2-line.mjs). ADR-0087 marker <!-- adr-0087: not-required (no-migration-prescription) … --> matches the gate's regex and names a category ADR-0087 §750 lists. Skills-docs: follow-up owed — … present; seat files the card and rewrites the line to #N. No spec/contract key touched; dataset-compiler.ts untouched (file surface held). Correct.

③ Boundary flags

  • count over a currency field is resolved as monetary. resolvedOf never reads m.aggregate, so { aggregate: 'count', field: 'amt_usd' } resolves to USD, fixed and ratio(count(amt_usd), sum(amt_eur)) would refuse, while the ruling says "a non-monetary operand (a count) is not compared". The pins cover count(*) only. Not a must-fix: (a) the record arm on main has had the identical corner since feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies #27338 (a count over a record amount resolves to record), (b) the fixed column chain on main (enrichResultColumns, monetary = !!mc.currency || meta?.type === 'currency', no aggregate test) labels that very count column with the field's code, so the guard refuses what the column would print, (c) 0 in tree. Recommend one change on both seams on the owed follow-up card: gate resolvedOf and the fixed chain's monetary on AMOUNT_AGGREGATES, as the record chain already does.
  • Derived measure with its own currency over a fixed common code — { derived: sum(eur, eur), currency: 'JPY' } compiles: Shape 1 reads "the aggregated field's fixed code" and a derived measure has none, so this is outside the ruled text, but it prints EUR amounts as JPY one level up — the same wrong-label the ruling refuses at level 0 (the record arm does refuse the derived analogue). For the follow-up card, not this PR.
  • Clause-②: yes (narrowing) is glossed in clause2-line.mjs as "widens one surface and narrows another"; this diff only narrows. The ruling fixed the spelling, so noted, not re-ruled.
  • dataset-compiler.ts:860 comment names the record arms only (dev flagged; outside surface; incomplete, not false).
  • Error messages: leak nothing beyond the dataset's own name/object/measures and the object's declared fixed code; no tenant, ctx or row data. Good.

④ Design judgment

The narrowing holds against the published spec on main: DatasetMeasure.currency is "Display currency code", and enrichResultColumns lets it win over a fixed field — so before this PR the spec let an author print EUR amounts labelled JPY, and computeDerived let USD−EUR compile to a bare number. Both refusals make the spec's own semantics real at the one compile seam, with the fix in the message, and take no capability away that produced a correct answer. The fixed flag on Resolved is the minimal carrier: the record half of guard 2 is unchanged and the fixed half is one conjunction, so the two arms cannot drift. Test design is sound: red-on-base, every op, one level up, both doors, and five controls incl. the open Q2 reading pinned as a CONTROL so a later ruling flips one visible line. The re-application from fea128ce is faithful where the ruling asks (moved pins identical) and deliberately departs only on the Q2 reading, which it surfaces.

Implemented-by: claude/project-thread-2w318c
Reviewed-by: session_016WV4czuTWNgKR22fKv7jia (seat subagent)

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#3 · os-elon-musk · session_016WV4czuTWNgKR22fKv7jia

Seat ACCEPT · PR #27991 · head c053f4cabaa4929a80aabba4320f406846a74d12 · 2026-10-30T07:00Z

维护者速读

Dataset compile now refuses two fixed-currency shapes with DATASET_INVALID / 400, and each message names the fix. The first is a measure currency that contradicts its field's fixed code. The second is a derived measure whose monetary operands resolve to two different fixed codes. Before, both compiled and returned a wrong number without an error. Both are narrowing: 0 in-tree datasets are hit, and a deployed tenant's stored dataset with either shape is refused at registration and at query time. This implements the maintainer ruling 6486427200 (ADR-0144 Open item 12, letter A).

席位核对

  • Shape: draft → main, Fixes #27579, 5 files: measure-currency-coherence.ts, three test files under service-analytics/src/, and a changeset. All are inside the claimed surface.
    • check-governed-merges --pr 27991: NOT governed, +172 / −54.
  • Contract review: PASS at this head, record 6516794387 (CONTRACT_REVIEW_TIER). Both arms refuse exactly the ruled shapes. All five controls and the record-mode arms from feat(spec,service-analytics): record-currency analytics — grouped answers name a per-row currency, the cube door refuses an ungrouped amount aggregate, compile guards on measure currencies #27338 are unchanged. Both doors compile before any query. The moved pins are byte-identical to fea128ce.
  • Open reading, not blocking: Shape 2 compares only fixed codes, which is the ruling's text. An explicit measure currency over a dynamic field, combined with a fixed code in another currency, still compiles, and one CONTROL pins that. Whether to refuse that pair too has gone to the maintainer as its own decision. If the answer is yes, it is a further narrowing on its own card, and this PR does not wait for it.
  • Verification (card thread): 10 pins red on base, every control green. Three ablations went red and restored clean. 58 of 59 derived gate families exit 0, and 1 is NOT MEASURED locally (check:dual-build-cjs-loads, which CI runs). The full dogfood battery passed 357/360 files and 2875 tests with 0 failures. The in-tree survey found 0, confirmed independently by the review.
  • Declarations: Clause-②: yes (narrowing), Metadata-protocol: narrowed, @objectstack/service-analytics minor with a BREAKING paragraph, ADR-0087 not-required (no-migration-prescription).
  • Skills-docs: a follow-up is owed. skills/docs (currency): four texts teach that a dataset measure's own currency always wins over a fixed field; after #27579 a contradicting code is refused #27994 carries the four texts (two skills rules, currency.mdx, data-api.mdx), to be edited after this merges.
  • Gate check-runs on this head: 48 total, 35 success, 13 skipped by the path filter, 0 failed. mergeable_state: clean.
  • Landing: Tier S, under the maintainer's landing grant for this term (2026-10-29 22:05Z, seat thread). Ready + auto-merge through the relay.

ACCEPT

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

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

service-analytics: refuse fixed-versus-fixed cross-currency measures at dataset compile (multi-currency, epic #8345)

1 participant