Skip to content

feat(spec)!: a joined report refuses a block that binds no dataset, at blocks[i].dataset, naming the block (#21702) - #21712

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21702-joined-block-dataset
Oct 4, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-21702-joined-block-dataset

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #21702
Clause-②: yes (narrowing)

Triage direction 5977861131 (enforce), claim 5977924610. The joined arm of ReportSchema's refinement now refuses each block of a joined report that binds no dataset, at blocks[i].dataset, naming the block, with the prescription to bind the block to a dataset. The refinement comment "each block dataset-bound" and the reports guide's type-table cell "(each block dataset-bound)" were declarations; they are now enforced.

What changes

  • The refusal (packages/spec/src/ui/report.zod.ts). A per-block arm inside the joined branch of ReportSchema's superRefine, right after the existing "needs blocks" check. For each block whose dataset is undefined it adds one custom issue at ['blocks', i, 'dataset']. The message comes from a module-private builder, joinedBlockDatasetRequired, worded like the neighbouring joined refusals:

    a `joined` report draws each block from that block's own `dataset`, and block `NAME` binds none, so nothing queries it and it draws no rows. Bind the block to a dataset: set its `dataset` to the dataset whose measures (`values`) and dimensions (`rows`) it shows.
    

    NAME is the block's name. A block whose name is empty (its own too_small issue, which does not stop the refinement) is named by position instead, as blocks[1].

  • What stays accepted. JoinedReportBlockSchema.dataset stays .optional(): blocks is read on a joined report only, so the requirement lives on the joined arm, and a block on a non-joined report is not judged (pinned). No type, export or JSON Schema key changes. The block's dataset .describe() now reads "Dataset name to bind (ADR-0021); a joined report refuses a block without one", and content/docs/references/ui/report.mdx is regenerated with gen:docs (two table rows).

  • The ADR-0087 kit, in feat(spec)!: FlowSchema refuses a create_record, update_record or delete_record node whose static objectName is a stored-metadata table, with the runtime's prescription (#21654) #21687's shape:

    The fragment and entry text state each decision in words and carry no tracker number. No tombstone (no key is removed) and no D2 conversion (only the author knows which dataset a block was meant to show).

  • content/docs/ui/reports.mdx: no edit. No sentence there became false; the type-table cell became true. See Acceptance notes.

Census, at base 16d241a6af, before any edit

git grep for a type: 'joined' report across examples/**, packages/**, skills/**, content/docs/**, docs/**, scripts/** and apps/** gave 33 lines in 15 files: CHANGELOG quotations, the spec's own comments, and these reports:

where joined reports unbound blocks
examples/app-showcase TaskOverviewReport 1 0
content/docs/ui/reports.mdx example 1 0
packages/lint validate-chart-bindings.test.ts (raw stacks, never parsed) 4 0
packages/platform-objects report-form-echo-decisions.test.ts 2 0
packages/spec tests (report.test.ts, filter-save-door-face-parity.test.ts, joined-report-block-type.test.ts) and the report-joined-chart-removed conversion fixture 10 0
packages/metadata-protocol protocol.invalid-metadata-422-face-inventory.test.ts 1 1, unbound on purpose (below)
skills/** 0 0
  • Triage measured hotcrm 4054ec26 (1 joined report, 0 unbound blocks) and cloud 2205b530 (none). I took those readings as given. I read hotcrm's src/sales/reports/churn.report.ts once, to shape the preservation fixture. Every block binds a dataset: three bind account_metrics and the fourth, recently_closed_lost, binds opportunity_metrics. So triage's line "every block binds account_metrics" is slightly off; its conclusion, zero unbound blocks, stands.

  • objectui (read only, at 2e818d0b51 and at this repo's pin ab18797215). Studio's joined-report authoring can save a block with no dataset. That producer is this narrowing's reach:

    • ReportDefaultInspector.tsx renders the blocks through SchemaForm with the spec reportForm "Joined blocks" repeater;
    • RepeaterField's add() seeds a blank row with every column undefined;
    • the row's dataset column is free text and not required (the block's derived JSON Schema required is ["name"], measured);
    • the bundled ReportSchema (clientValidation.ts) and the save door both accepted the result.

    Filed as Studio report inspector: a joined report's blocks repeater saves a block with no dataset, which objectstack's ReportSchema now refuses at blocks.N.dataset objectui#11601: category ①, no labels, dedupe query and hit count in its body. After the spec bump, Studio's live validation and the save door both refuse such a block at blocks.N.dataset.

  • The renderers, at the pin: DatasetReportRenderer's joined branch passes String(block.dataset ?? '') to each block's table, and that table's query hook goes idle on an empty name (:443). A report whose blocks all lack one fails isDatasetReport and falls through to the presentation bridge. DrillDownDrawer's isDatasetBoundReport lists the records instead of drawing it.

Fixture triage: one test-only file outside the claim's file surface

protocol.invalid-metadata-422-face-inventory.test.ts section 5 (the joined-report chart door pins) left its block unbound on purpose: the door's author-time gate refuses an unresolvable dataset with chart-dataset-unknown, and the stub engine had no dataset universe. The new refusal turned 2 of its 23 tests red (Tests 2 failed | 21 passed): the container case got a second issue at blocks.0.dataset, and the CONTROL was refused at blocks.0.dataset. That second red is the metadata save door (422 INVALID_METADATA, writeFace: 'meta-envelope') refusing the probe shape.

  • Disposition: add the declaration. The block binds task_metrics, and the harness gains an optional makeProtocol({ datasets }), which registers that dataset through registry.listItems('dataset'). Every other caller passes nothing, so the registry lists nothing, as before. Result: Tests 23 passed (23).
  • The registration is load-bearing. I mutated the CONTROL to call makeProtocol() without datasets (scripts/ablation-replace.mjs, anchor 1 → 0, blob f718099c2d → 9dc51bd7b8). Predicted 1 red / 22 green; observed Tests 1 failed | 22 passed. The failure was chart-dataset-unknown at reports[0].blocks[0].dataset, "Declared datasets: (none)". Restore: blob back to f718099c2d, which equals HEAD, and git diff HEAD is empty.
  • This file is outside the claim's declared file surface. It is reported in the dev report as a deviation.
  • Queue check: fix(metadata-protocol)!: the save door refuses every hook with no body, including one with neither a body nor a handler (#21689) #21706, queued at this writing, appends a section to the same file at line 570 and later. A local git merge-tree of this head with that queue head exits 0. The file is not merge=os-regen routed, so that local answer is also GitHub's.

Doors, tested and probed

  • Pins (packages/spec/src/ui/report-joined-block-dataset.test.ts, 16 tests):
    • the triage probe shape is refused once per block at blocks.0.dataset and blocks.1.dataset, each naming its block; a bound block beside an unbound one draws one issue, at the unbound index; an empty name is named by position;
    • the new issue joins the other joined-arm refusals rather than replacing them;
    • taking the advice parses;
    • CONTROLS: a block on a non-joined report, and JoinedReportBlockSchema parsed on its own, both still parse;
    • doors: defineReport throws; the registered report type schema refuses (and accepts the bound report); ObjectStackDefinitionSchema, the stack parse objectstack validate runs, refuses at reports.1.blocks.{0,1}.dataset; defineStack answers STACK_SCHEMA_INVALID / 422;
    • preservation: the showcase TaskOverviewReport (mirrored byte for byte) parses with output equal to its input, and a fixture shaped like hotcrm's customer_churn_signals parses, gaining only the drilldown default;
    • ledger: one D3 entry at protocol 18 with no conversion; the step-18 rationale names it; no JoinedReportBlock:dataset tombstone, with a control on a known tombstone.
  • objectstack validate, the real CLI (packages/cli/bin/run.js, built here). Fixtures were temporary, in the session scratchpad, outside the repo; @objectstack/spec resolved to this worktree's build.
    • The triage probe stack (two blocks, each only name + label): exit 1, "code": "STACK_SCHEMA_INVALID", defineStack validation failed (2 issues) at reports.0.blocks.0.dataset and reports.0.blocks.1.dataset, each with the message above.
    • CONTROL, the same report with both blocks bound to a declared dataset: exit 0, "valid": true.
    • The showcase app (examples/app-showcase/objectstack.config.ts, four reports including TaskOverviewReport): exit 0, "valid": true.
  • The CLI door's verdict is this arm's, proven through dist. The CLI reads @objectstack/spec through dist, so the mutation was rebuilt:
    • mutated leg: the ctx.addIssue of the new arm became a globalThis marker assignment (anchor 1 → 0, blob fadd536165 → 2cbfdb9feb); @objectstack/spec rebuilt; ablation-dist-preflight.mjs found the marker in dist (exit 0); the probe then gave exit 0, "valid": true. That reproduces the card's reading at main;
    • restore leg: blob back to fadd536165, which equals HEAD, and git diff HEAD is empty; rebuilt; --absent found the marker in none of 228 built files (exit 0); the probe gave exit 1 STACK_SCHEMA_INVALID again.

Ablation of the refusal, at the pins (predicted first)

scripts/ablation-replace.mjs replaced the arm's ctx.addIssue(...) with a no-op (anchor 1 → 0, blob fadd536165 → 45f34370c5). The pins import ./report.zod relatively, which resolves to src, so no build was needed. Predicted: 8 red, the 4 refusal pins and the 4 door pins; 8 green, the advice, the 2 controls, the 2 preservation pins and the 3 ledger pins; report.test.ts untouched. Observed: Tests 8 failed | 80 passed (88) over the pin file and report.test.ts. The 8 were exactly the predicted ones. Restore: blob equals HEAD (fadd536165), and git diff HEAD is empty.

Verification (all at head c4e3ab9631 unless noted)

  • @objectstack/spec full suite (vitest run --project local --maxWorkers=2): Test Files 613 passed (613), Tests 18201 passed | 1 todo.

  • pnpm --filter @objectstack/spec typecheck (tsc, scripts, test layer: check:test-typecheck: OK) and pnpm --filter @objectstack/metadata-protocol typecheck: exit 0. --listFiles shows both edited test files in their programs.

  • Consumers that parse a joined report: platform-objects report-form-echo-decisions.test.ts 35 passed; lint validate-chart-bindings.test.ts 47 passed; @objectstack/example-showcase whole suite 32 files, 399 passed; metadata-protocol face-inventory 23 passed.

  • pnpm --filter @objectstack/spec check:generated: one artifact stale before regeneration (content/docs/references/**), regenerated with gen:docs; then check:generated exit 0.

  • Derived gates: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 115 commands. Every one was run, with its exit code recorded before any pipe. --ran reconciled 115 derived, 115 run, 0 NOT-MEASURED, 0 UNRUN.

    • Two first exited 3 (prerequisite not met, packages with no dist): check:skill-examples and check:dual-build-cjs-loads. Both were re-run green after building those packages. All 115 exited 0.
    • Among them: check:adr-0087-registration, check:changeset-no-major, check:migration-registry, check:spec-changes, check:upgrade-guide, check:authorable-surface, check:api-surface, check:docs, check:doc-authoring, check:issue-citations, check:nul-bytes, check:cross-package-test-inputs.
  • eslint, narrowed and proven:

    1. population: eslint.config.mjs's **/*.{ts,…} block covers all five changed .ts files;
    2. --format json: 5 files, 0 errors, 0 warnings;
    3. invariance: the config enables no type-aware linting (no parserOptions.project, as its own comment states), so this diff cannot move a verdict on an untouched file.

    The repo-wide pnpm lint is CI's.

  • Declared to CI: the path-scheduled jobs (Test Core shards, Dogfood, Temporal Conformance, Build Core), the whole-workspace type-check lanes and every downstream consumer suite of @objectstack/spec beyond the four above.

Acceptance notes (observations, not filed)


Generated by Claude Code

claude added 4 commits October 4, 2026 08:17
…t blocks[i].dataset

The joined arm of ReportSchema's refinement now enforces the
'each block dataset-bound' contract the schema comment and the reports
guide already state. ADR-0087 D3 entry and step-18 rationale fragment.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…to a registered dataset

A joined report's block with no dataset is now refused at blocks.0.dataset,
so the fixture that left its block unbound (to dodge chart-dataset-unknown)
binds it and the harness registers the dataset it names.

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
…the joined block dataset refusal

Claude-Session: https://claude.ai/code/session_01T9u38rswFp5Rw8DswRUReJ
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s).

