Skip to content

feat(spec): a blueprint field carries a typed defaultValue, so an AI-built boolean flag defaults to false - #30655

Merged
objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-7e1clu
Nov 5, 2026
Merged

objectstack-fleet[bot] merged 5 commits into
mainfrom
claude/project-thread-7e1clu

Conversation

@objectstack-fleet

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

Copy link
Copy Markdown
Contributor

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Requested by Sales · project thread

Implemented by the card thread of claim 6668151062 (session_01L6bhgUCEUicLCQ4tkXayK2); round 1 adopted from contract review 6670092320.

Fixes #30584

Clause-②: yes (widening)
Metadata-protocol: added
Skills-docs: none — no file under skills/** restates a blueprint field key (skills/objectstack-ai/references/_index.md only points at solution-blueprint.zod.ts); the reference page content/docs/references/ai/solution-blueprint.mdx is regenerated from the new describe.

维护者速读

  • 改了什么:AI 搭建方案(蓝图)的字段可以写 defaultValue 了。宽松 schema 与 strict 结构化输出镜像都加了这个键,并按字段类型校验:布尔取 true/false,数字取数字,单选取本字段自己的选项值;21 种类型可写,其余 28 种写了就按类型名拒收,不再悄悄丢掉;current_user、NOW() 这类运行时令牌先按令牌拒收,理由说的是令牌,不借用类型的理由(第 1 轮)。
  • 为什么改:AI 照搭建指引把「是否完成 / 已付款」写成 defaultValue: false,以前解析成功但这个键被剥掉,建出的字段没有默认值,新记录是空而不是「否」,按「否」筛选会漏掉;按 cloud#3619 裁决 B 补上 spec 这一半。
  • 风险与回滚:只加不收,之前能解析的蓝图照旧能解析;strict 镜像的生产者每个字段要多发一个 defaultValue(无默认填 null)。回滚即还原 solution-blueprint.zod.ts、它的测试和台账三处站点,无数据迁移。
  • 先看:packages/spec/src/ai/solution-blueprint.zod.ts(准入表 BLUEPRINT_DEFAULT_VALUE_RULES 与 refuseUnfitDefaultValue)、packages/spec/src/ai/solution-blueprint.test.ts(末尾 blueprint field defaultValue 一节)、packages/spec/dropped-refinements.baseline.json(新增三处站点)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ai/solution-blueprint.test.ts,预期 97 个用例全绿。
  • 需要你做的:无,席位落地。

What changes

  • defaultValue on BlueprintFieldSchema and on the strict mirror's fields (SolutionBlueprintStrictSchema.objects[].fields[], the StrictField shape). Both sides declare one scalar union, boolean | number | string. Lenient: .optional(). Strict: .nullable() and present on every field, as with every other key in that mirror; the blueprint tools strip the null. The key is appended last on both sides, so the model generates it after type and options.
  • The type check lives on the lenient field, as a superRefine (refuseUnfitDefaultValue) run with when: () => true. It reads one table, BLUEPRINT_DEFAULT_VALUE_RULES, typed as a record keyed by every FieldType member: a type added to FieldType does not compile until it is placed in the table, admitted or refused with a reason. An admitted value is judged by checkLiteralDefaultValue from data/default-value-shape.ts, the same literal contract FieldSchema runs on a built field's defaultValue (ADR-0104 D1, valueSchemaFor(def, 'stored')). A select / radio default must be one of THIS field's options[].value; with no options declared it is refused. A runtime token (NOW(), current_user) is refused on every type, and it is judged FIRST, before the type's own rule: the refusal gives the token's reason (a blueprint default is a literal), never a refused type's reason, which is false of a token.
  • Every refusal is one custom issue at the field's defaultValue. It names the field and its type (Field "is_done" (boolean): …), says why, and prescribes the fix. A non-admitted type's refusal lists the types that take a default.
  • Both describes say the value becomes the built field's defaultValue, give the admitted list (built from the table, so describe and refusal read the same list), and state the boolean-flag rule: a 是否完成 / 已付款 / is_active flag is a boolean field that is never required and carries defaultValue: false. Each side uses its own spelling of an unset required ("omit required or set it false" / "required null or false").
  • Generated artifacts: content/docs/references/ai/solution-blueprint.mdx (gen:docs, two rows), packages/spec/authorable-surface/ai.json (one key). packages/spec/dropped-refinements.baseline.json gains three hand-edited sites (ai/BlueprintField root, ai/BlueprintObject fields.element, ai/SolutionBlueprint objects.element.fields.element) and its droppedRefinementSites total moves 1020 → 1023: the option-membership rule reads a sibling key, so it has no JSON Schema form.
  • Changeset 30584-blueprint-field-default-value.md: @objectstack/spec minor.

Tests

A new describe block, "blueprint field defaultValue", at the end of packages/spec/src/ai/solution-blueprint.test.ts, 12 cases:

  • a boolean with defaultValue: false parses and the key survives the parse (own-property check);
  • the admitted set is exactly the declared one: every FieldType member is tried, the 21 admitted take their value, each of the 28 others is refused with one by-name issue;
  • a wrong-typed value on each admitted type is refused, one issue, by name (a string on a boolean, a number on text, "9am" on time, a label "Open" on a select);
  • a select default that is not one of its OWN option values is refused, including a value another field of the same object declares, and a radio with no options;
  • a runtime token on an admitted type is refused;
  • a runtime token on a type that takes no default (user / lookup + current_user, date + NOW(), datetime + now()) is refused with one issue that gives the token's reason, says the type takes no defaultValue in a blueprint and ends Remove defaultValue from this field., and never the type's own reason (round 1);
  • an array default on a multiselect is refused by name (beside the key's own union issue);
  • an absent value parses (lenient); the strict mirror takes null and refuses the key missing, at the key's path;
  • the strict mirror accepts what the lenient schema accepts for each admitted type, and null;
  • VALUE parity for a key whose rule spans type: 49 types × 9 values the strict union accepts; the lenient schema either keeps the value byte-equal or refuses it with exactly one issue that names the type at this key, never strips it;
  • every value the blueprint admits is one FieldSchema admits on the built field;
  • both describes name every admitted type and state the boolean-flag rule.

Seven existing strict fixtures gained defaultValue: null (strict mode needs every key), including the leading-digit pin, whose "the only defect left is the leading digit" claim needs it.

Acceptance notes

  • The strict mirror's shape: z.union([z.boolean(), z.number(), z.string()]).nullable(). Strict structured output states a rule per key and cannot read type while it judges this one, so the mirror cannot say which arm a type takes. The per-type check lives on the lenient field, which apply_blueprint parses, following the precedent this file already sets for the view filter rule's operator ↔ value refinement. The mirror can therefore generate a value the lenient schema refuses (a string on a boolean), and the cross-product pin holds every such refusal to one by-name issue at the key. The existing identifier VALUE-parity walker does not walk union leaves and is unchanged and green.
  • The lenient key is the same scalar union, not z.unknown() like FieldSchema.defaultValue. The published JSON Schema then types the key as the scalars every admitted type draws from, rather than as anything. The price: the union's invalid_union aborts before an object refinement runs by default, so the refinement runs with when: () => true (the view.zod.ts / analytics-carrier-filter.ts precedent), and an array or object default gets the union's issue AND the by-name one. A lenient null is refused by the union, as on every other lenient key.
  • The admitted types, grounded in data/field-value.zod.ts's value classes, the ones valueSchemaFor answers for a built field's literal default: BOOLEAN_VALUE_TYPES (boolean, toggle); NUMERIC_VALUE_TYPES minus summary (number, currency, percent, rating, slider, progress); STRING_VALUE_TYPES minus password, secret, signature, qrcode (text, textarea, email, url, phone, markdown, html, richtext, code, color); CLOCK_TIME_TYPES (time, HH:MM[:SS], a wall-clock literal that does not age); SINGLE_OPTION_TYPES (select, radio), narrowed to the field's own option values (with no options, valueSchemaFor would take any string).
  • Refused by name, even where FieldSchema takes a default: date / datetime (a literal day is frozen at design time; "today" is the NOW() token, an instruction rather than a value); lookup / master_detail / tree / user and the five file types (the value is the id of a record or file that does not exist while the blueprint is designed; seed data is reported, never applied); multiselect / checkboxes / tags (an array, and this key carries one scalar); formula / summary / autonumber (computed, never read); password / secret (one credential on every record); signature / qrcode (made per record); the seven structured JSON types. The brief's example names a checkbox type: FieldType has none. checkboxes is the multi-option type and is refused with the other list types. Widening any of these later is additive; narrowing would be breaking, so the first cut is the narrow one.
  • The boolean-flag sentence lives in defaultValue's describe on both sides. required's describe is unchanged: no existing key's shape, default or describe moves.
  • Liveness: no row, measured. check:liveness at base 14c67d44 prints 43 governed types (33 authorable plus 10 via SPEC_ONLY_SCHEMAS); no blueprint shape is among them, because a blueprint is never persisted and is not a metadata type. The card's Acceptance asks for a planned row; that is a deviation with this reading. Carrier for "planned until the cloud half lands": cloud#3619 / cloud#3633 (the build applier that carries the value into the built field and admits the key in its undeclared-key refusal). Once carried, the value is the built field's defaultValue, governed by liveness/field.json props.defaultValue (live, packages/objectql/src/engine.ts).
  • Consumers in this repo: packages/lint/src/validate-expressions.formula-one-hop.test.ts reads BlueprintFieldSchema.shape and the strict field shape; the refined schema is still a ZodObject, and the file is 13/13 after a lint closure build. ui/packages/plugin-chatbot/src/mapMessages.ts names BlueprintFieldSchema only in a comment: it lifts each field's name / label from the plan envelope and imports no blueprint schema, so it neither reads nor drops the new key. packages/spec/src/type-alias-convention.pin.test.ts (input = output for both schemas) is green in the spec suite. The producers and readers that move on the new key (cloud's blueprint tools and build applier) are in objectstack-ai/cloud, outside this repo.
  • The ledger's measured header also carries refinementSitesThatDidProject: 369 and zod: "4.4.3", which already differ from what the build prints at base (714, zod 4.6.7). No test reads them; left alone.
  • Round 1 (contract review 6670092320, PASS, one should-fix, adopted by the seat). refuseUnfitDefaultValue judged the type before the token, so { type: 'user', defaultValue: 'current_user' } read "no record exists while the blueprint is designed" and { type: 'date', defaultValue: 'NOW()' } read "a literal date is fixed when the blueprint is written", both false of a token. The isRuntimeDefaultToken check now runs first for every type in the table. An admitted type keeps its message (… default is ${rule.takes}; write one, or remove defaultValue.); a refused type says the value is a runtime token, that a blueprint default is a literal, that the type takes no defaultValue in a blueprint (with the admitted list), and ends Remove defaultValue from this field.; rule.takes is not read there. Still refused, no token admitted. One new pin; ablation 3 below. origin/main merged in (a merge commit, not a rebase). The describe's "carried into the object field" sentence stays as written: it waits on cloud#3633 by the ruled sequencing.
  • Branch is the harness-assigned claude/project-thread-7e1clu, not claude/issue-30584-*; claim 6668151062 names it.

Verification

Readings on objectstack-ai/objectstack. Round 1 head 32b39007 (fix 80d143dc, then a merge of origin/main at 66a8822b; merge base for the change set 66a8822b). Round 0 head 13a078c8 (base 14c67d44); its readings are kept below where marked.

Repro at base 14c67d4

SolutionBlueprintSchema.safeParse of a blueprint whose boolean field carries defaultValue: false → success: true, parsed field keys [name, type], the key stripped. The strict mirror strips it too.

Ablation 1 (round 0, at 13a078c) — the type check detached

node scripts/ablation-replace.mjs --file packages/spec/src/ai/solution-blueprint.zod.ts --anchor '}).superRefine(refuseUnfitDefaultValue, { when: () => true }));' --replacement '})); // ABLATION: type check detached': anchor 1 → 0, blob 16639542 → cdf6df65. src/ai/solution-blueprint.test.ts → Tests 5 failed | 91 passed (96): the admitted-set, wrong-type, own-option, token and multiselect pins red. Restored: blob == HEAD (16639542), git diff HEAD empty. (The never-stripped cross-product pin stays green here by design: with no check the lenient side keeps every value.)

Ablation 2 (round 0, at 13a078c) — the lenient key renamed (the strip)

Same tool, anchor defaultValue: z.union([z.boolean(), z.number(), z.string()]).optional() → ablationStripped: …: blob 16639542 → f7c41b8f. → Tests 9 failed | 87 passed (96): the field key-parity pin, the key-survives pin, the never-stripped cross-product pin and the describe pin red with the refusal pins. Restored: blob == HEAD, git diff HEAD empty.

Ablation 3 (round 1) — the old type-before-token order restored

node scripts/ablation-replace.mjs --file packages/spec/src/ai/solution-blueprint.zod.ts --anchor ' // A token is judged before the type. On a type that takes no default, that' --replacement the round-0 if (!('takes' in rule)) { … return; } block placed back ahead of the token check: anchor 1 → 0, blob b490c570 → 234c4bbe. src/ai/solution-blueprint.test.ts → Tests 1 failed | 96 passed (97): the new pin "refuses a runtime token on a type that takes no default with the token's reason, never the type's" red. Restored: blob == HEAD (b490c570), git diff HEAD empty.

Suites

Round 1, at 32b39007:

  • Full build after the merge, turbo run build --filter=!@objectstack/docs --concurrency=3 under the lock: 118 successful, 118 total, exit 0; git status clean afterwards.
  • src/ai/solution-blueprint.test.ts: 97 passed (97).
  • pnpm --filter @objectstack/spec test under the lock: Test Files 817 passed (817), Tests 25786 passed | 1 todo (25787), command-exit 0.
  • pnpm --filter @objectstack/spec typecheck: exit 0; check:test-typecheck OK, 52 file(s) / 240 error(s) / 131 pinned signature(s), unchanged. (A first run without a raised heap died on a JavaScript heap out-of-memory inside tsc -p tsconfig.test.json; rerun with NODE_OPTIONS=--max-old-space-size=6144, green.)
  • pnpm --filter @objectstack/spec run check:generated: exit 0, every artifact current (same heap note: the first run's only red was check:test-typecheck on that out-of-memory, not a stale artifact).
  • eslint on the two changed source files: exit 0, no output. node scripts/check-nul-bytes.mjs: OK.

Round 0, at 13a078c8:

  • pnpm --filter @objectstack/spec test (under os-verify-lock.sh) at 13a078c8: Test Files 817 passed (817), Tests 25750 passed | 1 todo (25751), command-exit 0. The first run at 5d57ab6a was 1 red: scripts/dropped-refinements.test.ts pins the ledger's droppedRefinementSites total (expected 1023, read 1020); 13a078c8 moves the total.
  • src/ai/solution-blueprint.test.ts alone: 96 passed (96).
  • pnpm --filter @objectstack/spec typecheck: exit 0; check:test-typecheck OK, 52 file(s) / 240 error(s) / 131 pinned signature(s), unchanged.
  • pnpm --filter @objectstack/spec run check:generated: exit 0, every entry current (the first run before the regen commit was red on check:docs only; --fix regenerated the reference page, committed in 5d57ab6a).
  • eslint on the two changed source files: exit 0, no output.
  • packages/lint/src/validate-expressions.formula-one-hop.test.ts (reads both blueprint field shapes): 13 passed (13), after a lint closure build.
  • Full build, turbo run build --filter=!@objectstack/docs --concurrency=3 under the lock: 118 successful, 118 total, exit 0; git status clean afterwards (no generated file moved).
Derived gates

Round 1: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 32b39007: 102 derived families, the same list as round 0. Each was run on that head after the post-merge build, with its exit code written to disk before any pipe (cmd :: exit N), then reconciled: node scripts/pm/dispatch-gates.mjs --ran r1-ran.list --repo objectstack-ai/objectstack → Run reconciliation — 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. All 102 exited 0 on the first run; no prerequisite refusal. check:adr-0087-registration --base origin/main on the committed head: exit 0 (no declared-breaking changeset). Changed lines 450 (+441 / -9, 6 files) against merge base 66a8822b, under the 3000-line threshold.

Round 0: node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 13a078c8: 102 derived families. Each was run on that head with its exit code written to disk before any pipe (cmd :: exit N), then reconciled:

node scripts/pm/dispatch-gates.mjs --ran ran-final.list --repo objectstack-ai/objectstack → Run reconciliation — 102 derived, 102 run, 0 NOT-MEASURED, 0 UNRUN, exit 0. Every one of the 102 exited 0.

Six of them first exited 3 (PREREQUISITE NOT MET, built output missing): check:doc-formula-expressions, check:doc-security-posture (lint dist), check:skill-examples (client-react / client declarations), check:docs-transcript-drift (lint dist), check:dual-build-cjs-loads (several dists), check:lean-entry-closure (objectql dist). After the full build above, all six reran with exit 0; those are the readings counted. check:adr-0087-registration --base origin/main ran on the committed head: exit 0. Changed lines 430 (+421 / -9, 6 files), under the 3000-line threshold.

Dogfood battery: n/a — no ! in the title and no (narrowing) arm.


Generated by Claude Code

🤖 Generated with Claude Code

https://claude.ai/code/session_01L6bhgUCEUicLCQ4tkXayK2

…I-built boolean flag defaults to false

`BlueprintFieldSchema` and the strict structured-output mirror `StrictField`
gain `defaultValue`: a boolean / number / string union, optional on the lenient
side and required-but-nullable on the mirror. The lenient field checks the
value against its `type` with the field schema's own literal contract
(`checkLiteralDefaultValue`): 21 types take a default, the other 28 are refused
by name at the key, and a select / radio default must be one of the field's
own option values.

Claude-Session: https://claude.ai/code/session_01L6bhgUCEUicLCQ4tkXayK2
Co-authored-by: Claude <noreply@anthropic.com>
…ield's dropped refinement

The `defaultValue` type check is a lenient-side refinement with no JSON Schema
form (a select default must be one of the field's own option values), so the
three published schemas that reach the blueprint field record its site in
`dropped-refinements.baseline.json`. Adds the `@objectstack/spec` minor
changeset.

Claude-Session: https://claude.ai/code/session_01L6bhgUCEUicLCQ4tkXayK2
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:ai tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface labels Nov 5, 2026
@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ⚠️ 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json, packages/spec/dropped-refinements.baseline.json), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

What this run could not see
  • 2 changed file(s) yielded no anchor (packages/spec/authorable-surface/ai.json, packages/spec/dropped-refinements.baseline.json) — pages documenting those are invisible to this run
  • 3 anchor(s) matched too much of the corpus to be a work list: defaultValue (symbol, 41 pages), master_detail (symbol, 40 pages), defaultValue (literal, 41 pages)
  • 45 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 63 of 221 client-bound route-ledger rows — the other 158 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 158: 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; 103 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 — 170 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 b77c160a02c2bb69ad6c986461b2633fa8d28c18 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json b77c160a02c2bb69ad6c986461b2633fa8d28c18

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract-review-claim: session_01L6bhgTqqeHSwZYJmApbDCa · head 13a078c · 2026-11-05T21:09Z

Full review at CONTRACT_REVIEW_TIER (published schema face: ai/solution-blueprint.zod.ts; Clause-②: yes (widening), Metadata-protocol: added), run by an isolated at-tier subagent of this seat.


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 13a078c8a8cc60ad7f2fbd605468f1ada453234d
Local-runs: none
Scope: packages/spec/src/ai/solution-blueprint.zod.ts + packages/spec/src/data/default-value-shape.ts, packages/spec/src/data/default-value-tokens.ts, packages/spec/src/data/field.zod.ts (the FieldType enum and the FieldSchema defaultValue refinement), packages/spec/src/data/field-value.zod.ts (valueSchemaFor and the value classes), packages/spec/dropped-refinements.baseline.json with packages/spec/scripts/lib/dropped-refinements.ts and packages/spec/scripts/build-schemas.ts (the rule and the ledger gate that enforces it, read as one change), packages/spec/authorable-surface/ai.json, packages/spec/src/automation/io-node-config.zod.ts (the superRefine with when precedent), packages/lint/src/validate-expressions.formula-one-hop.test.ts and ui/packages/plugin-chatbot/src/mapMessages.ts (the in-repo readers of the blueprint shapes). The rest of the diff (packages/spec/src/ai/solution-blueprint.test.ts, content/docs/references/ai/solution-blueprint.mdx, .changeset/30584-blueprint-field-default-value.md) is the lane seat's ACCEPT at TIER_DEFAULT, exactly as a standalone PR of those files.

Inputs read: card #30584 (body; comments 6666474709, 6668086354, 6668151062, 6669582413), PR #30655 (body, file list: 6 files, +421 / -9, net diff against merge-base 14c67d44), and the check-runs on the head, read at 2026-11-05T21:31Z. objectstack-ai/cloud#3619 and cloud#3633 were NOT readable from this session: REST and gh api both answer 403 (repository not enabled for the session). Ruling B is read through the card's own quote 「改选 B,#3633 等 spec」 and the card's Measured section; nothing else about those two cloud records is asserted here.

Check-runs on the head: 65 runs, every one completed before this record was written (first read 2026-11-05T21:11Z, polled at 3-minute spacing, last run completed 2026-11-05T21:31Z); 57 success, 8 skipped, 0 failed, nothing pending. The ten required contexts: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (8/8 shards), Build Core success, Governed Surface Queue Guard success, Lint UI skipped (no ui/ path in the diff), Type Check UI success, Test UI success (16/16 shards), Build & E2E UI success, Live E2E UI success. Also green: Check Changeset, Spec property liveness, PR-body guards, Check PR Size, every Lint · and Type Check · job. These conclusions are the gate verdicts; the dev's 102-family local reconciliation in the PR body was read, not relied on.

① Derived judgments

  • Lenient accept set widens by one optional key — right. BlueprintFieldSchema (solution-blueprint.zod.ts:297) gains defaultValue: boolean | number | string, optional; packages/spec/authorable-surface/ai.json gains the single row ai/BlueprintField:defaultValue; the reference page regenerates two rows. Every blueprint that parsed before and carried no defaultValue parses byte-identically.
  • Strict mirror gains a REQUIRED nullable key (StrictField.defaultValue, :609) — right, and not a break. The mirror is declared in its own header (:533-546) as "used ONLY as the generateObject output contract"; the blueprint "is never persisted on its own" (:23-26), so no stored artifact carries the strict shape. In this repo nothing produces a strict object outside the spec test file (whose seven fixtures the PR updates) — validate-expressions.formula-one-hop.test.ts:127-128 reads .shape only and mapMessages.ts lifts name / label from the plan envelope. Every earlier mirror key landed the same way (aggregate, viewName, filters, startDateField: required-but-nullable, declared yes (widening)). A hand-built strict object outside this repo now needs the key: the changeset's last paragraph says so.
  • The 28 refused types — before this head, { type: 'date', defaultValue: '2026-01-01' } parsed with the key stripped; now it is refused by name. This is NOT a narrowing of the published accept set, by the repo's own classifier: the key was absent from authorable-surface/ai.json at base, so no value at it was "what a metadata author can write"; check-changeset-no-major.mjs:2316 states the rule ("a new authorable key ... a widening of a published accept set by definition"); the two prior blueprint-key PRs that added a key whose values the lenient parse refuses (9f024d8 filters / sort, 6ec0e12 option kind / stageField) were declared yes (widening) and yes; and the refusal-by-name is what ruling B ordered ("refuses the rest by name rather than stripping them"). So Clause-②: yes (widening) misses nothing. For the record: had it been a narrowing, the spelling would be yes (narrowing) (one arm — clause2-line.mjs:111-121 admits no two-arm form) with Metadata-protocol: narrowed, a BREAKING banner, an ADR-0087 marker and the dogfood battery. Not owed here.
  • New exports — none. BLUEPRINT_DEFAULT_VALUE_RULES (:142), BLUEPRINT_DEFAULT_VALUE_ADMITTED (:199), DEFAULT_VALUE_TYPE_RULE, booleanFlagRule and refuseUnfitDefaultValue (:235) are module-private (const / function, no export). The PR file list carries no api-surface/ or export-origins change, which is consistent. The public surface moves only by the authorable key and the describes; that is what is meant.
  • The dropped-refinements ledger: three new sites (ai/BlueprintField root "", ai/BlueprintObject fields.element, ai/SolutionBlueprint objects.element.fields.element), total 1020 to 1023. Right: the refinement sits on BlueprintFieldSchema, reached from exactly those three published schemas; the strict mirror carries no refinement. The ledger is hand-edited by design (lib/dropped-refinements.ts:68-80, "no gen: script") and build-schemas.ts checks it path by path in both directions, so a wrong or missing site reds the spec build.

② Semver level

@objectstack/spec: minor, Clause-②: yes (widening), Metadata-protocol: added — all three match what the diff publishes: a new authorable key on a published schema is added, and added takes at least minor on the package the diff moves (AGENTS.md Post-Task Checklist 3; check-changeset-no-major.mjs header). Both declaration lines are present, line-initial, on the PR body and the changeset body alike. Skills-docs: none holds: no file under skills/** restates a blueprint field key (the only summaryOperations docs hits are the object field's own, content/docs/data-modeling/field-types.mdx), and the reference page is regenerated in the diff. No (narrowing) arm and no ! in the title, so the dogfood battery is n/a (os-dev.md:276), as the dev marked.

③ Boundary flags

  • Liveness: no ledger row. Answered, holds. packages/spec/liveness/ at the head has no blueprint or solution file and SPEC_ONLY_SCHEMAS names no blueprint schema; the blueprint is declared never-persisted (:23-26), so the card's Acceptance row has no ledger to live in. The Spec property liveness check-run on the head is green. The carrier named for "planned until the cloud half lands" is cloud#3619 / cloud#3633, which this session could not read (403); the card's own text says cloud#3619 is pm:blocked on this card, and the built field's defaultValue is already governed by liveness/field.json. Accepted as a measured deviation.
  • Three hand-edited dropped-refinements sites, 1020 to 1023. Answered, right (① above): hand-editing is the ledger's own rule, the three paths are the three reaches of the one refinement, and the build gate verifies them.
  • checkbox does not exist; checkboxes refused with the list types. Answered, right. FieldType (field.zod.ts:49-110) has boolean, toggle and checkboxes and no checkbox; checkboxes is in MULTI_OPTION_TYPES (field-value.zod.ts:146-148), whose stored value is an array, so it belongs with multiselect / tags.
  • Harness branch claude/project-thread-7e1clu. Answered: claim 6668151062 names it as the card thread's branch, the PR head is on that branch in the base repository (not a fork), and the dispatched executor posted no second claim. The Duplicate Fix Guard is advisory. Implemented-by: below signs it.
  • The describe promises the built field's default before any applier carries it. Not a dev flag, raised here: "carried into the object field as its defaultValue" is true of no build until cloud#3633 lands. That is the ordering ruling B fixed (spec first, cloud waits), the same shape as the expression slot in ADR-0078's landing note, and the PR body and changeset both say the cloud applier moves on its own pin. Not a finding; the cloud half is cloud#3619's to close.
  • open_questions: empty. Nothing to escalate.

④ Design judgment

packages/spec/src/ai/solution-blueprint.zod.ts, against ruling B as the card quotes it, FieldSchema's own defaultValue contract, and the first consumer (cloud's blueprint tools and applier, outside this repo; in this repo the lint and UI readers named in Scope).

  • Admitted / refused table vs FieldSchema. Every admitted value is one the built field admits: an admitted value goes through checkLiteralDefaultValue (default-value-shape.ts:205-209), which is valueSchemaFor(def, 'stored'), the same contract FieldSchema runs on its literal branch (field.zod.ts:3241-3244). The 21 admitted types are the boolean, numeric-minus-summary, string-minus-credential-and-generated, clock-time and single-option classes of field-value.zod.ts:57-70, :83-85, :141-143; nothing admitted here is refused on a built field (the PR's last-but-one test walks all 21 through FieldSchema). Refused here though FieldSchema takes a literal: date / datetime, the reference and file types, the list types, the structured types, credentials and generated values. Of those, the two a blueprint author would reasonably set are user (or lookup to sys_user) with current_user and date / datetime with NOW() — both runtime tokens, which the describe says a blueprint does not carry. That is the stated design call (literal only; widening later is additive), and I accept it. percent, currency, rating, color, email are admitted, as they should be.
  • Should-fix — the refusal gives a false reason for the one value an author writes on a refused reference or date type. refuseUnfitDefaultValue judges the type (:247) before the token (:252), so { type: 'user', defaultValue: 'current_user' } is refused with "its value is the id of a user record, and no record exists while the blueprint is designed" (:169), and { type: 'date', defaultValue: 'NOW()' } with "a literal date is fixed when the blueprint is written" (:164). Both reasons are false of a token: current_user names no record and NOW() is not frozen; the true reason is the one the token branch states one line later ("a blueprint default is a literal value"). An author steered by the wrong sentence writes a record id or a literal day, which is worse. Fix in place: run the isRuntimeDefaultToken check before the takes check for every type, with the token sentence (or admit the token on the types FieldSchema admits it on, which is the additive widening). Prescriptive text is the house rule (AGENTS.md, "the lesson goes into the text"); the refusal itself is right, so this is not a FAIL.
  • Select / radio own-options check. Right. The lenient option value is z.string().regex(SNAKE_CASE) (:286), so a non-string value is refused by the option's own issue; optionCodes (field-value.zod.ts:733-736) stringifies whatever reaches it, so a numeric option value cannot let a mismatched default through. options absent or [] is refused as "declares none" (:259-262), which is stricter than valueSchemaFor's any-string fallback and is the right reading of "one of its own option values". The kind key on an option is not read, correctly.
  • Runtime-token refusal. Right on every admitted type, and consistent with FieldSchema, whose token branch also runs before the literal branch (field.zod.ts:3229-3239): a select whose options declare a current_user value is refused as a token on both schemas. The only token sentence that is wrong is the ordering case above.
  • when: () => true. Precedented exactly at io-node-config.zod.ts:1141-1145 (superRefine(httpConfigRefusals, { when: () => true })); view.zod.ts and analytics-carrier-filter.ts use the $ZodCheck spelling of the same idea. Issue shapes: an array or object default yields the union's invalid_union AND the by-name custom issue, both at defaultValue, the second explaining the first (the multiselect pin); a lenient null yields the union issue only (:240 returns); a type outside FieldType yields that key's issue only (:241 returns); an admitted type with a wrong scalar yields exactly one issue (pinned, issues: 1, named: 1). Not confusing.
  • The describes against the code. "The blueprint tools strip the null" is the mirror header's pre-existing claim about cloud code (:544-546), inherited by every nullable key and not verifiable here. "The value becomes the built field's defaultValue" is true of the design and of no build until cloud#3633 lands (③ above). The boolean-flag sentence ("never required ... carries defaultValue false") matches the card's ruled design item 3 and cloud's guidance as the card quotes it, and each side spells its own unset required. The admitted list in both describes is built from the table (:199-206), so describe, refusal and changeset read the same 21 types, and the describe pin checks every name.
  • Strict mirror validity. Every StrictField key is present and required; defaultValue is z.union([boolean, number, string]).nullable() (:609). The same construct already ships in the mirror at :644 (StrictViewFilterRule.value, a nullable union that also carries an array) and in the widget condition.value, so the providers' strict subset is not newly exercised. type is the full FieldType enum on both sides (:280, :583), so the cross-product pin covers every member.
  • Tried and not broken: a select default equal to another field's option (refused at the right path, pinned); textarea: '' (a real default, admitted, matches the engine's presence rule); time: '09:00:00' (admitted by ClockTimeValueSchema, the describe under-promises with "HH:MM", harmless); summary (numeric class, refused by the table, consistent with COMPUTED_VALUE_TYPES); the table's exhaustiveness (a Record keyed by every FieldType member, so a new member fails to compile until placed).

Findings: one should-fix (token-before-type ordering of the refusal reason, solution-blueprint.zod.ts:247-258). No must-fix.

Implemented-by: claude/project-thread-7e1clu
Reviewed-by: session_01L6bhgTqqeHSwZYJmApbDCa

VERDICT: PASS


Generated by Claude Code

… before its type is judged

A `user` field with `defaultValue: 'current_user'` or a `date` field with
`defaultValue: 'NOW()'` was refused with that type's own reason (no record
exists yet / a literal day is frozen), which is false of a token. The token
check now runs first for every type, and on a type that takes no default the
refusal says it is a runtime token, that the type takes no defaultValue in a
blueprint, and to remove it.

Claude-Session: https://claude.ai/code/session_01L6bhgUCEUicLCQ4tkXayK2
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 32b39007ee649da8520bdcc92e212a81871da576
Local-runs: none
Scope: packages/spec/src/ai/solution-blueprint.zod.ts + packages/spec/src/data/default-value-tokens.ts (isRuntimeDefaultToken), packages/spec/src/ai/solution-blueprint.test.ts (the new pin, read to judge the hunk), and the merge commit's own three readings (git diff-tree --cc, the diff against each parent). Delta review: this record judges the two commits since 13a078c8 (80d143dc, the fix; 32b39007, the merge of origin/main at 66a8822b) and re-states the round-0 lines that the moved tree could have changed. Everything else on this head is covered by the round-0 record 6670092320 (PASS), whose readings this delta re-checked against the new base and found unmoved: of the 11 commits main took between 14c67d44 and 66a8822b, none touches any cited file (data/field.zod.ts, data/field-value.zod.ts, data/default-value-shape.ts, data/default-value-tokens.ts, scripts/lib/dropped-refinements.ts, scripts/build-schemas.ts, automation/io-node-config.zod.ts, packages/spec/liveness/, the lint reader, the two classifier scripts) and none touches any of the six PR files. The rest of the diff (the test file, the regenerated reference page, the ledger and authorable-surface artifacts, the changeset) remains the lane seat's ACCEPT at TIER_DEFAULT.

Inputs read: the card thread's round-1 report 6670803741; PR #30655 as refreshed at 2026-11-05T22:09Z (body, file list: 6 files, +441 / -9, net diff against merge-base 66a8822b); the two commits on the tree; the check-runs on 32b39007, read at 2026-11-05T22:17Z. objectstack-ai/cloud is still not readable from this session (403), as in round 0.

Check-runs on the head: 69 runs, every one completed before this record was written (first read 2026-11-05T22:10Z, polled at 3-minute spacing, last run completed 2026-11-05T22:16Z); 59 success, 10 skipped, 0 failed, nothing pending. The ten required contexts: Lint & Repo Gates success, TypeScript Type Check success, Test Core success (8/8 shards), Build Core success, Governed Surface Queue Guard success, Lint UI skipped (no ui/ path in the diff), Type Check UI success, Test UI success (16/16 shards), Build & E2E UI success, Live E2E UI success. Also green: Check Changeset (both runs, the second on the 22:09Z body refresh), Spec property liveness, PR-body guards (both runs), Check PR Size, every Lint · and Type Check · job; the four extra skipped beyond round 0 are the body-refresh re-runs of Auto Label and Check PR Size and the usual opt-in jobs. These conclusions are the gate verdicts; the round-1 report's local reconciliation was read, not relied on.

① Derived judgments

  • No accept-set or public-surface change beyond round 0. The net diff against the new base is byte-identical, hunk for hunk, to the diff of 80d143dc against the old base (14c67d44), on all six PR files. The lenient key, the strict key, the admitted and refused sets, the ledger's three sites and the authorable-surface row are exactly what 6670092320 judged. 80d143dc moves only the ORDER of two checks inside the module-private refuseUnfitDefaultValue and one refusal message; no export, no key, no describe.
  • Nothing newly admitted. A runtime token is refused on every type: on an admitted type by the token branch as before, on a refused type now by the token branch instead of the type branch. The only input whose verdict changed is none; the only input whose MESSAGE changed is a token on one of the 28 refused types.
  • The merge 32b39007 is clean. git diff-tree --cc 32b39007 prints the sha and no hunk (no hand-resolved conflict anywhere in the merge); the diff of the merge against its first parent 80d143dc is empty on all six PR files; the diff of main between the two bases is empty on all six PR files; the diff against the second parent 66a8822b restricted to the PR files equals the net diff. No generated artifact text-merged (the os-regen driver had nothing to defer: no PR file moved on main).

② Semver level

Unchanged and still right: .changeset/30584-blueprint-field-default-value.md is byte-identical to round 0 (@objectstack/spec: minor, Clause-②: yes (widening), Metadata-protocol: added); the refreshed PR body carries the same two line-initial declarations and the same Skills-docs: none line, which still holds (no skills/** file restates a blueprint field key; 80d143dc touches no describe). No (narrowing) arm and no ! in the title, so the dogfood battery stays n/a. Check Changeset on the new head is in the tally below.

③ Boundary flags

  • The PR body's Round 1 note against the tree. Matches. Its quoted admitted-type message (… default is ${rule.takes}; write one, or remove defaultValue.) is what :252 still builds; its account of the refused-type message (runtime token, literal, "takes no defaultValue in a blueprint" with the admitted list, ending Remove defaultValue from this field., rule.takes not read) is :253-254 and :255-256; "a merge commit, not a rebase" is the two-parent 32b39007; the blob b490c570 named in ablation 3 is the file's blob at the new head (index 16639542e..b490c5707). The line budget (+441 / -9, 6 files, base 66a8822b) matches the file list read from the API.
  • The round-1 report's fifth deviation (the r1 body was handed to the seat as a file, not patched by the dev) is a process note on who wrote the body; the body as stored at 22:09Z is what was judged above. No boundary question is open; open_questions is empty.
  • The four round-0 deviations are unchanged and stay answered as in 6670092320.

④ Design judgment

packages/spec/src/ai/solution-blueprint.zod.ts at 32b39007, the 80d143dc hunk, against the round-0 should-fix and FieldSchema's own token-before-literal order (field.zod.ts:3229-3244, unmoved).

  • The should-fix is landed as asked. isRuntimeDefaultToken(defaultValue) now runs at :250, before the 'takes' in rule branch at :259, for every type the table holds (the own-property guard at :241 still returns first for a type outside FieldType, leaving that key's own issue, as before). The docblock's order sentence (:224-226) was updated to match.
  • An admitted type keeps its message, byte for byte. instead at :252 is the exact tail the round-0 token branch carried ("A "type" field's default is TAKES; write one, or remove defaultValue."), appended to the same head sentence; the test "refuses a runtime token on an admitted type" is unchanged and still pins text / time / select.
  • A refused type now gets the token reason and nothing false. :253-254 says the value is a runtime token resolved at record creation, that a blueprint default is a literal the built field stores as written, that a "type" field takes no defaultValue in a blueprint, gives the admitted list, and ends "Remove defaultValue from this field." It reads neither rule.takes (the arm has none) nor rule.because (the sentence that was false of a token). Tried: user / lookup + current_user, date + NOW(), datetime + now() (the case-insensitive spelling isNowDefaultToken accepts) — each one issue, at defaultValue, naming the field and type.
  • The new pin asserts the right things (solution-blueprint.test.ts:1425-1437): for user, lookup (with reference: 'sys_user'), date and datetime it requires exactly one issue, one by-name refusal at the key, the substring "is a runtime token", the substring A "type" field takes no defaultValue in a blueprint, the exact ending "Remove defaultValue from this field.", and the ABSENCE of both type reasons ("no record exists", "fixed when the blueprint is written"). The PR body's ablation 3 (old order restored, this pin alone red) is read, not relied on; the pin's text is what makes the order load-bearing.
  • No other refusal path or message moved. The takes-less branch (:259-262), the own-options branch (:265-268) and the literal branch (:270-274) are byte-identical to round 0 and keep their relative order; for an admitted type the sequence of outcomes is the same as before (the moved takes check was a no-op on that path), so no existing pin's expectation changes, and each branch still returns after one ctx.addIssue at ['defaultValue'] — exactly one issue per refusal, at the field's defaultValue. The when: () => true double-issue shape for an array or object default is untouched (a token is a string, so the union never fails on this path).
  • Consistency with FieldSchema. The built field's own gate also discriminates the token before the literal (field.zod.ts:3229-3239); the blueprint now reads the same way, which closes the only divergence round 0 named.

Findings: none. The round-0 should-fix is closed by 80d143dc; the merge adds nothing to judge.

Implemented-by: claude/project-thread-7e1clu
Reviewed-by: session_01L6bhgTqqeHSwZYJmApbDCa

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

domain:spec#3 · os-sales · session_01L6bhgTqqeHSwZYJmApbDCa

维护者速读

  • 改了什么:AI 搭建方案(蓝图)的字段可以写 defaultValue 了。宽松 schema 与 strict 结构化输出镜像都加了这个键,并按字段类型校验:布尔取 true/false,数字取数字,单选取本字段自己的选项值;21 种类型可写,其余 28 种写了就按类型名拒收,不再悄悄丢掉;current_user、NOW() 这类运行时令牌先按令牌拒收,理由说的是令牌,不借用类型的理由(第 1 轮)。
  • 为什么改:AI 照搭建指引把「是否完成 / 已付款」写成 defaultValue: false,以前解析成功但这个键被剥掉,建出的字段没有默认值,新记录是空而不是「否」,按「否」筛选会漏掉;按 cloud#3619 裁决 B 补上 spec 这一半。
  • 风险与回滚:只加不收,之前能解析的蓝图照旧能解析;strict 镜像的生产者每个字段要多发一个 defaultValue(无默认填 null)。回滚即还原 solution-blueprint.zod.ts、它的测试和台账三处站点,无数据迁移。
  • 先看:packages/spec/src/ai/solution-blueprint.zod.ts(准入表 BLUEPRINT_DEFAULT_VALUE_RULES 与 refuseUnfitDefaultValue)、packages/spec/src/ai/solution-blueprint.test.ts(末尾 blueprint field defaultValue 一节)、packages/spec/dropped-refinements.baseline.json(新增三处站点)。
  • 两分钟验证:pnpm --filter @objectstack/spec exec vitest run src/ai/solution-blueprint.test.ts,预期 97 个用例全绿。
  • 需要你做的:无,席位落地。

席位意见:契约复审两轮都 PASS。第一轮唯一的 should-fix:运行时令牌写在不收默认值的类型上(如 user + current_user、date + NOW())时,拒收理由借用了类型自己的说法,对令牌来说是错的。第 1 轮改成先判令牌,理由如实,仍然拒收;增量复审确认其余拒收路径一字未动。描述里"值会带进建出的对象字段"要等 cloud#3633 的应用端落地才成立,这是裁决定下的先 spec 后 cloud 顺序,不算缺陷。

席位核对 — ACCEPT · domain:spec#3 · head 32b39007ee649da8520bdcc92e212a81871da576

  • Shape: 6 files (+441 −9), draft, base main, first card reference Fixes #30584. Card thread of claim 6668151062, branch claude/project-thread-7e1clu. Two commits since round 0's 13a078c8: 80d143dc (the should-fix) and 32b39007 (a merge of origin/main at 66a8822b).
    • Hand-written:
      • packages/spec/src/ai/solution-blueprint.zod.ts: defaultValue on BlueprintFieldSchema (optional) and on the strict mirror's field (nullable, required, as every key in that mirror);
      • the BLUEPRINT_DEFAULT_VALUE_RULES table keyed by every FieldType;
      • refuseUnfitDefaultValue;
      • both describes;
      • the test block;
      • three hand-edited sites in packages/spec/dropped-refinements.baseline.json (1020 → 1023);
      • the changeset.
    • Regenerated: content/docs/references/ai/solution-blueprint.mdx, packages/spec/authorable-surface/ai.json (one key).
    • No new export: the table and the refinement are module-private.
  • Premise measured: at base 14c67d44, a boolean blueprint field with defaultValue: false parsed success: true on both schemas, with the key stripped (dev report 6669582413). So an AI-built 是否 / 已付款 flag came out with no default, which is the card's symptom. cloud#3619 ruling B, quoted on the card, orders the spec half: a typed key, a type-checked value, and refusal by name instead of stripping.
  • Gates:
  • Contract review:
    • 6670092320 on 13a078c8: PASS, Served-tier: CONTRACT_REVIEW_TIER, no must-fix, one should-fix: a token on a refused type was refused with the type's own reason (user, date).
    • Delta 6670989717 on 32b39007: PASS, no findings.
      • Token-first now runs for every type.
      • The admitted message is byte-identical, and the refused-type message reads neither rule.takes nor rule.because.
      • Nothing is newly admitted, and every other refusal path is unmoved.
      • The merge is clean, and the refreshed body matches the tree.
  • Declarations: Clause-②: yes (widening), Metadata-protocol: added, Skills-docs: none (no file under skills/** restates a blueprint field key), @objectstack/spec minor. The body and the changeset agree. The review judged the 28 refused types against the repo's classifier. The key was absent from the authorable surface at base, and ruling B ordered refusal by name, so this is no narrowing of a published accept set and no (narrowing) arm is owed. The strict mirror's required-nullable key is an output contract the model fills, not a stored or persisted shape.
  • Accepted deviations:
    • Liveness has no ledger row. A blueprint is not one of the 43 governed metadata types; the built field's defaultValue is governed by liveness/field.json (live). Carrier: cloud#3619 / cloud#3633.
    • The dropped-refinements ledger is hand-edited by design: the option-membership rule reads a sibling key and has no JSON Schema form. build-schemas.ts verifies it path by path, and Build Core is green.
    • The brief's example checkbox type does not exist. checkboxes is a multi-option type and is refused with the list types.
    • Harness branch claude/project-thread-7e1clu, named by claim 6668151062.
    • origin/main was merged, not rebased.
    • Neither the reviewer nor this seat could read objectstack-ai/cloud (403). Ruling B is read through the card's quote.
  • Carried: cloud#3633, the cloud applier that carries a blueprint field's defaultValue into the built object field. Until it lands, the describe's "carried into the object field" names the ruled end state rather than today's build, which is the ruled spec-first sequencing. Nothing filed here.
  • Landing: not governed (check-governed-merges --pr 30655: 0 of 6 paths, 450 lines ≤ 3000). The hand-off is pr_ready plus automerge_enable, through the merge queue.

Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review November 5, 2026 22:20
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Nov 5, 2026
Merged via the queue into main with commit 4bf9a2d Nov 5, 2026
70 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-7e1clu branch November 5, 2026 22:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation protocol:ai size/m tests tooling Fix lands in tooling, not a product package — enters pm:queue only with Unblocks: or a named surface

Projects

None yet

1 participant