Skip to content

fix(spec): a count roll-up saves without summaryOperations.field - #27533

Open
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-33w2eq
Open

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/project-thread-33w2eq

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

Requested by Yibo Wang · project thread

Fixes #27462

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: updated here. skills/objectstack-data/rules/field-types.md:125 spelled the summary config row ({ object, field, function, relationshipField?, filter? }), teaching field as always required; it now reads ({ object, field? (omit for `count`), function, relationshipField?, filter? }). The words are paid for in the same file: its sum example (:259) drops the comment "(ignored for count)", which the row now states. skills/objectstack-data/rules/relationships.md:201 is a sum example whose comment stays true; not edited.

Seat ruling. The spec follows its own describe. summaryOperations.field becomes optional when function is count and stays required for every other function, with the refusal for a missing field on a non-count function naming the function and the fix. Not "Studio fills an arbitrary field": that would store a value the engine ignores.

维护者速读

  • 改了什么:汇总字段(roll-up)用 count 时不再要求 summaryOperations.field;sum/min/max/avg 缺 field 仍被拒,报错指明函数和改法。Studio 汇总编辑器在 count 时不再显示「Child field to aggregate」。
  • 为什么改:规范自己的说明写着 count 忽略该字段,但校验却要求它,Studio 里存 count 汇总会 422,作者只能随便选一个子字段。
  • 风险与回滚:放宽接受集,原来能存的定义照旧能存(count 带 field 仍接受、仍忽略);引擎本来就不为 count 读字段。回滚即还原本 PR。
  • 先看:skills/objectstack-data/rules/field-types.md(:125 与 :259 两行),packages/spec/src/data/field.zod.ts(summaryFunctionReadsField 与 refuseSummaryWithoutField),ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx(SummaryConfigFields 的条件渲染)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/field.test.ts -t "summaryOperations",应见 count 无 field 通过、sum/min/max/avg 无 field 各被拒共 5 个用例全绿。
  • 需要你做的:席位 ACCEPT 后批准本 PR:它改了受管的 skills/objectstack-data/rules/field-types.md(一处表格行写明 count 可省略 field)。

What changes

Measured on origin/main 273137d2 (before). Spec parse, FieldSchema.safeParse of a summary with summaryOperations: { object: 'child', function: 'count' }:

count false [{"expected":"string","code":"invalid_type","path":["summaryOperations","field"],"message":"Invalid input: expected string, received undefined"}]
sum   false [{"expected":"string","code":"invalid_type","path":["summaryOperations","field"],"message":"Invalid input: expected string, received undefined"}]
count+field true {"object":"child","field":"amount","function":"count"}