1 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/analytics.mdx (via ReportSchema (symbol, a top-level const))
What this run could not see
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 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 — 138 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 7e0066af7a8e05709dc60096c69bc85e299d0331 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 7e0066af7a8e05709dc60096c69bc85e299d0331

⚠️ 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 7e0066af7a8e05709dc60096c69bc85e299d0331 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 37194495975 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Console Pin Gate — 失败步骤: Build the Console SPA at the pinned objectui SHA

    ✗ Built console still carries the PUBLISHED @objectstack/spec.
    

↳ 失败原因 是判读的关键:超时(Test timed out in … / Hook timed out in …)多半是负载/时序,不是本 PR 的回归;
断言(AssertionError: …)才指向真实的行为改变。两者的 FAIL 行长得一模一样,只有这一行能区分。

⚠️ 断言这一侧有一类例外,判据是断言在测什么,不是它是不是 AssertionError。 断言的对象是产品行为(一个值、一个形状、一次拒收)⇒ 照上面读:真实的行为改变,去查,⛔ 不要重排掉;
断言的对象是这次实验自身的有效性前提(跑完的耗时、负载下的先后、任何只在时间预算内才成立的条件)⇒ 它跟超时是同一类,同样对负载敏感,重排一次是合法的判别手段。
识别是机械的:断言的消息或它比较的值本身点名了一段时长、一个时间戳、一个耗时计数。实测过的一对 —— AssertionError: SecurityPlugin.init() ran: expected false to be true 测的是产品行为(真回归);
AssertionError: this run took over a second, so second-precision stamps could have differed too: expected 1006 to be less than 1000 测的是实验前提:它守护的那条不变式当时是绿的,同一个 head 原样重排一次即成功。
穿着 AssertionError 外衣的时间测量,仍然是时间测量。(⛔ 这只改「怎么读一次红」,不改「哪些测试可以重排」——后者由别处管。)

跨 PR 相同签名(24h,按失败测试文件聚合):

  • ⚠️ 本次没有可用的聚合签名(日志里没有能解析出测试文件名的 FAIL 行)—— 这不是「没有同签名的其他 PR」,是这一轮没测到。跨 PR 聚合本次不可用,请手工比对其他 PR 的同类评论。
  • ⚠️ 24h 评论账本没读完(超过 5 页仍未读到窗口尽头),所以上面的「不同 PR 数」是下界,不是全量。

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 11 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 看上面的「跨 PR 相同签名」;已有汇总 issue ⇒ flaky/环境问题实锤,去那张 issue 上谈,修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

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 protocol:ui size/l tests tooling

Projects

None yet

2 participants