Skip to content

fix(objectql)!: a multi-valued reference element is held to the single value's stored form (#22908) - #22947

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22908-multi-reference-element-shape
Oct 12, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/issue-22908-multi-reference-element-shape

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

Fixes #22908
Clause-②: yes (narrowing)

Two queue builds of this PR went red on the suite-duration ceiling gate (#22949), over packages this diff does not touch. The queue kept the entry, and a third build landed it. A park line written here in error is withdrawn.

What changed

packages/objectql/src/validation/record-validator.ts, the multi-value branch of validateOne. It returned null early for lookup / user / file / image ("reference integrity is handled elsewhere"), so an element of a multiple: true field was never parsed against its stored form. Those types now fall through to the ADR-0104 value-shape arm that a single value goes through. That arm parses the whole array with valueSchemaFor(def, 'stored'), which is already z.array(element) for a multi-valued field (ReferenceIdValueSchema for lookup / user, FileReferenceIdValueSchema for file / image). The result is the same refusal (invalid_type, message key invalid_value_shape), under the same posture, with the same warn-first sink. No second shape rule was added, as triage 6115825886 binds.

  • One predicate, isValueShapeJudgedType, now answers "does the ADR-0104 arm judge this type" in all three places that ask it: the arm, the multi-value branch that hands an element to it, and valueShapeViolation (the scan's predicate). Before this, the scan and the branch could disagree about which types those are.
  • valueShapeDetail names the index of the wrong element on a multi-valued field (at index 1: ...), so a refusal names the field and the index. The write path and os migrate value-shapes share this one reading. repeater / vector details are unchanged.
  • System principal: the shape check is the validator's, which already runs for an isSystem write, so the system door now refuses too. assertReferencesResolve is untouched (read-only): its isSystem latitude on whether an id EXISTS stays as isSystem 写入仍可产生悬空 lookup 引用——需要一条只报告不拦截的巡检(#4441 残留) #4551 left it, and a test pins that.
  • file / image with multiple: true: measured and given their own stored form, the opaque sys_file id, under the media posture (mediaValueShapeStrict / the file-references flag). A verified media flag does not open reference elements, and the value-shapes flag does not open media elements (pinned).

One changeset, .changeset/22908-multi-reference-element-shape.md: @objectstack/objectql minor, BREAKING (accept-set narrowing), ADR-0087 not-required (no-migration-prescription), FROM / TO stated.

Premise, checked on origin/main 018b077fbf before any edit

  • At the validator: with valueShapeStrict: true, { team: [{ id: 'x' }], watchers: [false] } on a multi-valued lookup / user was ADMITTED, while the single-valued control { owner: { id: 'x' } } was refused invalid_type.
  • At claim, read-only (triage's item). Does ADR-0104's value-shapes scan report a stored multi-valued element of the wrong shape? Yes. scanValueShapes over rows team: [{ id }], team: [false], watchers: [42] reported team count 2 and watchers count 1, plus a single-valued control row, with blocking 4. So no gap card is owed. But at BASE the scan counted values the strict write path admitted, which contradicts the scan's own header ("count exactly what strict mode would reject"). This change closes that disagreement, and a test pins the two in agreement. The media half has its own evidence: backfill-file-references.ts converts arrays element by element.

Pins (tests beside the code)

  • packages/objectql/src/validation/multi-reference-element-shape.test.ts (validator level): for lookup / user with multiple: true, the three bad elements (boolean, number, embedded record) are refused on insert and update, naming the field and index 0. The wrong element at index 2 is named as index 2. CONTROLS: the single-valued rows refuse the same three with no index; a well-formed id array and [] are accepted; a non-array is still the array refusal. A readonly multi-valued reference is judged by its shape, as a readonly single value is. Warn-first admits the element and reports it to the sink (value-shape gate for references, media for files). The gate split holds, and the OS_ALLOW_LAX_VALUE_SHAPES hatch still works. file / image elements are held to the sys_file id form. The scan predicate and the strict write path agree on every probe.
  • packages/objectql/src/engine-multi-reference-element-shape.test.ts (engine write path, strict posture): the three bad elements on lookup and user with multiple: true, through a caller's context (the engine call the REST data routes make) and the system principal, on insert and update. Each is refused as a ValidationError with code VALIDATION_FAILED and fields[0] { field, code: 'invalid_type' }, and nothing is stored or changed. CONTROLS: the single-valued rows are unchanged on both doors; well-formed id arrays are accepted on both doors; and a well-formed id that names no row is reference_not_found for the caller but still written by the system principal (the isSystem existence latitude is kept).

The real door: PR #22907's reference-shape family (one-off, not committed)

runReferenceShapeProofs from @objectstack/verify (a real bootStack, the REST data routes as the signed-in caller, and hooks.run with { system: true }) over a fixture with accounts (lookup, multiple), watchers (user, multiple), and single-valued account / assignee controls. It ran as a scratch test file that was deleted afterwards. It resolves @objectstack/objectql through its exports, so through the built dist/.

  • Fixed build: 4 fields, 4 proven, 0 holes; 48 probes, 48 refused invalid_type (REST answers 400), 0 admitted, 0 unattributed.
  • Ablated build (below): the two multi-valued fields read holes, 8 of 12 admitted each. That reproduces the card's measurement: REST embedded record admitted 201 / 200; REST boolean / number refused only as reference_not_found 400; system door all admitted. The single-valued controls stay proven. Summary: 2 proven, 2 holes, 32 refused, 16 admitted.

Ablation (one-off; scripts/ablation-replace.mjs in WRAP mode, plus an EXIT/INT/TERM trap)

  • Mutation: the early return restored as if (isValueShapeJudgedType(t) && String('ABLATION-22908-MARKER')) return null; ahead of the option arm. Anchor 1 to 0, blob 671387d31cbd to 7cc631fd7095. On disk: marker 1, the early-return line 1.
  • objectql pins (src-resolved): 43 failed, 10 passed (53). The 10 that pass are the controls.
  • pnpm --filter @objectstack/objectql build, then scripts/ablation-dist-preflight.mjs: the marker is present in 4 dist files. The real-door reading is above.
  • Restore: git checkout HEAD -- ABS_PATH by the tool. Blob after restore equals HEAD (671387d31cbd), git diff HEAD is empty, git status is clean. Rebuilt; --absent preflight: the marker is absent from all 14 dist files. objectql pins 53/53 pass; the real-door reading is 48/48 refused.
  • First attempt was a no-op: the tool refused before the child ran, because my replacement still contained the anchor (count 1 to 1). It restored to the HEAD blob and took no reading. The second attempt is the one reported.

Local verification

All read at the final head, 55cf89916e, after the last commit.

  • pnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2: 405 files passed, 7911 tests passed (through os-verify-lock, VERDICT command-exit 0).
  • pnpm --filter @objectstack/objectql typecheck (tsc --noEmit and check:test-typecheck): exit 0, test layer held at 40 files / 234 errors / 65 pinned signatures, with no new signature. Both new test files are in the tsconfig.test.json program (--listFiles); tsconfig.json excludes tests.
  • Gates: the 69 commands derived at dispatch for the declared surface all exit 0. Re-derived on the real diff (dispatch-gates --commands, change set from the merge base): 66 families, all inside the 69. dispatch-gates --ran with recorded exit codes: 66 derived, 66 run, 0 NOT-MEASURED, 0 UNRUN (a derived zero). The 3 extra are engine-surface families: check:error-status-conformance, check:stack-collection-maps, check:swallow-census-controls. One gate went red during development, check:objectql-double-limit, which was right: the new engine test's stub find ignored the caller's limit. It now applies the bound after the filter.
  • The dist-reading gates (check:dts-closure, check:dual-build-cjs-loads, check:lean-entry-closure, check:published-files, check:sourcemap-no-sources-content, check-dts-emitted --self-test) ran after pnpm --filter @objectstack/objectql build.
  • Lint, narrowed and proven: eslint's own config (calculateConfigForFile) applies to all 3 changed .ts files, and none is ignored. eslint --no-inline-config --format json over them gave 3 files, 0 errors, 0 warnings. The config enables no type-aware linting (no parserOptions.project, no typed rules), so this diff cannot move the verdict on any untouched file. The repo-wide pnpm lint is CI's.
  • Consumers: no export, spec key or response shape changes, so no import-side suites were run. A grep of downstream writers of multi-valued arrays (plugin-approvals attachments, service-storage gallery, rest and runtime fixtures, the showcase seed) found id elements only. CI runs the full set.

Acceptance notes (observations; not filed)

  • The spec's authoring-time default-value gate (actionableValueIssueMessage, packages/spec/src/data/default-value-shape.ts) is a deliberately separate reading. It does not name the index for a multi-valued literal default. That is polish, not a defect class; carrier: none.
  • assertReferencesResolve's note "already-expanded objects are skipped" now describes a path reachable only under warn-first. On a strict deployment the validator refuses an expanded element first, for both cardinalities, as it already did for a single value.
  • Not measured: what the console (objectui) writes into a multi-valued lookup / user / file / image on save. No objectui checkout was available here. If a console editor wrote expanded records back as elements, a strict deployment would now refuse that save. The single-valued field has refused that shape under the same posture all along.
  • ⛔ Not folded here, per triage: making os verify --reference-shapes the default. The one-off reading above is the input for that separate call. examples/app-showcase itself was not run through the CLI.

Generated by Claude Code

…e value's stored form

The record validator's multi-value branch returned early for lookup / user /
file / image, so an element was never parsed against valueSchemaFor's
z.array(element). An embedded record, a boolean or a number was admitted as
an element on every door, the system write included, where the single-valued
field refuses the same three invalid_type (ADR-0104 D1).

Those types now fall through to the ADR-0104 arm the single value goes
through: same derived schema, same posture, same sink, same refusal. The
detail names the offending element's index on a multi-valued field, for the
write path and the value-shapes scan alike (one helper).

Claude-Session: https://claude.ai/code/session_01ADzJtzYTLUfgrRZHxkagkX
Co-authored-by: Claude <noreply@anthropic.com>
…rd narrows the option arm

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

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

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

  • content/docs/api/error-catalog.mdx (via invalid_option (literal, a string literal in validateOne))
  • content/docs/deployment/cli.mdx (via unrecognized_keys (literal, a string literal in valueShapeDetail))
  • content/docs/protocol/kernel/error-handling.mdx (via invalid_option (literal, a string literal in validateOne))
  • content/docs/protocol/objectql/state-machine.mdx (via invalid_option (literal, a string literal in validateOne))
  • content/docs/protocol/objectql/types.mdx (via unrecognized_keys (literal, a string literal in valueShapeDetail))

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

  • content/docs/releases/v17/17-0.mdx (via unrecognized_keys (literal, a string literal in valueShapeDetail))
  • content/docs/releases/v17/17-5.mdx (via invalid_option (literal, a string literal in validateOne))
  • content/docs/releases/v17/17-6.mdx (via invalid_option (literal, a string literal in validateOne))

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
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 17 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 51b43a8dc45fef9d86229509ab6b2679ffa9d501 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 51b43a8dc45fef9d86229509ab6b2679ffa9d501

⚠️ 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 51b43a8dc45fef9d86229509ab6b2679ffa9d501 → 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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 55cf89916e46ef675a9ce842d9740bb38fa076cb
Local-runs: none

Inputs: card #22908 (body, triage grade 6115825886, claim 6116778532, os-dev-report 6117876326), PR #22947 (body, its one bot comment, 4-file list, 4 commits), the net diff of the branch against origin/main from merge base 018b077fbf (490 additions / 31 deletions; record-validator.ts is the only source change, plus two tests and one changeset), the check-runs on the head, and on origin/main the files the brief names, read to judge what changes. PR #22907's reference-shape.test.ts read as the instrument's cited context. Nothing built, run or re-run.

① Derived judgments

  1. What changes. validateOne's multi-value branch no longer returns null for lookup / user / file / image. The branch is gated on !isValueShapeJudgedType(t) and judges option types only; the four value-shape types fall through to the ADR-0104 D1 arm, which parses the whole value with shapeSchemaFor(def), that is valueSchemaFor(def, 'stored'), which is already z.array(element) for a field isMultiValueField accepts (MULTI_CAPABLE_TYPES is select, radio, lookup, user, file, image). The element rule is therefore the single value's own: ReferenceIdValueSchema for lookup / user (a non-empty string that is not embedded-record JSON text), FileReferenceIdValueSchema for file / image (an opaque sys_file id, bounded charset). One shape rule kept, no second rule written anywhere in the diff. Right.

  2. Which writes are refused, under which posture. Posture is unchanged and shared with the single value. References: valueShapeStrictEffective(valueStrict), where OS_ALLOW_LAX_VALUE_SHAPES=1 (opt-out) beats OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1 (all-class opt-in) beats the deployment's verified adr-0104-value-shapes row (a born-attested fresh store, or os migrate value-shapes --apply), consulted only when the object declares a scannable covered field (objectHasCoveredValueField via isScannableValueShapeField, the dormancy rule). Media: mediaStrictEffective(mediaStrict) with OS_ALLOW_LAX_MEDIA_VALUES and the adr-0104-file-references row, dormant unless the object declares a media field. Under strict, an embedded record, a boolean or a number as an element of a lookup / user with multiple: true is refused invalid_type. Right. The same rule also refuses any other non-id element (null, an empty or whitespace string): that goes past the card's three named shapes but is the single value's z.string().min(1), the scan already counted exactly these at base, and the changeset's TO states the rule rather than only the three examples. Right, and named here because a single-valued blank is isMissing and never reaches the arm while a blank element inside an array does; multiValueStorageForm does not wrap blanks and normalizeBlankTypedValues leaves string-stored columns alone, so nothing upstream removes one. Inherent in the shared schema, not a defect. Under warn-first the element is admitted, warnOnce-logged and reported to onAdmittedValueShapeViolation with gate value-shape or media, the field, the type and the index-bearing detail. Right, pinned.

  3. Both doors. The check is validateRecord / validateRecordInScope's, which the engine runs on insert (single and batch) and on both update passes for every context, so the caller's door (the engine call the REST data routes make) and the system write (isSystem) refuse alike under strict. Pinned at engine level for caller and system contexts, insert and update, with the row count and the held value unchanged after the refusal. assertReferencesResolve is not in the diff: its early return for context.isSystem and its skip of an object-typed element stand, so isSystem 写入仍可产生悬空 lookup 引用——需要一条只报告不拦截的巡检(#4441 残留) #4551's existence latitude is untouched. Right, pinned by the engine test's last case (a well-formed id naming no row is reference_not_found for the caller and still stored by the system principal). Under warn-first a caller's boolean or number element still meets the existence check and answers reference_not_found as before.

  4. file / image with multiple: true. Each element is held to FileReferenceIdValueSchema under the media posture; a verified media flag does not open reference elements and the value-shape flag does not open media elements. Pinned with the two flags crossed. avatar / video / audio are not multi-capable, so unreached. Right.

  5. Scan predicate and write path. valueShapeViolation now uses isValueShapeJudgedType and the def-aware valueShapeDetail; scanValueShapes imports it unchanged; the validator test pins scan-equals-strict-write on six probes per field. At base the scan already walked the z.array form while the write path admitted the element; that disagreement, which contradicted the scan's own header, is closed. isScannableValueShapeField (which fields are walked: references and structured JSON, never system or readonly, never media) is unchanged. Right.

  6. Readonly multi-valued references. Before, if (shapeOnly) return null; left the branch ahead of any element check; now the four types bypass that gate and reach the arm, which the single readonly value already reached under the A system-context write skips value-shape validation for readonly fields: a seed's malformed readonly datetime ('yesterday', an unresolved cel envelope) is stored verbatim, while the same value on a non-readonly field is refused #21663 ruling. So a readonly multi-valued lookup / user / file / image written by a system writer, hook or stamp is shape-judged under the object's posture (pinned, stamped_team). Right, the ruling applied to the other cardinality; named because a platform writer storing an off-shape element in such a column on a born-strict store is now refused at that write. The Test Core and Dogfood shards on this head are the canary (see the pending runs in ③).

  7. Error envelope. ValidationError, code VALIDATION_FAILED (the pre-existing REST mapping answers 400, which the dev's real-door reading reports), one fields[] entry { field, code: 'invalid_type', constraint: { type, detail } }, message key invalid_value_shape, whose template renders the detail. The index is carried as text: at index N: prefixed to the chosen zod issue's message from its path[0], only when isMultiValueField(def); the single value's detail is unchanged (pinned: no index named on the controls), and repeater / vector are not isMultiValueField, so unprefixed. The wrong element at position 2 is named as index 2 (pinned). Triage asked for the field and the index to be named; they are. No structured index key is added, which is consistent with how the scan's ValueShapeFinding.detail already carries the prescription. Right.

  8. Exports and public surface. packages/objectql/src/index.ts is untouched; isValueShapeJudgedType is module-private; the private valueShapeDetail gains a def parameter; the exported valueShapeViolation signature is unchanged; no spec change, no new key, no new env var, no wire-shape change. "No export change" is right.

  9. Option types. For multiselect select / radio and the multi-option types the branch behaves as before (shapeOnly gate, picklistUnresolved, invalid_option with kept options). Right.

  10. Claim surface honoured. The diff is exactly record-validator.ts, two tests beside the code (src/validation/ for the validator, src/ beside engine.ts for the engine door), one changeset; engine.ts read-only. Merge base matches the claim's read sha; the dev's line budget (490 / 31 over 4 files) matches the PR.

② Semver level

  • Changeset .changeset/22908-multi-reference-element-shape.md: @objectstack/objectql at minor, the only package the diff publishes; a BREAKING banner naming it an accept-set narrowing, shipped minor under the launch-window convention check-changeset-no-major records; exactly one ADR-0087 marker, not-required (no-migration-prescription), with its argument (no spec key, authorable spelling, export or stored shape moves; stored rows are os migrate value-shapes / files-to-references business); FROM / TO stated per door and per posture; the one-line fix stated (write ids as elements, never the record a read with expand returns). Level and marker match what the diff publishes. Right. Check Changeset is green on the head; check-adr-0087-registration and check-changeset-no-major ride Lint & Repo Gates, still in progress at the final read (the dev reports both at exit 0 on this head).
  • Agreement of the Clause-②: line. PR body line 2, the changeset body, the claim and triage's grade all read yes (narrowing). They agree.
  • Is yes (narrowing) legal here? It is a legal spelling: scripts/pm/clause2-line.mjs, the fleet's one reader, accepts four combinations and refuses only no (widening); AGENTS.md says the same. It is not the accurate value. The value answers one question, whether the card widens a published accept set or the public surface, and this diff widens nothing (①.8): it only narrows. The reader's own gloss reserves yes (narrowing) for a diff that widens one surface and narrows another; the dispatch's rule text says a card that only narrows is Clause-②: no; the lane's two precedents (objectql: a detail write reads its master-detail header elevated (tenant kept), so a parent-scoped requiredWhen may disclose one bit of a header the caller cannot read — measure whether any real configuration reaches it #22519, objectql: an after* hook with onError: 'log' whose condition cannot be evaluated refuses the write (HookConditionError) while the row stays stored #22856) carry no (narrowing) for the same class. The dev flagged it and did not re-rule, which is right: the value is triage's and the claim's. Consequence for the gates: none. The yes floor (at least minor) is met by the minor a BREAKING narrowing takes anyway, and the ADR-0087 disposition is demanded by the narrowing arm and the banner, which both gates read identically under either value; the mis-declaration errs toward more review, not less. Correction owed by the seat, not by a respin: no (narrowing) on the claim and on the PR body line 2 (a body edit); the changeset's own line follows the next time the head moves. Not a reason to withhold PASS.

③ Boundary flags

Every deviations entry and every out_of_scope_findings entry of os-dev-report 6117876326 is answered below; open_questions is empty.

  • D1, the Clause-② arm. Answered in ②: legal spelling, inaccurate value, no gate consequence, seat correction owed.
  • D2, the real-door pin one-off rather than kept. Verified on origin/main: the @objectstack/verify real-stack fixture in reference-shape.test.ts (rs_contact) declares single-valued reference fields only; the multiple: true rs_board.accounts sits in the derivation test alone. So no in-tree test drives PR feat(verify): the reference-value shape family, runnable as os verify --reference-shapes (#15953 slice 1) #22907's family over a multi-valued field on a real stack, and the dev's one-off (48 of 48 refused on the fixed build; 16 admitted under ablation, reproducing the card's measurement) is evidence read from the report, not a pin. Accepted here: the claim's file surface is tests beside the code, and the kept engine pin drives both contexts through the same engine door the REST data routes call. Escalated to the seat: the card for the separate os verify --reference-shapes default call (triage's not-folded item, feat(verify): the reference-value shape family, runnable as os verify --reference-shapes (#15953 slice 1) #22907's recommendation) carries, in its scope, a multiple: true lookup and user in the verify real-stack fixture so the family's multi-valued rows become permanent. Not blocking.
  • D3, ablation attempt 1 a no-op. The tool refused before running and restored the HEAD blob; the second attempt's reading (43 pins failed, the 10 controls passed; restore verified blob-equal, tree clean, 53 of 53 after) is the one that counts. Procedural, answered.
  • D4, attribution. The four commits carry the AGENTS.md pair, Co-authored-by: Claude and a Claude-Session trailer naming the dispatching session; AGENTS.md's attribution rule is the one that binds over a harness reminder. Right.
  • D5, gates re-run at the final head. Every figure reported is from the head under review; CI on the same head agrees so far (28 success, 0 failure). Answered.
  • O1, the scan already reports stored multi-valued elements. Verified from source: valueShapeViolation went through shapeSchemaFor (the z.array form) at base and isScannableValueShapeField does not exclude multiple, so the triage's conditional gap card is not owed. Right.
  • O2, the authoring-time default-value gate names no index. A separate reading in packages/spec by design; polish, outside the filing classes; carrier none. Agreed.
  • O3, the objectui console write path, NOT MEASURED. Answered as far as this repository answers it, then escalated.
    • The showcase declares project.team_members (Field.user, multiple: true) and project.view.ts renders it as a list column and a form field; no in-repo seed, dogfood script or verify fixture writes it, so a console save is its only producer and no check on this head exercises it.
    • A warn-first deployment (an existing store with no verified adr-0104-value-shapes row and no env switch, which is what an upgraded store is) refuses nothing a console save sends that it did not refuse before: a wrong element is admitted, logged and tallied; a boolean or number element still meets the caller's existence check and answers reference_not_found as before.
    • A strict deployment refuses a console save whose multi-valued lookup / user element is not a non-empty id string (an expanded record object, a boolean, a number, null, an empty string) and whose multi-valued file / image element is not a sys_file id (a URL string, an expanded file object). Strict is wider than the brief's framing of the default: per ADR-0104 every fresh store is born-attested, so every showcase or CRM fresh boot and every dogfood run is strict from birth, as is a store after the scan's --apply or under OS_DATA_VALUE_SHAPE_STRICT_ENABLED=1. Under that same posture the single-valued field has refused an expanded record since D1 landed, and born-strict dogfood stores save single-valued lookups through the console today, so a console that posts expanded objects only for its multi-valued control would be an inconsistency nothing evidences.
    • Escalation: a read-only dogfood item for the seat: on a fresh showcase, save a project with team members through the console record form and read the stored column back. If the console posts expanded objects, the fix is at the producer (objectui), never a lenient alias in this validator (Prime Directive Add comprehensive test suite for Zod schema validation #12). Not blocking this landing: the warn-first default refuses nothing new, and the strict refusal is the one the single value already carries.
  • O4, assertReferencesResolve's "already-expanded objects are skipped" note now describes a warn-first-only path. Comment accuracy; engine.ts was read-only under the claim; a one-line edit for the next PR in this lane that opens engine.ts. Agreed, carrier none.
  • The docs-drift advisory on the PR (five hand-written pages, via the invalid_option and unrecognized_keys literals): both literals are moved inside the branch, not changed; the only new author-facing text is the at index N: prefix inside the existing detail slot. No documented string changes; no docs edit owed.
  • Check-runs on the head at the final read: 33 runs, 28 success, 3 skipped by path (Build Docs, Console Pin Gate, Packed-tarball smoke), 0 failure, and 2 still in progress: Lint & Repo Gates and Test Core (6/6). The PR touches no governed surface (Governed Surface Queue Guard green); this record is the contract-tier review the narrowing owes before the queue. The verdict below stands on the diff and the card; the queue still waits for those two to complete green, and a red on either returns the PR to the dev, not to this record.

Implemented-by: claude/issue-22908-multi-reference-element-shape
Reviewed-by: session_01ADzJtzYTLUfgrRZHxkagkX

VERDICT: PASS

Adopted by domain:engine#2 (session_01ADzJtzYTLUfgrRZHxkagkX) at 2026-10-12T05:00Z from an isolated at-tier reviewer whose transcript shows read-only tool use (its one write is its own scratch record). The two runs pending at the record's final read have since concluded: on this head Lint & Repo Gates and all six Test Core shards are success; 31 success, 3 skipped, 0 failed. On ②: the seat keeps Clause-②: yes (narrowing) as triage and the claim declared it. The rule reads "a declaration of yes for a narrowing is never an error", and correcting only the PR body would put it out of step with the changeset's own line.

@github-actions

Copy link
Copy Markdown
Contributor

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

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

分类:failure —— 按下面的日志分诊。

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

  • Test Core — 失败步骤: Check the per-package suite-duration ceilings(日志不可读,点进 job 看)

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

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

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

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

历史信号:

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

分诊清单:

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

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Queue-red triage: a superseded group. The PR is still in the queue, so nothing is re-armed

domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T05:30Z. Answers the merge-queue triage comment 6118315596.

@github-actions

Copy link
Copy Markdown
Contributor

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

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

分类:failure —— 按下面的日志分诊。

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

  • Test Core — 失败步骤: Check the per-package suite-duration ceilings(日志不可读,点进 job 看)

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

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

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

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

历史信号:

  • ⚠️ 本 PR 过去 24h 已在队列失败 1 次(不含本次)。 内容未变而反复失败 ⇒ 高度怀疑 flaky 测试或与同组 PR 的语义冲突,重排不解决。
  • 过去 24h 队列共有 11 个失败构建(不含本次)。

分诊清单:

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

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

@objectstack-fleet

objectstack-fleet Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor Author

⚠️ Correction · domain:engine seat 2 · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T06:04Z. The statement below that the queue dropped PR #22947 is false. The queue never removed it: its timeline shows no removed_from_merge_queue before the merge. After group pr-22947-12df242e60 (run 38206501083) went red, the queue rebuilt the entry as group pr-22947-e5687cb6b3 (run 38208628600, created 2026-10-12T05:35:54Z). That run went green, and the PR merged as 21914c3fb3 at 06:02Z. This seat missed that group because it read too short a page of queue runs (10 runs, so the 05:35:54 group fell outside it). The park below never took effect, and none is in force now. The suite-ceiling readings below are accurate.

Queue-red, second build · domain:engine seat 2 (#20966) · os-steve · session_01ADzJtzYTLUfgrRZHxkagkX · 2026-10-12T05:39Z. Answers triage comment 6118364721.

Run 38206501083 (group pr-22947-12df242e60) went red at the same step, over a different package this diff does not touch: @objectstack/client: 202.15s, over its 179.74s ceiling. Slowest file: src/auth-login-register-envelope.test.ts 18.34s. @objectstack/cloud-connection read 159.22s (ok), and @objectstack/objectql read 696.95s against 952.27s (ok). The queue dropped the PR.

Disposition: parked, as the previous audit said it would be. The PR is not re-armed while #22949 is open. The card is pm:blocked with Blocked-by: #22949, and this PR's body names #22949. The signature is posted on #22949 as a sighting.

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/l tests tooling

Projects

None yet

2 participants