The metadata write door, saveMetaItem({ type: 'object', … }) on the metadata-protocol stub engine (the #8783 harness), an object whose child_count is that count roll-up:

count-no-field:   422 INVALID_METADATA [{"path":"fields.child_count.summaryOperations.field","message":"Invalid input: expected string, received undefined","code":"invalid_type"}] rows=0
sum-no-field:     422 INVALID_METADATA [{"path":"fields.child_count.summaryOperations.field","message":"Invalid input: expected string, received undefined","code":"invalid_type"}] rows=0
count-with-field: saved rows=1

That is the card's 422 ("fields.child_count.summaryOperations.field expected string").

After (head 023da3b2), same probes:

count-no-field:   saved rows=1
sum-no-field:     422 INVALID_METADATA [{"path":"fields.child_count.summaryOperations.field","message":"`summaryOperations.function: 'sum'` needs `summaryOperations.field`, the child field the sum aggregates: name it (e.g. `field: 'amount'`), or use `function: 'count'`, which counts the child rows and reads no field.","code":"custom"}] rows=0
count-with-field: saved rows=1

The spec (packages/spec/src/data/field.zod.ts)

  • summaryOperations.field is z.string().optional(); its describe now reads "Field on the child object to aggregate. Required for sum, min, max and avg; count counts the child rows and reads no field, so a count may omit it (ignored when given)."
  • The rule lives once: export function summaryFunctionReadsField(fn) answers fn !== 'count' (an unset function answers true). The summaryOperations object carries .superRefine(refuseSummaryWithoutField), which reads that predicate and adds a custom issue at ['field'], so the error stays located at summaryOperations.field and travels with the shape wherever it is embedded (objectui's form.zod.ts takes SpecFieldSchema.shape.summaryOperations). This follows the CurrencyConfigSchema precedent (refuseFixedCurrencyWithoutCode, spec(field): fixed-mode currencyConfig with no defaultCurrency materialises 'CNY' — require the code, drop the default (narrowing; #27119 Q2 X, G3) #27157): a value-conditional requirement on an inner object.
  • The rule is value-conditional ("required unless function is count"), which no arm of the closed projectable list states (dependent-required is presence-only and its docblock names this if/then shape as staying dropped). So summaryOperations joins dropped-refinements.baseline.json at each of the 14 schemas that embed a field (data/Field, data/Object, data/ObjectExtension, system/AddFieldOperation, …); measured.droppedRefinementSites 892 → 906. publishedSchemasWithDroppedRefinements stays 244: all 14 were already in the ledger.
  • The two metadata-form sub-rows (field.form.ts roll-up composite, object.form.ts quick-add grid) drop required: true. Their help text ("ignored for count") is unchanged, so no translation bundle moves.

Readers of summaryOperations.field, measured

Reader On an absent field for count, before Change
objectql buildSummaryIndex (engine.ts) copies so.field into sourceField (undefined) none; SummaryDescriptor.sourceField becomes optional to say so
objectql aggregateSummaryValue already sends no field for count none
objectql createBatchSummaryAccumulator reads row[sourceField] only when fn !== 'count' guard added for a non-count descriptor without a field (reads no value, as an absent column)
lint ROLLUP_NON_NUMERIC_AGGREGAND (data-model-rules.ts) scoped to min/max, checks typeof … === 'string' none
spec checkFieldCompleteness / lint functional completeness reads summaryOperations presence only none
metadata-core FLS references keeps summaryOperations as is none
Studio SummaryConfigFields (ObjectFieldInspector.tsx) showed the control for count with "still required — pick any child field" reads summaryFunctionReadsField: no control for count, the note "Count counts the child rows, so it needs no child field." (en/zh)
ui snapshotSummaryPlan.ts (sum only) indexes summarized.fields[sum.field] type guard for the now-optional key

The hand-written content/docs/data-modeling/field-types.mdx table and its count example move with the rule; content/docs/references/** is regenerated (gen:docs), with api-surface/data.json and export-origins/data.json (gen:api-surface, gen:export-origins). check:authorable-surface is green and the base file does not move.

Tests

  • spec src/data/field.test.ts: a count with no field parses unchanged; sum/min/max/avg with no field are each refused with one custom issue at ['summaryOperations','field'] whose message names the function and the function: 'count' alternative; summaryFunctionReadsField answers.
  • objectql src/summary-rollup.test.ts: the parent and child definitions go through ObjectSchema.parse first; a count with no field seeds 0, counts single and batch inserts (4), holds through an update, and drops on delete (3).
  • objectql src/summary-batch-accumulation.test.ts: a count descriptor with no sourceField still adds one per admitted row.
  • app-shell ObjectFieldInspector.test.tsx: no aggregate-field control for count; picking "Count — child records" saves { object, function: 'count' } and FieldSchema accepts the saved field; control: a sum still offers the control and the spec still refuses it without a field.
Ablations (each restored, `git diff HEAD` empty)

Run through scripts/ablation-replace.mjs.

  • Refusal off (if (false /* ablated-27462 */ && …) in refuseSummaryWithoutField): spec field.test.ts 4 failed | 281 passed (the four refusal cases).
  • Widening off (field: z.string() again): 5 failed | 280 passed (the count case, plus the four refusal cases, whose issue becomes invalid_type).
  • Studio predicate off ({true /* ablated-27462 */ ? ( in SummaryConfigFields): ObjectFieldInspector.test.tsx 1 failed | 44 passed (the count control case).

Acceptance notes

  • A count that carries a field stays accepted and ignored, as before. Studio does not clear it when the function switches to count, so switching back to a sum keeps it.
  • Stored rows: the change only widens the accept set, so every row that loaded before loads the same. A non-count row saved without field could only have reached sys_metadata around the parse; the engine reads no value for it (the batch guard) instead of indexing row[undefined].

Verification

r2 (head 7533c782). Contract review 6496620533 (FAIL on 023da3b2): ②-1 was discharged by r1. ③-1: skills/objectstack-data/rules/field-types.md spells field? with "(omit for count)" in the summary row and drops the duplicate "(ignored for count)" comment from its sum example; the changeset adds Skills-docs: updated here and '@objectstack/skills': minor. No code change. ③-2 (the QA checklist item) is a follow-up card; docs/qa is untouched.

Skills readings (the token ratchet)
  • skills/objectstack-data/rules/field-types.md: 12623 bytes / 322 lines before and after; 3156 tokens against a ceiling of 3158 both times (check-skills-token-ratchet.mjs, exit 0). The ceiling does not move.
  • Whole bundle: ratcheted (authored) 207935 / 217474 tokens, bundle total 219187, unchanged; all skills/*/SKILL.md together 4614 lines, unchanged (no SKILL.md moves).
Derived gates on head `7533c782`: 116 of 116 run, every one exit 0

dispatch-gates --commands now derives 116 families (the 109 below plus 7 for the skills/** path: check-skills-token-ratchet.mjs and its --self-test, check:agent-test-spelling, check:pm-governed-merges, check:pm-skill-id-lint, check:skill-compatibility, check:skill-frame-sync). All 116 were run serially on 7533c782, with the full build from r0 still on disk; dispatch-gates --ran: "116 derived famil(ies) accounted for — 116 run, 0 NOT-MEASURED". check-changeset-no-major with this body as the event: exit 0.

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-engine-split-ratio.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/check-skills-token-ratchet.mjs --self-test :: exit 0
node scripts/check-skills-token-ratchet.mjs :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:agent-test-spelling :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-governed-merges :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:pm-skill-id-lint :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-compatibility :: exit 0
pnpm check:skill-frame-sync :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

r1 (head 39742179). The declaration is Metadata-protocol: added: a count roll-up with no summaryOperations.field is a newly accepted value. The changeset declares the same and grades every package it names minor; the commit moves only .changeset/27462-count-rollup-without-field.md. check-changeset-no-major with this body as the event and check-empty-changeset were rerun on that head; the readings below are from 023da3b2, whose tree differs from it only in that changeset.

All on head 023da3b2, under os-verify-lock (shared-box seconds; one hold of 2390 s for the chain).

Package suites
  • closure build '@objectstack/objectql^...' '@objectstack/lint^...' '@objectstack/platform-objects^...': exit 0
  • pnpm --filter @objectstack/spec test: exit 0 · Test Files 750 passed · Tests 22319 passed | 1 todo
  • pnpm --filter @objectstack/spec typecheck: exit 0
  • pnpm --filter @objectstack/spec test:repo: exit 0 · Test Files 86 passed · Tests 1419 passed
  • pnpm --filter @objectstack/objectql test: exit 0 · Test Files 483 passed · Tests 9090 passed
  • pnpm --filter @objectstack/objectql typecheck: exit 0
  • pnpm --filter @objectstack/metadata-protocol test: exit 0 · Test Files 279 passed | 4 skipped · Tests 21774 passed | 26 skipped
  • pnpm --filter @objectstack/lint test: exit 0 · Test Files 177 passed · Tests 7589 passed
  • pnpm --filter @objectstack/platform-objects test: exit 0 · Test Files 97 passed · Tests 1369 passed
  • '@object-ui/app-shell^...' build, then pnpm --filter @object-ui/app-shell type-check: exit 0
  • cd ui && vitest run packages/i18n/ packages/app-shell/src/views/approval-decision/ …/ObjectFieldInspector: exit 0 · Test Files 118 passed · Tests 1699 passed | 13 skipped (narrowed to the files the diff moves and their importers; CI's Test UI runs the rest)
Derived gates: 109 of 109 run, every one exit 0

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 109 families against merge base 273137d22; each was run serially on head 023da3b2 and recorded as CMD :: exit N. Three (spec check:skill-examples, check:dual-build-cjs-loads, check:i18n) exited 3 (prerequisite: built output) on the first pass and exit 0 after a full turbo run build --concurrency=2 (exit 0, 677 s held). dispatch-gates --ran: "109 derived famil(ies) accounted for — 109 run, 0 NOT-MEASURED".

node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0
node scripts/check-adr-0087-registration.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs --self-test :: exit 0
node scripts/check-adr-links.mjs :: exit 0
node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0
node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0
node scripts/check-closing-keyword-parity.mjs :: exit 0
node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0
node scripts/check-comment-mask-adoption.mjs :: exit 0
node scripts/check-comment-mask-corpus.mjs :: exit 0
node scripts/check-dev-prereqs.mjs --self-test :: exit 0
node scripts/check-doc-route-spelling.mjs --advisory :: exit 0
node scripts/check-doc-route-spelling.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs --self-test :: exit 0
node scripts/check-docs-section-name.mjs :: exit 0
node scripts/check-dts-emitted.mjs --self-test :: exit 0
node scripts/check-empty-changeset.mjs --base origin/main :: exit 0
node scripts/check-empty-changeset.mjs --self-test :: exit 0
node scripts/check-engine-split-ratio.mjs --self-test :: exit 0
node scripts/check-issue-citations.mjs :: exit 0
node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0
node scripts/check-plugin-teardown-shape.mjs :: exit 0
node scripts/check-sdui-manifest.mjs --self-test :: exit 0
node scripts/check-sdui-manifest.mjs :: exit 0
node scripts/check-section-landing-index.mjs --self-test :: exit 0
node scripts/check-section-landing-index.mjs :: exit 0
node scripts/ci/assert-turbo-remote-cache.mjs --self-test :: exit 0
node scripts/ci/plan-test-core-shards.mjs --self-test :: exit 0
node scripts/ci/prune-turbo-cache.mjs --self-test :: exit 0
node scripts/docs-audit/check-affected-docs.mjs :: exit 0
node scripts/docs-audit/check-drift-comment.mjs :: exit 0
node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0
node scripts/release-pending-publish.mjs --self-test :: exit 0
pnpm --dir ui exec vitest run scripts/ :: exit 0
pnpm --filter @objectstack/lint run check:doc-formula-expressions :: exit 0
pnpm --filter @objectstack/lint run check:doc-security-posture :: exit 0
pnpm --filter @objectstack/spec run check:api-surface :: exit 0
pnpm --filter @objectstack/spec run check:authorable-surface :: exit 0
pnpm --filter @objectstack/spec run check:browser-reachable-entries :: exit 0
pnpm --filter @objectstack/spec run check:docs :: exit 0
pnpm --filter @objectstack/spec run check:dual-source-exports :: exit 0
pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0
pnpm --filter @objectstack/spec run check:empty-state :: exit 0
pnpm --filter @objectstack/spec run check:entry-nameability :: exit 0
pnpm --filter @objectstack/spec run check:export-origins :: exit 0
pnpm --filter @objectstack/spec run check:exported-any :: exit 0
pnpm --filter @objectstack/spec run check:generated :: exit 0
pnpm --filter @objectstack/spec run check:liveness :: exit 0
pnpm --filter @objectstack/spec run check:llms-txt :: exit 0
pnpm --filter @objectstack/spec run check:skill-examples :: exit 0
pnpm --filter @objectstack/spec run check:skill-refs :: exit 0
pnpm --filter @objectstack/spec run check:strictness-ledger :: exit 0
pnpm --filter @objectstack/spec run check:variant-docs :: exit 0
pnpm --filter @objectstack/spec run check:yaml-examples :: exit 0
pnpm check:changeset-gate-self-tests :: exit 0
pnpm check:ci-filter-parity :: exit 0
pnpm check:closed-ceremony-text :: exit 0
pnpm check:corpus-claim-drift :: exit 0
pnpm check:cross-package-test-inputs :: exit 0
pnpm check:dispatcher-error-vocabulary :: exit 0
pnpm check:doc-anchors :: exit 0
pnpm check:doc-authoring :: exit 0
pnpm check:doc-frontmatter :: exit 0
pnpm check:docs-audit-scope :: exit 0
pnpm check:docs-redirects :: exit 0
pnpm check:docs-single-h1 :: exit 0
pnpm check:docs-spec-enumerations :: exit 0
pnpm check:docs-transcript-drift :: exit 0
pnpm check:driver-memory-census :: exit 0
pnpm check:dts-closure :: exit 0
pnpm check:dual-build-cjs-loads :: exit 0
pnpm check:durability-log-level :: exit 0
pnpm check:engine-double-contract :: exit 0
pnpm check:gitlink-declared :: exit 0
pnpm check:i18n :: exit 0
pnpm check:issue-citations :: exit 0
pnpm check:keyed-text-bounds :: exit 0
pnpm check:lean-entry-closure :: exit 0
pnpm check:logger-receiver-detach :: exit 0
pnpm check:merge-driver :: exit 0
pnpm check:nul-bytes :: exit 0
pnpm check:objectql-double-limit :: exit 0
pnpm check:org-identifier :: exit 0
pnpm check:page-declaration-shape :: exit 0
pnpm check:platform-object-tenancy-census :: exit 0
pnpm check:pm-changeset-deadline-census :: exit 0
pnpm check:pm-prior-rulings :: exit 0
pnpm check:published-files :: exit 0
pnpm check:published-readme-links :: exit 0
pnpm check:query-options-erasure :: exit 0
pnpm check:react-page-adapter-contract :: exit 0
pnpm check:refd-timer-probe :: exit 0
pnpm check:registry-log-declared :: exit 0
pnpm check:rest-log-spy-declared :: exit 0
pnpm check:role-word :: exit 0
pnpm check:skill-identifier-liveness :: exit 0
pnpm check:slot-lookup :: exit 0
pnpm check:sourcemap-no-sources-content :: exit 0
pnpm check:spec-docblock-symbol-anchors :: exit 0
pnpm check:spec-parsed-alias :: exit 0
pnpm check:system-context-census :: exit 0
pnpm check:test-source-alias :: exit 0
pnpm check:tier-file-adoption :: exit 0
pnpm check:type-check-coverage :: exit 0
pnpm check:type-check-debt :: exit 0
pnpm check:undeclared-dep-imports :: exit 0
pnpm check:vendor-version-stamps :: exit 0
pnpm check:watch-hint-literal :: exit 0
pnpm check:where-matcher :: exit 0

Dogfood: not run — no ! in the title and the Clause-②: arm is (widening), not (narrowing).


Generated by Claude Code

…rations.field

`summaryOperations.field` is optional when `function` is `count` (which
counts child rows and reads no field) and still required for sum, min,
max and avg, refused at `summaryOperations.field` with a message naming
the function and the fix. The predicate `summaryFunctionReadsField` lives
once in the spec; Studio's roll-up editor reads it and offers no
child-field control for a count.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh4Bxv1fXaUEeGaKDn
…ounts, and saves from Studio

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzh4Bxv1fXaUEeGaKDn
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:data tests tooling labels Oct 29, 2026
@github-actions

github-actions Bot commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/objectql, @objectstack/spec, touching 7 documentable anchor(s). ⚠️ 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/data.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/concepts/metadata-driven.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/external-datasources.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/field-types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/data-modeling/import-mappings.mdx (via sourceField (symbol, a field of interface SummaryDescriptor))
  • content/docs/data-modeling/validation-rules.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/troubleshooting.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/deployment/validating-metadata.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/getting-started/quick-reference.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/kernel/contracts/data-engine.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/backward-compatibility.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectql/types.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/protocol/objectui/concept.mdx (via FieldSchema (symbol, a top-level const))

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

  • content/docs/releases/v17/17-0.mdx (via FieldSchema (symbol, a top-level const))
  • content/docs/releases/v17/17-1.mdx (via FieldSchema (symbol, a top-level const))

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

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/spec/api-surface/data.json, packages/spec/dropped-refinements.baseline.json, packages/spec/export-origins/data.json) — pages documenting those are invisible to this run
  • 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 — 169 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 9c5131479d56426dd2c61ca78f8da6271de13171 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9c5131479d56426dd2c61ca78f8da6271de13171

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

Contract-review-claim: session_01AavokzJ5DndAwitDXvKy4U · head 023da3b2d48ccb56351bbbbf103aec7e5140e5cb · 2026-10-29T12:14Z

Triage seat, taking needs:contract-review on this head (#27062 (a)). The review runs at CONTRACT_REVIEW_TIER. If the lane claims or posts a record on this head first, triage withdraws.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 023da3b2d48ccb56351bbbbf103aec7e5140e5cb
Local-runs: none
Scope: packages/spec/src/data/field.form.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.form.ts + .changeset/27462-count-rollup-without-field.md, packages/spec/src/shared/strict-object.ts (closedObject), packages/spec/src/ui/component.zod.ts:6736, packages/spec/src/ui/view.zod.ts:3586 (metadata-form row schema), packages/spec/scripts/lib/refinement-projection.ts, packages/spec/dropped-refinements.baseline.json, packages/objectql/src/engine.ts (buildSummaryIndex), packages/objectql/src/summary-aggregate.ts, packages/objectql/src/summary-batch-accumulation.ts, packages/lint/src/data-model-rules.ts:845-905, ui/packages/app-shell/src/views/metadata-admin/inspectors/ObjectFieldInspector.tsx (SummaryConfigFields), ui/packages/app-shell/src/views/approval-decision/snapshotSummaryPlan.ts, skills/objectstack-data/rules/field-types.md:125,259, skills/objectstack-data/rules/relationships.md:201, docs/qa/platform-checklist/areas/studio-authoring.json (studio-authoring.summary-field-authoring), .github/workflows/pr-automation.yml (WHICH LEVEL), scripts/check-changeset-no-major.mjs

Inputs read: card #27462 body and all 3 comments (triage 6491275416, claim 6492937618, os-dev-report 6495136739); PR #27533 body, 22-file list, the one PR comment (docs-drift bot), the net diff against main; 70 check-runs on the head: every one completed, each success or skipped (the skipped rows are opt-in harnesses and matrix placeholders), none pending, none failed; rulings #27345 and #27157 via gh api GET. The premise was re-read on origin/main: summaryOperations.field is z.string() there, so a count without it is refused as the card describes.

① Derived judgments

  • Accept set, metadata door: summaryOperations: { object, function: 'count' } with no field is now accepted at FieldSchema and therefore at every embedding (data/Object, data/ObjectExtension, system/AddFieldOperation, the 14 ledger sites). RIGHT: this is what the card asked and what the describe always said. A count that carries field stays accepted and ignored (engine reads none for count). RIGHT, and no stored row changes meaning.
  • Accept set, every other function: sum/min/max/avg without field is still refused, now by a custom issue at ['summaryOperations','field'] with a prescription naming the function and the count alternative, instead of invalid_type. RIGHT: same refusal set, better message; the path is unchanged so Studio's objectFieldLocator (fields.NAME.summaryOperations.field) still lands it on the inspector. Zod 4 skips object-level checks when the base parse is aborted, so a missing or invalid function yields its own invalid_type and no second issue from the refinement, consistent with the head's exactly-one-issue tests.
  • Public TS surface of @objectstack/spec/data: one new export, summaryFunctionReadsField(fn); api-surface/data.json and export-origins/data.json carry it. RIGHT as an additive export; under ruling A (2026-10-21) a TS export alone does not set the level.
  • Public TS type of @objectstack/objectql: SummaryDescriptor.sourceField becomes optional. RIGHT: it is what buildSummaryIndex already stored for a field-less count; aggregateSummaryValue sends no field for count and the batch accumulator reads row[sourceField] only when fn !== 'count', now guarded for undefined. The one other sourceField reader in packages/objectql/src is the guarded line itself.
  • Published JSON Schema: field leaves required on every embedding; the value-conditional rule is dropped (no projectable arm states an if/then: the closed list in refinement-projection.ts is required-one-of, non-blank-string, dependent-required (presence-only), banned-keys, banned-key-pattern), so summaryOperations is recorded at 14 sites in dropped-refinements.baseline.json, 892 to 906, with the schema count unchanged at 244. RIGHT and honest; check:generated, check:strictness-ledger, check:authorable-surface are green on the head.
  • Metadata forms (field.form.ts, object.form.ts): the field sub-row drops required: true. RIGHT, and the only expressible option: the metadata-form row schema (view.zod.ts:3586) has required: z.boolean().optional() and no conditional row attribute, so "required unless count" cannot be stated in the form; the door refuses a field-less sum one seam later with the prescription. Help text unchanged, so the generated translation bundles do not move (check:generated green).
  • Studio editor: SummaryConfigFields reads the same predicate; no child-field control for count, the note replaces the old "still required, pick any child field" copy (en/zh). RIGHT: one rule, two readers, as the seat ruling asked ("not Studio fills an arbitrary field").
  • snapshotSummaryPlan.ts: type guard for the now-optional key on a sum. RIGHT, no behaviour change.
  • Lint ROLLUP_NON_NUMERIC_AGGREGAND: scoped to min/max and guarded by typeof childFieldName === 'string'. No change needed. RIGHT.
  • Metadata-protocol kind: declared none. WRONG, see ②.

② Semver level

  • Changeset .changeset/27462-count-rollup-without-field.md: @objectstack/spec patch, @objectstack/objectql patch, @object-ui/app-shell patch, @object-ui/i18n patch, with Clause-②: yes (widening) and Metadata-protocol: none in both the PR body and the changeset body. The Check Changeset run and check-changeset-no-major are green, but that gate only checks the declared kind against the levels; which kind is right is this section's job.
  • The kind is wrong. The WHICH LEVEL rule (pr-automation.yml:757, ruling A 2026-10-21 on changeset levels: minor only when the metadata protocol changes (added / narrowed / semantic), patch otherwise; release notes grouped by what the reader must do (maintainer ruling A, 2026-10-21) #25105; AGENTS.md Post-Task step 3) sets the level by "the change to what a metadata author can write". This diff changes exactly that: an author can now write a count roll-up without summaryOperations.field, which every spec before this head refuses with 422 at the door. none is defined as "everything else: description or error text, non-authorable internal types, REST, drivers, services, the console"; a widened authorable accept set is none of those. The stated reason added takes minor ("an app that writes a new key must be able to name a minor floor") applies with more force here: an app that writes the newly accepted shape against an older spec is not silently stripped, it is refused. A requirement loosened is the mirror of "a requirement tightened" (narrowed), and the PR's own account calls it an accept-set widening.
  • Must-fix (②-1): Metadata-protocol: added in the PR body and the changeset body, and '@objectstack/spec': minor (the moved package that carries the widening; the other three may stay patch). With a non-none kind the Skills-docs: line is owed for real (review-checklist.md L18), see ③/④.
  • Clause-②: yes (widening) is right. (widening) adds no BREAKING banner and no ADR-0087 disposition is owed; none is written. Right.
  • Clause-②: yes (widening). The contract review is owed at this tier and this record is it.

③ Boundary flags

  • open_questions: none declared. Nothing to escalate.
  • Deviation 1, Skills-docs: none edited, naming field-types.md:259 and relationships.md:201: the two named lines are sum examples with field: 'amount' // ignored for count and stay true. The dev missed the row that spells the shape: skills/objectstack-data/rules/field-types.md:125 writes summaryOperations ({ object, field, function, relationshipField?, filter? }), marking relationshipField and filter optional and field not, which after this head teaches a requirement the spec no longer has. Under the declared none the line was not owed; under the corrected kind (②-1) it is, and none edited is false. Must-fix (③-1), folded into ②-1: spell field? at :125 with a word that a count omits it (or a one-line count example beside the sum one), and write Skills-docs: updated here.
  • Deviation 2, ui vitest narrowed to the moved files and importers: discharged by the head's check-runs, Test UI 16/16 shards green and Type Check UI green.
  • Deviation 3, an 18 KB probe file left at the dev container's root: outside the repo and the diff, nothing in the tree; no action here.
  • Docs-drift bot (advisory): of the 12 hand-written pages it lists, the only one that restates this rule is content/docs/data-modeling/field-types.mdx, and the diff moves it (table row and the count example drop field: 'id'); import-mappings.mdx is listed through an unrelated sourceField. The 2 release-owned pages are untouched. Right.
  • Follow-up owed, should-fix (③-2): docs/qa/platform-checklist/areas/studio-authoring.json, item studio-authoring.summary-field-authoring (the card's own QA source), acceptance clause 3 reads "saved spec-valid: summaryOperations { object, field, function }" and its verify expects both summaries to carry "the authored object, field and function". On this head the count saves and reads back with no field, so the clause now scores a correct run as a failure. A checklist-author follow-up (revision 3: the count carries no field, the sum does), not a change on this PR.

④ Design judgment

packages/spec/src/data/field.zod.ts (against the seat ruling in 6492937618, the #27157 refuseFixedCurrencyWithoutCode precedent it cites, and its first consumers: the metadata door and ObjectQL.buildSummaryIndex)

  • The rule lives once, summaryFunctionReadsField, and the refinement reads it; the message names the function and the fix. Matches the ruling's two halves: optional for count, required with a prescription for the rest, and Studio does not fill an arbitrary field.
  • .superRefine(refuseSummaryWithoutField) sits on the strictObject node (a closedObject, a ZodObject subclass) before .optional(), so FieldSchema.shape.summaryOperations carries the check into component.zod.ts:6736 and into objectui's form.zod.ts:1170 (which walks and clones defs, checks included). Verified by reading, not by the PR body.
  • Tried to break it: (a) summaryOperations absent: the optional short-circuits, no refinement. (b) function absent or not in the enum: the object parse is aborted, Zod 4 skips the check, one issue. (c) count with a stale field: accepted, engine ignores it (aggregateSummaryValue omits field for count; the accumulator never reads it for count). (d) a non-count descriptor with no field reaching the registry around the parse: the accumulator's new guard reads no value; aggregateSummaryValue would send field: undefined, which is the pre-existing behaviour for that already-refused shape, not widened here. (e) the summaryField/targetField aliases: strict-object suggestion vocabulary, refused as unknown keys as before. No finding.
  • The published JSON Schema can no longer say "field required on a sum"; the ledger records it at every site and the door still refuses. This is the ADR-sanctioned disposition for a value-conditional rule. Should-fix, not blocking: none beyond ②.
  • Finding: the one new public export is summaryFunctionReadsField(fn: string | undefined), typed on string rather than the function enum. Acceptable: Studio feeds it '' || undefined, and the describe says an unset function answers true. No change asked.

packages/spec/src/data/field.form.ts and packages/spec/src/data/object.form.ts (against the schema they mirror and the metadata-form row schema at view.zod.ts:3586)

  • Both sub-rows drop required: true, the comment in each points at the predicate. The row schema offers required as a plain boolean and no conditional form, so the alternative "required unless count" cannot be authored; dropping it is the design, and the help text "ignored for count" still tells the author which case needs it. metadata-form-zod-reconciliation and the declared-rows pin are unmoved and green. The reconciliation's object.fields.summaryOperations entry still describes the quick-add grid as capturing object / field / function, which is true as a control set. No finding.

Governed skill surface (not in hits, read because the Skills-docs line claims it): skills/objectstack-data/rules/field-types.md:125 teaches field as a required member (see ③-1). Must-fix as part of ②-1.

Must-fix: ②-1 (kind added, spec minor, both declaration sites) with ③-1 (field-types.md:125 and Skills-docs: updated here). Should-fix: ③-2 (checklist item revision, follow-up card). Everything else holds against the cited code.

Implemented-by: claude/project-thread-33w2eq
Reviewed-by: session_01AavokzJ5DndAwitDXvKy4U

VERDICT: FAIL


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Regen-provenance: 6496620533 · 023da3b2d48ccb56351bbbbf103aec7e5140e5cb → 3974217946302492aed78fc4fb09da3e4e26cf85 · git diff --name-only 023da3b2 39742179 → .changeset/27462-count-rollup-without-field.md only

The one commit after the reviewed head touches the changeset alone, so record 6496620533 keeps governing this head, and its verdict is still FAIL. The commit answers the declaration half of must-fix ②-1: Metadata-protocol: added now stands in the PR body and the changeset, and @objectstack/spec is minor. Still open on this head:

  • ③-1 (folded into ②-1 by the record): skills/objectstack-data/rules/field-types.md:125 still spells summaryOperations ({ object, field, function, … }) with field required, and the PR body still says Skills-docs: none edited. With a non-none kind that line is owed (skills/** is governed, so either edit it here at Tier H or write Skills-docs: follow-up owed — #N against a filed card).

A head that closes it gets a delta review from this seat.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract-review-claim: session_01L6bhgV4fMGJHSdSdx3u9mK · head 7533c78 · 2026-10-29T15:40Z


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

triage#1 · os-sales · session_01L6bhgV4fMGJHSdSdx3u9mK

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 7533c782584d6c8f9bda75d66ad1a3ed02d319c4
Local-runs: none
Scope: packages/spec/src/data/field.form.ts, packages/spec/src/data/field.zod.ts, packages/spec/src/data/object.form.ts, skills/objectstack-data/rules/field-types.md + .changeset/27462-count-rollup-without-field.md, packages/skills/package.json (the published package the skills edit moves), scripts/check-skills-token-ratchet.mjs:657 (the gate on the rules file), skills/objectstack-data/rules/relationships.md:70,201,292, skills/objectstack-data/evals/objects-fields-relationships.json, .claude/skills/pm-dispatch/references/review-checklist.md:18, docs/qa/platform-checklist/areas/studio-authoring.json (③-2, via card #27608)

Delta review. The governing record on this PR is 6496620533 (FAIL on 023da3b2), carried to 39742179 by Regen-provenance 6500781856 with ②-1's declaration half already discharged there. The head has moved by ONE commit, 7533c782 "docs(skills): a count roll-up omits summaryOperations.field", read via the compare endpoint 39742179...7533c782: 2 files, +4 −2, skills/objectstack-data/rules/field-types.md (two lines) and .changeset/27462-count-rollup-without-field.md (two lines). Under contract-review.md L42 a commit that only lands findings gets a delta review from the same reviewer; this record is it. Every section the delta touches is judged afresh below; everything the delta does not touch stands as judged in 6496620533, named where it applies.

Inputs read: card #27462 body and all 4 comments (triage 6491275416, claim 6492937618, os-dev-report 6495136739, claim amendment 6499259221); PR #27533 body (r2), its 23-file list, all 5 PR comments (docs-drift bot, triage claim, record 6496620533, Regen-provenance 6500781856, this seat's claim 6501245922), the net diff against main, the compare delta; the 69 check-runs on 7533c782: 59 success, 9 skipped (opt-in harnesses and matrix placeholders), 1 cancelled (PR-body guards, run 40900431333 started 15:19:50Z, superseded by concurrency; the same-named run 40900531011 on this head completed success at 15:20:45Z), 0 in_progress, 0 failure. The touched files were read in full at the head with git show; the rules file was also read at 39742179 and at origin/main.

① Derived judgments

  • Every accept-set and public-surface judgment in record 6496620533 ① stands: the delta moves no code, no schema, no form row, no generated output (git diff --stat 39742179 7533c782: the changeset and the rules file only). FieldSchema.shape.summaryOperations at the head is unchanged from the reviewed tree: object required, field z.string().optional(), function the five-value enum, relationshipField?, filter?, .superRefine(refuseSummaryWithoutField) on the strict object, summaryFunctionReadsField exported.
  • The one item 6496620533 ① named WRONG, "Metadata-protocol kind: declared none", is now Metadata-protocol: added in the PR body and in the changeset (r1, confirmed in the head tree). RIGHT: a count roll-up without summaryOperations.field is a newly accepted authorable value.
  • New in the delta, the governed skills surface: skills/objectstack-data/rules/field-types.md:125 now spells the summary config as ({ object, field? (omit for \count`), function, relationshipField?, filter? }). Judged against field.zod.ts:1939-1970at the head key by key:objectrequired,fieldoptional,functionrequired,relationshipFieldandfilteroptional. RIGHT, and the qualifier "(omit forcount)" ties the optionality to the one function the spec exempts, so a reader is not taught that a sum` may omit it. The spec says a count "may omit it (ignored when given)"; the row says "omit", which is the seat ruling's guidance ("not Studio fills an arbitrary field") stated for the authoring agent. RIGHT.
  • :259, the sum example's comment, drops "(ignored for count)" and reads "// child field". RIGHT: the row now carries the count rule, the example is a sum, and the comment stays true for a sum. The file's byte count is unchanged, 12623 at 39742179 and at 7533c782 (+20 −20 bytes).
  • Public surface of @objectstack/skills (packages/skills, publishConfig.access: public, no private, shipping skills/** through sync-catalog.mjs): one published rules file changes two lines. The changeset now names it. RIGHT, see ②.

② Semver level

  • Changeset .changeset/27462-count-rollup-without-field.md at the head: @objectstack/spec minor, @objectstack/objectql minor, @objectstack/skills minor, @object-ui/app-shell minor, @object-ui/i18n minor; body carries Clause-②: yes (widening), Metadata-protocol: added, and a Skills-docs: updated here line.
  • ②-1 of 6496620533 asked for kind added and @objectstack/spec minor at both declaration sites. Both stand in the head tree and in the PR body. Closed. The other three packages went to minor rather than staying patch; the record allowed either, check-changeset-no-major is green, and grading up on a kind that takes "at least minor on each package the diff moves" (claim amendment 6499259221, AGENTS.md L1023) is not a finding.
  • New in the delta: '@objectstack/skills': minor. Owed: the delta edits a file the published @objectstack/skills package ships, and review-checklist.md L25 requires a changeset for every touched published package (precedent: .changeset/26777-report-export-options-rule.md and nine others on main name this package for skills-only edits). The level follows the kind on the PR. RIGHT. Check Changeset is green on the head.
  • Clause-②: yes (widening) stands as judged in 6496620533: no BREAKING banner, no ADR-0087 disposition owed. The review is owed at this tier and this record is it.

③ Boundary flags

  • open_questions: none declared (os-dev-report 6495136739). Nothing to escalate.
  • ③-1 of 6496620533 (folded into ②-1): field-types.md:125 taught field as required and the body said Skills-docs: none edited. At the head the row spells field? with "(omit for count)" and the PR body reads Skills-docs: updated here, naming :125, :259 and relationships.md:201. Each claim was checked against the tree: :125 reads exactly the text the body quotes; :259 reads "// child field" as the body says; relationships.md:201 is a sum example whose comment "child field to aggregate (ignored for count)" is unchanged and stays true (a count that carries field is accepted and ignored). review-checklist.md L18 ("the Skills-docs: line present and true") is met. Closed.
  • The changeset's own Skills-docs: line adds "so the file's size is unchanged": true, 12623 bytes before and after, and the published-skills token ratchet (check-skills-token-ratchet.mjs:657, ceiling 3158 on origin/main and on the head) reads ceil(12623/4) = 3156 on the head. On the merge tree the check-runs ran against (origin/main has since moved the file's Currency section, 12631 bytes, ceiling unchanged), the byte-neutral delta reads ceil(12631/4) = 3158, at the ceiling and not over it; Lint & Repo Gates and Lint · repo & tooling gates are green on that tree. No local run was needed for this arithmetic.
  • ③-2 of 6496620533 (should-fix, follow-up card for the QA checklist item studio-authoring.summary-field-authoring): card qa checklist: studio-authoring.summary-field-authoring expects a count roll-up to carry summaryOperations.field, which #27533 makes optional #27608 exists, open, created 2026-10-29T14:43:59Z, titled for exactly that revision; docs/qa/** is untouched on this PR as the record asked. Discharged.
  • Deviations 2 and 3 of the os-dev-report stand as judged in 6496620533 (discharged by the head's check-runs; outside the tree).
  • PR author is objectstack-fleet[bot], so no login note is owed here.
  • Docs-drift bot (advisory, 6495135782): computed on the merge of this head into 9c513147; its list is the same as on the reviewed head and stands as judged in 6496620533 ③. The release-owned pages are untouched.

④ Design judgment

skills/objectstack-data/rules/field-types.md (the file the delta edits; judged against the spec it teaches at the head, field.zod.ts:559-585 and :1939-1970, the seat ruling in 6492937618, and the card's scenario: an author adding a count roll-up with no child field)

  • The row at :125 is the only place in skills/** that spells the summaryOperations shape, and it now matches the strict object key for key. Every other roll-up in the skill is a sum example that carries field (field-types.md:254-264, relationships.md:62-76, :195-210, :288-294); none shows a count carrying a field, so nothing in the catalog still teaches the pattern the ruling rejected. The skill's eval objects-fields-relationships.json asks for "a summary roll-up field on the parent" and names no function or field, so it is unaffected.
  • Tried to break it: (a) an agent reading field? and omitting field on a sum: the qualifier "(omit for count)" scopes the optionality, and the door refuses with a prescription naming the function and the fix; (b) an agent writing a count with a stale field: accepted and ignored, as the spec describe says, and relationships.md:201 still tells it so; (c) a count with no field written from this row: parses at FieldSchema, saves at the metadata door, counts in the engine (6496620533 ④, unchanged tree). The row now answers the card's scenario where before it contradicted it. No finding.
  • Cost: the words are paid for inside the same file (the duplicate comment at :259), size unchanged, the ratchet ceiling does not move. This is the discipline the ratchet's header asks for. No finding.
  • Noted, not a finding: relationships.md:201 keeps "(ignored for count)" while field-types.md:259 drops it; both comments are true on a sum example, and the normative shape is the :125 row.

packages/spec/src/data/field.zod.ts, packages/spec/src/data/field.form.ts, packages/spec/src/data/object.form.ts: not touched by the delta; byte-identical to the tree record 6496620533 judged (git diff --name-only 39742179 7533c782 lists neither). The ④ judgments in 6496620533 stand: the rule lives once in summaryFunctionReadsField, the refinement travels with FieldSchema.shape.summaryOperations into every embedding, the form rows cannot state a conditional requirement and drop required: true with help text unchanged. The ④ paragraph there on the governed skill surface (":125 teaches field as a required member, must-fix as part of ②-1") is the one item the delta answers, judged above.

Must-fix: none. ②-1 (kind added, spec minor) and ③-1 (field-types.md:125, Skills-docs: updated here) of record 6496620533 are closed in the head tree; should-fix ③-2 is discharged by card #27608. The delta adds nothing outside those findings and breaks nothing the previous record judged.

Implemented-by: claude/project-thread-33w2eq
Reviewed-by: session_01L6bhgV4fMGJHSdSdx3u9mK

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-support-ai · session_01WGufzgbJhFoXYsLSe5WSPP

维护者速读 (席位终稿)

  • 改了什么:汇总字段(roll-up)用 count 时不再要求 summaryOperations.field;sum/min/max/avg 缺 field 仍被拒,报错指明函数和改法。Studio 汇总编辑器在 count 时不再显示「Child field to aggregate」。受管文件 skills/objectstack-data/rules/field-types.md 的配置行同步写明 count 可省略 field。
  • 为什么改:规范自己的说明写着 count 忽略该字段,校验却要求它,Studio 里存 count 汇总会 422,作者只能随便选一个子字段。
  • 风险与回滚:只放宽接受集,原来能存的定义照旧能存(count 带 field 仍接受、仍忽略);引擎本来就不为 count 读字段。回滚 = revert 本 PR。
  • 先看:skills/objectstack-data/rules/field-types.md(:125 与 :259 两行),再看 packages/spec/src/data/field.zod.ts 的 summaryFunctionReadsField / refuseSummaryWithoutField。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/data/field.test.ts -t "summaryOperations",count 无 field 通过、sum/min/max/avg 无 field 各被拒,5 个用例全绿。
  • 需要你做的:批准本 PR(受管 skills/**,Tier H)。契约复核 PASS 6501439971,Head-sha 7533c782584d6c8f9bda75d66ad1a3ed02d319c4;批准后由席位落地。

席位意见:接受。分诊契约复核在当前 head 上 PASS(6501439971),上一轮 FAIL(6496620533)的两条 must-fix(声明 added + spec minor;skills 第 125 行)都已在树上关闭,should-fix ③-2 由 #27608 接走。

席位核对 · ACCEPT · domain:spec seat 3 · os-support-ai · session session_01WGufzgbJhFoXYsLSe5WSPP · 2026-10-29T15:49Z

Head: 7533c782584d6c8f9bda75d66ad1a3ed02d319c4 · card #27462 · claim 6492937618, amended 6499259221

  1. 形态: a draft PR to main carrying Fixes #27462: 23 paths (+253 / −45), inside the claim's surface as amended. summaryOperations.field becomes optional for count with a refinement that refuses it missing on every other function, one predicate (summaryFunctionReadsField) read by spec, objectql and Studio, regenerated references and ledgers, and the governed skills row. check-governed-merges reports GOVERNED (1 of 23 paths: skills/objectstack-data/rules/field-types.md), so the landing tier is H: a maintainer's hand or an authorized APPROVED review, then the seat lands it.
  2. 前提实测: the premise holds. Before/after probes in the PR body: on main a count with no field is 422 at the metadata door; on the head it saves, and a sum with no field is refused with a prescription. The contract review (6501439971, PASS, on this head) checked the skills delta against field.zod.ts and the seat ruling (6492937618); the earlier record's ④ judgments of the untouched spec files stand.
  3. 门禁 check-runs (on this head): 69 runs read at 2026-10-29T15:49Z: every run is success or an expected skip. One PR-body guards run shows cancelled; it was superseded by a later run on the same head that is success (the body-edit trigger). Check Changeset is success; the changesets are minor with Clause-②: yes (widening) / Metadata-protocol: added, matching the claim amendment.
  4. 接受的偏差: relationships.md:201 keeps "(ignored for count)" on a sum example while field-types.md:259 drops it; both are true, noted by the reviewer and not a finding.
  5. 范围外去向: the QA checklist's summary-field-authoring text is carried by card qa checklist: studio-authoring.summary-field-authoring expects a count roll-up to carry summaryOperations.field, which #27533 makes optional #27608. Skills-docs: updated here (the one governed row); no other skills text teaches a count carrying a field.
  6. 落地条款: Tier H. The seat readies the PR, labels it needs-user-decision and requests the governed approvers. It does not arm auto-merge. After an authorized approval the seat lands it (ready, auto-merge, follow to MERGED) and lands spec/Studio: a count rollup cannot be saved without summaryOperations.field, which the spec itself says is ignored for count #27462. A head move after this record sends it back to review.

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

spec/Studio: a count rollup cannot be saved without summaryOperations.field, which the spec itself says is ignored for count

2 participants