Skip to content

fix(lint,metadata-protocol,rest,runtime): refuse a sum/avg roll-up the engine cannot recompute, and answer a landed child write's success - #28781

Merged
objectstack-fleet[bot] merged 18 commits into
mainfrom
claude/project-thread-1minft
Nov 1, 2026
Merged

objectstack-fleet[bot] merged 18 commits into
mainfrom
claude/project-thread-1minft

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Requested by Leehom · project thread

Fixes #28661

Clause-②: no (narrowing)
Metadata-protocol: narrowed
Skills-docs: none — skills/objectstack-data/rules/relationships.md and rules/field-types.md teach summaryOperations only with sum over a numeric amount and count; no file under skills/** teaches a sum/avg over a text field, and every taught example stays admitted.

维护者速读

  • 问题:已发布的汇总字段对文本子字段做 sum/avg 时,子记录每次增改删都答 500,但写入其实已落库;客户端重试 create 会产生重复记录。
  • 前半:汇总规则从只在 os lint 里跑,搬进作者规则注册表(gating),os validate、os build、os lint 和运行时保存/发布门都会拒绝;草稿的 _diagnostics 也报出来。
  • 子对象不存在(qsf_nope)没有拒绝:全量 dogfood 测到 unmanaged install 会先写父对象、后写它汇总的子对象,拒绝会打断这种安装;按派单第 2 条放行,列为待决问题。
  • 后半:写入已落库、只是父记录汇总重算失败时,REST 和运行时分发器的 POST/PATCH/DELETE 答写入本身的成功响应(201/200,与正常成功同一个 body,不加新字段、不加新错误码);汇总列过期这件事按字段只在日志里以 error 说一次。
  • protocol 的 createData/updateData/deleteData 仍抛 ERR_SUMMARY_RECOMPUTE(带 written),导入和种子加载的契约不变;未改 packages/spec/**。
  • 需要你看:子对象不存在要不要以别的方式处理(见 Acceptance notes);表单提交、批量写、clone、MCP 等其它写入门未在本 PR 处理。

What changes

Half 1 — the doors refuse a roll-up the engine refuses at every recompute.

  • packages/lint: the roll-up rule (lintSummaryRollups, data-model-rules.ts) leaves lintDataModel and becomes a gating entry of the authoring-rule registry (authoring-rules.ts), on all three commands and at the runtime object door (runtimeTypes: ['object']). It judges:
    • sum/avg over a child field outside the aggregate × field-type table (isAggregateCompatibleWithFieldType, @objectstack/spec's AGGREGATE_FIELD_TYPE_COMPATIBILITY — the predicate the engine's aggregate door already refuses with) → rollup/non-numeric-aggregand at objects.<parent>.fields.<field>.summaryOperations.field, naming the accepted child types and count;
    • min/max (the arm os lint already reported, same message and fix) — now also gating at os validate and os build;
    • a summaryOperations.object naming no object the pass holds stays SILENT at every door — see Acceptance notes for the measured reason.
  • os explain rollup/non-numeric-aggregand now covers sum/avg.
  • The draft read already folds the publish verdict into _diagnostics (Studio/metadata: a draft the editors, the draft _diagnostics and the Problems tab all call valid is refused only at publish — and one such draft rolls back the whole package release #26707), so a draft carrying such a roll-up now reads valid: false with the same entry publish refuses with.

Half 2 — a child write that landed answers its success.

  • packages/metadata-protocol: createData, updateData and deleteData build the answer the write earned when the engine throws ERR_SUMMARY_RECOMPUTE ("the triggering records WERE written"), attach it to the refusal under a non-enumerable symbol, and rethrow — so every existing caller (import runner, seed loader) reads the refusal exactly as before. The carrier lives in @objectstack/types (summary-recompute-answer.ts), the one package the protocol, rest and runtime all depend on (rest has the protocol as a dev dependency only).
  • packages/rest (crud-routes.ts, stale-summary-answer.ts) and packages/runtime (domains/data.ts): POST, PATCH, DELETE /data/:object[/:id] answer that earned body with the write's own success status (201/200), plus the dropped-fields header the success path sets. No member, header or code is added; the body is the one the same write answers when every roll-up recomputes.
  • The stale summary column is logged at error, once per child object and summary field, with the consequence and the fix (AGENTS.md degradation-log rule); the engine's own warn per failure is unchanged.

Gate fallout. check:engine-double-contract's by-id seam scan now also reads a receipt returned through a same-function const answer = { … } (the const opts = { where: { id } } reading it already makes, applied to the answer), with an A/B self-test pair; its ledger learns the new test file's engine double. Four docs transcripts move from 59 to 60 author-time rules (main's #28775 had already moved them from 58 to 59).

Tests

All red on the merge base 2678600d and green on the head.

  • packages/lint — data-model-rules.summary-rollup.test.ts: sum/avg over text refused with the exact message, path and fix; table parity over every FieldType for both functions; sum over percent refused, avg admitted; sum over a number admitted; a missing child raises nothing; lintDataModel no longer reports it; os explain facts. runtime-gate.object-writes.test.ts: the door refuses sum/avg over text at objects.qsf_parent.fields.probe_sf.summaryOperations.field, admits sum(amount), admits a parent whose child does not exist yet (with and without a closure), refuses the CHILD write that turns a summed field into text, does not charge a write for a stored parent already in violation; roster pins list the rule. data-model-rules.fix-lines.test.ts: one-line fix.
  • packages/metadata-protocol — protocol.summary-rollup-publish-gate.test.ts (9): active save 422 INVALID_METADATA for sum and avg; the draft saves, reads _diagnostics.valid: false with rollup/non-numeric-aggregand, and both publishMetaItem and publishPackageDrafts refuse; sum(amount) admitted; missing child admitted by save and publish; a draft child in the same package publish is judged with its parent (numeric admitted, text refused). Base: 5 red / 4 green (the controls).
  • packages/rest — data-write-summary-recompute-refused.test.ts (5, real SqlDriver on better-sqlite3): POST 201 with the created record, persisted once, child_sum 295 / child_count 3; PATCH 200, child_sum 230; DELETE 200 {object,id,success:true}, child_sum 95 / child_count 2; one error line naming qsf_parent.probe_sf; the protocol's createData still throws ERR_SUMMARY_RECOMPUTE with written. Base: POST/PATCH/DELETE answered 500.
  • packages/runtime — domains/data-write-summary-recompute-refused.test.ts (4): the dispatcher's /data door answers the same 201/200 bodies, persists once, recomputes the valid roll-ups, and logs once through the host logger. Base: 4/4 red.
  • Ablations (each mutation landed on disk, restored to the HEAD blob, git diff HEAD empty): sum/avg arm off → lint 6 red; registry runtimeTypes emptied → lint 4 red; earned-answer symbol detached (dist rebuilt) → REST 4 red; REST POST relay disabled → REST 1 red; runtime data.ts at base → runtime 4 red.
  • Suites: @objectstack/lint 7730/7730, @objectstack/rest 7203 passed (352 skipped), @objectstack/runtime 9194 passed (19 skipped), @objectstack/metadata-protocol 21958 passed (26 skipped; 4 reds in two ssl-redaction files from main were a stale post-merge dist and went green after rebuilding spec), @objectstack/types 846/846.
  • Full dogfood on the head fe13d22d (after merging main at 5493b884, closure rebuilt): Test Files 374 passed, 3 skipped (377); Tests 3034 passed, 28 skipped (3062). On the same head: @objectstack/lint 7775/7775, and the three new door files 9 / 5 / 4 passed.

Acceptance notes

  • The narrowing is already denied by published text. @objectstack/spec's AGGREGATE_FIELD_TYPE_COMPATIBILITY (the aggregate × field-type table) does not admit sum/avg over text, and the engine's aggregate door refuses the pair with its own message ("sums 'memo', a declared text field, which the engine does not sum") — on a roll-up after the write committed, every time. The lint rule's published message for rollup/non-numeric-aggregand already refused the min/max arm on os lint. No roll-up the engine could recompute is newly refused.
  • The missing-child half is admitted (open question). The first full dogfood on this branch refused it and turned 9 cases of install-local-unmanaged-mode.dogfood.test.ts red: the unmanaged install saves crm_opportunity (line_total = sum(crm_opportunity_line_item.amount)) one active save before the child exists. Per the order, the arm was removed and the case is left open; a missing child keeps reading 0 / null, and nothing fails.
  • Census. examples/app-showcase (24 objects, 9 roll-ups), examples/app-crm (6, 1), examples/app-todo (1, 0) through the built rule: 0 findings. examples/app-multi-package declares no summary field. hotcrm's one roll-up is sum over a currency field: admitted.
  • Doors not changed. Form submit (form-routes.ts), the batch and *ManyData routes, cloneData, the MCP create_record bridge and callData's ObjectQL fallback (no protocol registered) still surface ERR_SUMMARY_RECOMPUTE as before. Only the card's three data doors, on both transports, answer the earned body.
  • No packages/spec/** edit.

Verification

Live probe on a stock showcase boot, card steps 2–4. Sign in as the seeded admin, install a scratch package, author qsf_parent (child_sum = sum(amount), child_count = count) and qsf_child (memo text, amount number, parent master_detail), publish, then seed P and two children:

pnpm dev -- --fresh -p <port>
curl -X POST /api/v1/auth/sign-in/email -d '{"email":"admin@objectos.ai","password":"admin123"}'
curl -X POST /api/v1/packages -d '{"manifest":{"id":"app.qsf","name":"QSF","version":"0.1.0","type":"app","namespace":"qsf"},"enableOnInstall":true}'
curl -X PUT '/api/v1/meta/object/qsf_parent?mode=draft&package=app.qsf' -d '<parent + probe_sf = {object:qsf_child, function:sum, field:memo}>'   # step 2
curl '/api/v1/meta/object/qsf_parent?state=draft&package=app.qsf'                                                                   # _diagnostics
curl -X POST /api/v1/packages/app.qsf/publish-drafts -d '{}'                                                                       # step 3
curl -X PATCH /api/v1/data/qsf_child/qsfc1 -d '{"amount":31}'                                                                     # step 4
curl -X POST /api/v1/data/qsf_child -d '{"id":"qsfy1","name":"y1","amount":60,"parent":"qsfp1"}'
curl -X DELETE /api/v1/data/qsf_child/qsfy1
base (lint/metadata-protocol/rest/runtime at 2678600d) head
step 2 _diagnostics {"valid":true} valid:false, rollup/non-numeric-aggregand (sum and avg)
step 3 publish published refused, rollup/non-numeric-aggregand at …probe_sf.summaryOperations.field
step 4 PATCH / POST / DELETE 500 / 500 / 500 INTERNAL_ERROR (row persisted, child_sum 91 → 151 → 91) 200 / 201 / 200 (row persisted once, child_sum 168 → 228 → 168), one [REST] error line for qsf_parent.probe_sf
probe_nope (missing child) published published (admitted)

Step 4 on head was taken on a boot with only @objectstack/lint at base, because the head gate no longer lets the bad roll-up be stored.

Gates: node scripts/pm/dispatch-gates.mjs --commands derived 112 commands on the head fe13d22d; all 112 exit 0, and --ran reconciles 112 derived, 112 run, 0 NOT-MEASURED. Scoped pnpm lint over the 17 changed .ts/.mjs files: exit 0. pnpm check:nul-bytes: exit 0. Typecheck (tsc --noEmit) of lint, metadata-protocol, rest, runtime and types: exit 0 each.


Generated by Claude Code

claude added 15 commits November 1, 2026 13:28
… the publish door

Co-Authored-By: Claude <noreply@anthropic.com>
…compute

Co-Authored-By: Claude <noreply@anthropic.com>
…tale roll-up

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…elay, seam scan, adr-0087 disposition)

Co-Authored-By: Claude <noreply@anthropic.com>
…ine double; types in changeset

Co-Authored-By: Claude <noreply@anthropic.com>
… unmanaged install writes the parent first

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…x comment

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Nov 1, 2026
@github-actions

github-actions Bot commented Nov 1, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 5 package(s): @objectstack/lint, @objectstack/metadata-protocol, @objectstack/rest, @objectstack/runtime, @objectstack/types, touching 35 documentable anchor(s). ⚠️ 2 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/types/src/index.ts), 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/api/client-sdk.mdx (via meta.publishItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/publish, selected by route anchor /:type/:name/publish), publishItem (sdk, the bare tail of client method meta.publishItem, bound to POST /api/v1/meta/:type/:name/publish))
  • content/docs/api/metadata-api.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/concepts/metadata-lifecycle.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/data-modeling/currency.mdx (via /:object/import/jobs (route, bridged from symbol createData — its route source's handler names it))
  • content/docs/data-modeling/import-mappings.mdx (via /:object/import/jobs (route, bridged from symbol createData — its route source's handler names it))
  • content/docs/deployment/validating-metadata.mdx (via AUTHORING_RULES (symbol, a top-level const object), runtimeTypes (symbol, a field of const object AUTHORING_RULES))
  • content/docs/kernel/contracts/data-engine.mdx (via createData (symbol, a method of class ObjectStackProtocolImplementation), updateData (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/kernel/contracts/metadata-service.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/kernel/services-checklist.mdx (via createData (symbol, a method of class ObjectStackProtocolImplementation), deleteData (symbol, a method of class ObjectStackProtocolImplementation), updateData (symbol, a method of class ObjectStackProtocolImplementation))
  • content/docs/permissions/system-context.mdx (via /:object/import/jobs (route, bridged from symbol createData — its route source's handler names it), /import/jobs/:jobId/undo (route, bridged from symbol deleteData — its route source's handler names it))
  • content/docs/protocol/kernel/error-handling.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/protocol/objectql/state-machine.mdx (via /:object/import/jobs (route, bridged from symbol createData — its route source's handler names it), /import/jobs/:jobId/undo (route, bridged from symbol deleteData — its route source's handler names it))

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

  • content/docs/releases/v16.mdx (via ERR_SUMMARY_RECOMPUTE (literal, a string literal in a comment in SummaryRecomputeRefusal; a string literal in isSummaryRecomputeRefusal))
  • content/docs/releases/v17/17-0.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/releases/v17/17-1.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/releases/v17/17-2.mdx (via /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/releases/v17/17-3.mdx (via AggregationFunction (symbol, a top-level type))
  • content/docs/releases/v17/17-4.mdx (via createData (symbol, a method of class ObjectStackProtocolImplementation), /:type/:name/publish (route, bridged from symbol createData — its route source's handler names it; bridged from symbol deleteData — its route source's handler names it))
  • content/docs/releases/v17/17-7.mdx (via /api/v1/data/... (route, a path literal in a comment on a changed line))

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
  • 2 changed file(s) yielded no anchor (packages/lint/src/index.ts, packages/types/src/index.ts) — pages documenting those are invisible to this run
  • 1 cross-cutting symbol(s) contributed no route anchor: updateData (4 routes)
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 216 client-bound route-ledger rows — the other 156 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 156: 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; 101 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 — 51 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 8196881e7042f4e57543a6aaceffc1ab0ec89422 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 8196881e7042f4e57543a6aaceffc1ab0ec89422

⚠️ 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 8196881e7042f4e57543a6aaceffc1ab0ec89422 → 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

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: fe13d22d05d92d0e45e3190c159d58f59bac9f70
Local-runs: none
Scope: packages/lint/src/authoring-rules.ts, packages/lint/src/data-model-rules.ts, packages/lint/src/index.ts, packages/lint/src/rule-explanations.ts, packages/lint/src/data-model-rules.fix-lines.test.ts, packages/lint/src/data-model-rules.summary-rollup.test.ts, packages/lint/src/runtime-gate.object-writes.test.ts, packages/metadata-protocol/src/protocol.ts, packages/metadata-protocol/src/protocol.summary-rollup-publish-gate.test.ts, packages/rest/src/crud-routes.ts, packages/rest/src/stale-summary-answer.ts, packages/rest/src/data-write-summary-recompute-refused.test.ts, packages/runtime/src/domains/data.ts, packages/runtime/src/domains/data-write-summary-recompute-refused.test.ts, packages/types/src/index.ts, packages/types/src/summary-recompute-answer.ts, scripts/check-engine-double-contract.mjs, scripts/engine-double-contract.pinned.json + packages/spec/src/data/aggregate-field-type-compatibility.ts, packages/spec/src/api/error-code-ledger.zod.ts, packages/spec/scripts/check-error-code-provenance.ts, packages/objectql/src/aggregate-field-type-door.ts, packages/objectql/src/summary-aggregate.ts, packages/objectql/src/summary-errors.ts, packages/objectql/src/engine.ts (insert/update/delete recompute throw sites, the aggregate door call), packages/metadata-protocol/src/runtime-authoring-gate.ts, packages/metadata-protocol/src/seed-loader.ts, packages/core/src/utils/import-runner.ts, packages/lint/src/runtime-gate.ts, packages/runtime/src/action-execution.ts (callData), packages/rest/src/error-response.ts, AGENTS.md (changeset level rule)

Dispatch rulings judged against the tree, not adopted: (1) refuse sum/avg over a non-numeric child at save and publish; (2) refuse a missing child too unless a legitimate authoring order needs it admitted; (3) fix the persisted-write 500 only through an answer the platform already publishes, no new code, member or shape; (4) no packages/spec edit.

① Derived judgments

Accept-set changes the diff implies, each judged:

  • sum/avg whose child field type the aggregate table refuses is now refused under rollup/non-numeric-aggregand at os validate, os build, os lint (gating registry entry lintSummaryRollups, surfaces: CLI_AND_RUNTIME, runtimeTypes: ['object']), at the active object save (422 INVALID_METADATA), at publishMetaItem and publishPackageDrafts, and the draft's _diagnostics reads valid: false. RIGHT, and it is ruling 1. The predicate is isAggregateCompatibleWithFieldType(fn, childType) from @objectstack/spec/data, the same call aggregate-field-type-door.ts makes on engine.aggregate, which aggregateSummaryValue (summary-aggregate.ts) calls on every recompute (engine.ts calls assertAggregationFieldTypesAccepted inside aggregate). So the numeric-type set matches what the recompute actually aggregates: sum admits number, currency, rating, slider, progress, summary, boolean, toggle; avg adds percent; text, option, reference, temporal, formula, autonumber, JSON-stored types are refused for both. The engine door's one extra refusal, a multiple: true declaration, falls only on types already outside both rows, so no pair is refused at one door and admitted at the other. sum over percent refused and avg over percent admitted is the table's own row, and the engine refuses the same pair after the commit: RIGHT, though the card's word "non-numeric" undersells it (the rule's message names the accepted set, so the author reads the real criterion).
  • A child field whose declared type is not a FieldType member: the lint arm would refuse (the predicate answers false) where the engine door is silent (DECLARED_FIELD_TYPES guard). Unreachable past the object schema, which refuses the type first; noted, not a finding.
  • Silence when the child object or child field does not resolve, and never on count: RIGHT, the table's own consumer tier ("cannot answer, do not block"), and the tier the retired lintDataModel arm already held.
  • The min/max arm keeps summaryRollupAnswerFitsColumn (numeric or boolean child) and its message, and moves from os lint-only into the gating entry, so it now also gates os validate, os build and the runtime object door. RIGHT as a narrowing (the stored column cannot hold the answer), and declared BREAKING in the changeset; see ② for the one sentence that understates its reach.
  • A missing summaryOperations.object stays admitted at every door. RIGHT under ruling 2's own exception: the dev measured install-local --unmanaged saving crm_opportunity (its line_total roll-up) one active save before crm_opportunity_line_item, and the CLI pass may lack a dependency package's child. The runtime door's snapshot is the live universe plus the batch's pending drafts (runtime-authoring-gate.ts mergePendingDeclarations), so a child present in another package IS judged, and a draft child in the same publish batch is judged with its parent (tested both ways). The differential (lint/src/runtime-gate.ts, finding(lint): the runtime gate's object-write baseline drops the written object's stored self, so a master save is refused (422) for a stored detail's violation — the gate's "never charged to someone else's write" contract #22118) charges a stored parent's finding only to the write that creates it, so a child write that turns a summed field into text is refused and a label-only parent re-save of a legacy violation is not: RIGHT, and tested.
  • Half 2, the wire: POST/PATCH/DELETE /api/v1/data/:object[/:id] on RestServer (crud-routes.ts) and on the runtime dispatcher (domains/data.ts callDataWrite) answer 201/200 with the body the same write answers when every roll-up recomputes, plus the dropped-fields header the success path sets. RIGHT, and this is the answer ruling 3 asks for: written on SummaryRecomputeError is, at each of the three engine throw sites, exactly the value the engine returns on the success path (rowHookContexts[0].result or the batch outcomes for insert, hookContext.result for update and delete), and createData/updateData/deleteData run the same omitInternalFieldsFromWriteResponse, byIdUpdateRecord, null-readback probe, withheldMentions and notice code on it before building the same literal. No new error code, no new response member, no new status: RIGHT.
  • Is packages/types/src/summary-recompute-answer.ts or packages/rest/src/stale-summary-answer.ts a new public answer shape on the wire? NO. The carrier is a non-enumerable property under Symbol.for(...), which JSON.stringify omits and which earnedAnswerOf reads back only in-process; stale-summary-answer.ts writes no response, it hands the protocol's own result to the route's existing res.status(201).json(result) relay. They are internal carriers. What IS new is the package API of @objectstack/types (SummaryRecomputeRefusal, isSummaryRecomputeRefusal, staleSummaryRefusalOrThrow, withEarnedAnswer, earnedAnswerOf, staleSummaryAnswerLines) and of @objectstack/lint (lintSummaryRollups): additive exports, levelled in ②, not wire shapes.
  • Comparison with the live readers: seed-loader.ts (writeRecoveringSummary) and import-runner.ts (recoverSummaryStale) both match the refusal by an inline code === 'ERR_SUMMARY_RECOMPUTE' comparison, no imported class and no local constant, and read e.written as the write's result; the seed loader logs the stale column at error. The PR's reading of the refusal is the same reading, and both callers are untouched: the protocol rethrows the same error instance with the same code, failures and written. RIGHT in substance; the one place the PR departs from that precedent is spelling the code as a const in @objectstack/types, and the tree's own gate refuses that departure (④, must-fix).
  • Doors not changed (form submit, batch and *ManyData, cloneData, MCP create_record, callData's ObjectQL fallback, which builds no carrier so the refusal propagates as before): disclosed; outside the card's three doors. See ③.
  • scripts/check-engine-double-contract.mjs: receiptReturnPos now also reads a receipt returned through a same-function const answer = { ... }, mirroring localWhereIdBindings (same ts.forEachChild walk, same over-approximation into nested closures), with an A/B self-test pair (bound receipt with refusal reported as shared; without, reported with refusal === null). RIGHT: without it the by-id seam scan would lose updateData/deleteData once their literal moved into a const. engine-double-contract.pinned.json gains the new metadata-protocol test file's engine double (delete/findOne/update, 1 each): a ledger entry, not a change of contract.
  • rule-explanations.ts: os explain rollup/non-numeric-aggregand now covers both arms and names the reach. RIGHT.

② Semver level

Changeset .changeset/28661-summary-rollup-aggregand-gate.md: @objectstack/lint minor, @objectstack/metadata-protocol minor, @objectstack/types minor, @objectstack/rest patch, @objectstack/runtime patch; BREAKING (narrowing) banner, ADR-0087 marker not-required (no-migration-prescription), both declaration lines present.

Clause-②: no (narrowing), on the PR body and the changeset body; Metadata-protocol: narrowed. Matches what ships: a value an author could write (sum/avg over a text child) is now refused.

Against AGENTS.md's level rule ("a kind takes at least minor on a package the diff moves, none takes patch; a non-protocol change, widening or breaking, ships patch"): lint and metadata-protocol carry the narrowing, minor is RIGHT. rest and runtime are non-protocol bug fixes, patch is RIGHT. types ships additive exports consumed by rest's /data doors; its own CHANGELOG levelled the identical shape minor at 18.0.0 (notNullRefusalSite and siblings, "Clause-②: yes (widening: three additive exports ...)"), so minor follows the package's precedent; RIGHT. Check Changeset on the head: success.

One prose gap, should-fix: the "Now, at the doors" paragraph says the min/max arm "now also gates os validate and os build"; it also gates the runtime object save and publish door for the first time (the registry entry is CLI_AND_RUNTIME), which the BREAKING line should name since a Studio or REST author with a stored max over a datetime roll-up is refused at the next publish.

③ Boundary flags

  • open_questions 1 (missing child qsf_nope, options A/B/C, dev recommends A): ANSWERED A for this PR. Ruling 2 admits a missing child when a legitimate authoring order needs it, and the dev measured one (install-local --unmanaged, 9 reds). The runtime door still judges a child present anywhere in the live universe or the same publish batch, so what stays admitted is exactly the not-yet-present child, which reads 0/null and fails nothing. B (refuse only where the full universe is known) would need a second rule id and door-specific reach, and the CLI pass cannot tell "not yet installed" from "in a dependency package", so even there the table's own tier says be silent. Left to the maintainer only if a non-gating advisory (C) is wanted later.
  • open_questions 2 (other write doors still answer 500 after a landed write): ESCALATED as a follow-up card, not a finding against this PR. The card named POST/PATCH/DELETE on the data door and the order scoped half 2 to "the objectql / REST write path that surfaces SummaryRecomputeError"; the carrier is in place, so the follow-up is a wiring change per door. Nothing in this PR makes those doors worse.
  • Dev flag "step 4 on head was taken on a boot with only @objectstack/lint at base": a measurement method, not a product flag; the REST and runtime tests on the head cover the path the gate now prevents from being stored.
  • Docs-drift bot: two changed files yielded no anchor (packages/lint/src/index.ts, packages/types/src/index.ts); the four prose edits are 58 to 59 author-time rules in CLI transcripts. Prose faces, not reviewed.
  • Check-runs on the head (gate verdicts): Lint · docs & code guards FAILURE, red gate Error-code provenance guard (pnpm --filter @objectstack/spec check:error-code-provenance): "@objectstack/types stamps 'ERR_SUMMARY_RECOMPUTE' (constdef) at packages/types/src/summary-recompute-answer.ts:37, not listed under its own owner key"; 51 other gates in the lane passed. Lint & Repo Gates FAILURE as the aggregator of that lane. Answered in ④ as the must-fix. Everything else completed is success or skipped; Test Core, Build Core, Type Check (workspace, consumer gates, UI) and Test UI were still in progress at the time of this record and are not read as green here.

④ Design judgment

The narrowing, read against the published spec on main: AGGREGATE_FIELD_TYPE_COMPATIBILITY (packages/spec/src/data/aggregate-field-type-compatibility.ts) already declares sum and avg over the text family refused, the engine's aggregate door already refuses the pair on every recompute, and the roll-up never answered a value; the only thing the consumer lost is a 500 on every child write. The authoring door now makes the spec's own refusal real, at the door, in the spec's own words (accepted set named). No declared capability is taken away at the consumer. Half 2 answers an already-published answer (the write's own success body) and leaves the protocol-level refusal, the seed loader and the import runner as they were. The design holds on both halves; the tree does not.

Must-fix:

  1. The head's own gate is red. check:error-code-provenance reads const SUMMARY_RECOMPUTE_CODE = 'ERR_SUMMARY_RECOMPUTE' in packages/types/src/summary-recompute-answer.ts as a stamp site of a registered code attributed to @objectstack/types, which has no owner-key row for it. The guard's two prescribed fixes (a row under the @objectstack/types owner key, or a PROVENANCE_WAIVERS entry) both live in packages/spec/src/api/error-code-ledger.zod.ts, which ruling 4 closes. The route that stays inside the rulings is the one the live tree already uses for this code: seed-loader.ts and import-runner.ts match it by an inline code === 'ERR_SUMMARY_RECOMPUTE' comparison with no constant, which is not a stamp pattern, and @objectstack/types emits nothing (it only recognises the engine's refusal). Drop the const, compare inline in isSummaryRecomputeRefusal (keeping the module header's note that the class lives in objectql and would cycle), type SummaryRecomputeRefusal.code as the literal, and let the lane go green on the next push. Until the gate answers success the head is refused on the check-runs alone.

Should-fix:

  1. Changeset: name the runtime object save/publish door in the min/max sentence of the BREAKING paragraph (② above).
  2. staleSummaryAnswerLines keeps reportedStaleSummaries for the life of the process, keyed child and parent.field, never cleared. After an operator fixes and republishes the field, a later recompute failure of the same column for a different reason (a driver fault after retries) is not said again by the door; the engine's own warn per failure still fires, so the operator is not blind, but the "reported once per field" promise is once per process, which the line could say.
  3. Open question 2's follow-up card (form submit, batch and *ManyData, cloneData, MCP create_record), filed with the dedupe words the dev report gives.

Implemented-by: claude/project-thread-1minft
Reviewed-by: session_01FEkQ5oc56xChD6Wrc4cGSt

VERDICT: FAIL

…y is per process

The recogniser re-declared objectql's ERR_SUMMARY_RECOMPUTE as a const,
which check:error-code-provenance reads as a second stamp. Compare it
inline, as the import runner and seed loader do. The once-per-field log
line and the changeset now say the memory lasts the process, and the
changeset names the runtime save/publish door for the min/max arm.

Co-Authored-By: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: c3e6ceb5265e3f35667450e03ee9a6864bcc1924
Local-runs: probe — the provenance guard's three published STAMP_PATTERNS (objlit, assign, constdef) replayed over packages/types/src/summary-recompute-answer.ts at this head with comments masked, because the Lint · docs & code guards lane that carries the real check:error-code-provenance run was still in progress and the checkout here has no tsx to run the script itself
Scope: packages/types/src/summary-recompute-answer.ts, packages/runtime/src/domains/data.ts + .changeset/28661-summary-rollup-aggregand-gate.md (prose face, read for ②), packages/spec/scripts/check-error-code-provenance.ts (STAMP_PATTERNS, comment masking), packages/spec/src/api/error-code-ledger.zod.ts (the objectql owner row), packages/metadata-protocol/src/seed-loader.ts and packages/core/src/utils/import-runner.ts (the inline-comparison precedent)

Delta of one commit over the FAIL record on fe13d22; everything that record judged right stands unchanged, the three files above are the whole diff.

① Derived judgments

  • Must-fix 1 answered. const SUMMARY_RECOMPUTE_CODE is gone; isSummaryRecomputeRefusal compares code === 'ERR_SUMMARY_RECOMPUTE' inline, the reading seed-loader.ts and import-runner.ts already use, and the module header now says the code is read here and stamped by objectql, whose owner key holds the ledger row (@objectstack/objectql, line 777 onwards of the ledger). The probe over the comment-masked file finds no objlit, assign or constdef site; the three remaining mentions are the inline comparison (not a pattern: the assign regex needs = then a quote, and === breaks it) and two comments, which the guard masks. RIGHT, and no packages/spec edit (ruling 4).
  • The deviation, readonly code: string in place of the literal type: ACCEPTABLE. The guard's objlit pattern is \bcode:\s*'X' and its own comment says it "also matches the same spelling in a TYPE literal; see the over-match bound", so a literal-typed member would have been a second stamp attributed to @objectstack/types, with the only cures in the spec ledger. string plus the docblock stating the invariant is the honest spelling inside the rulings; the type guard still narrows callers to the refusal, and the two live readers of this code type it no tighter (e?.code). No accept set, wire member or code moves.
  • Should-fix 3 answered: the operator line says "Reported once per field per process", the set's declaration and staleSummaryAnswerLines' docblock say the memory is never cleared and a republish does not re-arm it, the runtime callDataWrite docblock says the same, and the stale twin name is corrected to earnedWriteDespiteStaleSummary, the function stale-summary-answer.ts exports. RIGHT.
  • No behaviour changes beyond the log text; the REST and runtime tests on the head that assert the line by prefix or count are not touched by the suffix (the dev's claim; the lane will say).

② Semver level

Changeset levels unchanged (lint, metadata-protocol, types minor; rest, runtime patch), both declaration lines unchanged. Should-fix 2 answered: the BREAKING line and the "Now, at the doors" bullet now name the runtime object save/publish door for the min/max arm, and the "Now, after a write" bullet says once per field per process. RIGHT.

Clause-②: no (narrowing), on the PR body and the changeset body.

③ Boundary flags

④ Design judgment

The one design question of the delta is whether reading the code inline, untyped, is the right seam. It is: @objectstack/types recognises the engine's refusal and emits nothing, the ledger's provenance half is "listed once per emitting package", and the two live readers of the same refusal sit on the same side of the line the same way. Nothing else in the delta touches a door, an accept set or an answer shape. No must-fix. No new should-fix.

Implemented-by: claude/project-thread-1minft
Reviewed-by: session_01FEkQ5oc56xChD6Wrc4cGSt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

席位核对

ACCEPT · domain:server seat 5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt · 2026-11-01T19:48Z · head c3e6ceb5 · #28661

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

席位意见:与 diff 相符。汇总字段对引擎不接受的子字段类型做 sum/avg,以前能保存发布、之后每次子记录写入都答 500;现在 os validate/os build/os lint、运行时保存和发布都会拒绝并指出位置。写入已落库、只是汇总重算失败时,REST 和运行时分发器答写入本身的成功响应,不再让客户端重试出重复记录。

  1. Shape: draft against main, Fixes #28661, changesets lint / metadata-protocol / types minor (BREAKING banner on the narrowing), rest / runtime patch, Clause-②: no (narrowing), Metadata-protocol: narrowed, Skills-docs: none with its reason. check-governed-merges --pr 28781: NOT governed (0 of 23 paths), +1406 / -122. git merge-tree against main: clean. No packages/spec/** edit.
  2. Against the order: ruling 1 → rollup/non-numeric-aggregand as a gating registry entry (CLI_AND_RUNTIME, runtimeTypes: ['object']) judged by isAggregateCompatibleWithFieldType, the predicate the engine's aggregate door applies on every recompute; ruling 2 → a missing child stays admitted, on measured evidence (below); ruling 3 → the persisted-write 500 answers through the protocol's existing success body, no new code, member or status; ruling 4 → spec untouched, including the provenance fix.
  3. Accepted deviations: SummaryRecomputeRefusal.code typed string, not the literal: the provenance guard reads a literal-typed member as a second stamp whose only cure is the closed spec ledger (judged ACCEPTABLE by the delta review). The min/max arm now also gates at os validate, os build and the runtime object door, named in the changeset's BREAKING line. scripts/check-engine-double-contract.mjs learns the const answer = {…} receipt with an A/B self-test pair, so the new seams are not lost.
  4. Gates: ## Contract review at CONTRACT_REVIEW_TIER: FAIL on fe13d22d (comment 6562903045, must-fix: provenance guard red), delta PASS on this head (comment 6562962856, no must-fix; should-fixes 2 and 3 answered). Lint · docs & code guards on this head: success. Remaining check-runs still running at ACCEPT; auto-merge waits on every required one.
  5. Out of scope: open question 1 answered A: the dev's dogfood measured install-local --unmanaged saving a parent before the child it rolls up (9 reds if refused), so a missing child stays admitted and the runtime door still judges a child present in the live universe or the same publish batch. Open question 2 answered A: the other write doors (form submit, batch / *ManyData, cloneData, the MCP create_record bridge, callData's fallback) are filed as data writes: form submit, batch / *ManyData, cloneData, the MCP create_record bridge and callData's fallback still answer 500 after a landed write when the summary recompute fails #28782, blocked on this PR. "Reported once per field" is once per process, stated in the log line.
  6. Landing: pr_ready + automerge_enable through the relay.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 6e86fff7454fe0bac0f614e5a9cdbb7bf564448c
Local-runs: none
Scope: 5768005 (merge of origin/main at d9c4cdc into the branch) over every PR path, 6e86fff (content/docs/deployment/cli.mdx, content/docs/deployment/validating-metadata.mdx, content/docs/getting-started/build-with-claude-code.mdx, content/docs/ui/react-pages.mdx) + the PR's own hunks at both bases (5493b88 and d9c4cdc) over packages/lint/src/authoring-rules.ts, packages/lint/src/index.ts, packages/lint/src/rule-explanations.ts, packages/metadata-protocol/src/protocol.ts (the four PR files main also moved), and the check-runs on c3e6ceb and 6e86fff

Second delta over the PASS record on c3e6ceb: two commits, a merge and a four-line docs commit. Nothing judged on fe13d22 or c3e6ceb moves.

① Derived judgments

  • The merge commit carries no resolution hunks. git diff-tree --cc 57680055 is empty, so no file holds content that is in neither parent. Against its first parent (the branch), the merge changes four PR files, all with main's own hunks (authoring-rules.ts +22, the validateCustomizationNamespace registry entry from feat(lint): warn when a package namespace is too long to have a paired customization namespace #28775; index.ts; rule-explanations.ts; protocol.ts) and nothing of the PR's; against its second parent (main at d9c4cdc), the merge changes exactly the PR's 19 non-prose files. The PR's own hunks (-U0, over every non-prose path) at the old base 5493b88 and at the new base d9c4cdc are identical as a set; the one textual difference is hunk order inside authoring-rules.ts, where main's new entry now sits beside the lintSummaryRollups entry. RIGHT.
  • Main moved none of the cited code between 5493b88 and origin/main: data-model-rules.ts, lint/runtime-gate.ts, packages/types/src, crud-routes.ts, error-response.ts, runtime domains/data.ts, runtime-authoring-gate.ts, seed-loader.ts, import-runner.ts, objectql's summary-aggregate.ts, summary-errors.ts and aggregate-field-type-door.ts, spec's aggregate table, error-code ledger and provenance guard, check-engine-double-contract.mjs are all untouched. Every ① judgment of the two earlier records stands on the same text.
  • 6e86fff changes four CLI transcripts from "author-time rules (59)" to "(60)": main's feat(lint): warn when a package namespace is too long to have a paired customization namespace #28775 added a registry rule, so the count the transcripts quote moved under the PR. Prose faces, read for the gate they answer; RIGHT.

② Semver level

Unchanged: lint, metadata-protocol, types minor; rest, runtime patch; BREAKING (narrowing) banner, ADR-0087 marker, both declaration lines as on c3e6ceb. Check Changeset on 6e86fff: success.

Clause-②: no (narrowing), on the PR body and the changeset body.

③ Boundary flags

  • The provenance lane that carried must-fix 1 concluded on c3e6ceb: Lint · docs & code guards success, Lint & Repo Gates success. packages/types/src/summary-recompute-answer.ts is byte-identical between c3e6ceb and 6e86fff, so that verdict carries; the delta PASS's pending condition is discharged.
  • c3e6ceb also concluded Type Check · workspace failure (and its aggregator TypeScript Type Check): the red step is "CLI transcripts in content/docs match the registry they quote" (check:docs-transcript-drift), run on the PR merge ref (merged into main at 56fdd56, which already held feat(lint): warn when a package namespace is too long to have a paired customization namespace #28775's rule), where the registry runs 60 rules and the transcripts said 59. That is the gate 6e86fff answers; main's own type-check runs at 5493b88 and d9c4cdc are success, so nothing of it is the PR's code.
  • Check-runs on 6e86fff at the last read: 38 runs, 11 success (Check Changeset, Governed Surface Queue Guard, PR-body guards, Check PR Size, Check Documentation Links among them), 9 skipped, 18 in progress, among them Lint · docs & code guards, Type Check · workspace, the other Lint and Type Check lanes and the builds; Test Core shards not yet planned. No run on this head has concluded failure. The verdict stands while Type Check · workspace concludes success on this head; a red there is the transcript gate again and reopens the docs commit, not the design.

④ Design judgment

The delta adds no code of the PR's own. The narrowing, the carrier and the three doors are the text judged on fe13d22 and c3e6ceb, now sitting on a base whose only change near them is one more registry entry beside theirs. No must-fix, no should-fix.

Implemented-by: claude/project-thread-1minft
Reviewed-by: session_01FEkQ5oc56xChD6Wrc4cGSt

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:server#5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt

席位核对 · re-land

ACCEPT · domain:server seat 5 · os-project-manager · session_01FEkQ5oc56xChD6Wrc4cGSt · 2026-11-01T20:25Z · head 6e86fff7 · #28661

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

席位意见:与上次 ACCEPT(6563000294,head c3e6ceb5)相同;本次只是合并 main(#28775 新增一条作者规则),并把四处文档里的规则数从 59 改成 60。

  1. Shape: unchanged. git merge-tree against current main: clean. The PR body's gate-fallout sentence corrected by the seat through the relay (59 to 60).
  2. Against the order: the delta is merge 57680055 (no resolution hunks) and 6e86fff7 (four content/docs transcripts, "author-time rules (60)"), answering the check:docs-transcript-drift red on c3e6ceb5's merge ref.
  3. Accepted deviations: as before.
  4. Gates: second delta ## Contract review at CONTRACT_REVIEW_TIER on this head: PASS, comment 6563411890, no must-fix, no should-fix. Check-runs on this head still running; auto-merge stays armed and waits on every required one.
  5. Out of scope: as before (data writes: form submit, batch / *ManyData, cloneData, the MCP create_record bridge and callData's fallback still answer 500 after a landed write when the summary recompute fails #28782).
  6. Landing: auto-merge already enabled on this PR; no new relay op.

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 1, 2026
Merged via the queue into main with commit 1617b7f Nov 1, 2026
69 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-1minft branch November 1, 2026 21:03
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/xl tests tooling

Projects

None yet

1 participant