feat(spec,automation): create_record / update_record fields.* accept the CEL value envelope — declared and evaluated together - #20205
Conversation
…-role CEL envelope slot
The expression ledger gains two `value` rows, `create_record.fields.*` and
`update_record.fields.*`: the same shape and dialect rules the
`assignment.assignments.*` slot has. A field value may now be a
`{ dialect: 'cel', source }` envelope beside a `{token}` template or a
literal; a plain string keeps its template meaning unchanged.
- The CRUD `fields` map's values carry the value-slot contract
(`FlowValueSlotSchema`), declared to the reconciliation ratchet through
`LEDGER_DECLARED_NODE_CONFIG_SCHEMAS` because the descriptors publish the
map as `additionalProperties: true`.
- The refusal sentence is slot-neutral (`VALUE_ENVELOPE_REFUSAL`); the
published `ASSIGNMENT_VALUE_ENVELOPE_REFUSAL` is the same string, so a
refused field value is no longer told it is an assignment.
- One factory builds every value slot's contract, declared above the CRUD
schemas so `OS_EAGER_SCHEMAS=1` never meets it in its temporal dead zone.
- `resolveFlowNodeValueSlots` hands tooling every authored value of a value
slot (strings included) through the ledger's own path walk.
- Generated artefacts regenerated; the three new dropped-refinement sites
are declared in the ledger with its header totals.
The executor half lands in the next commit of the same change, so the slot
is never declared without being evaluated.
Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
…date_record fields.*
The executor half of the `fields.*` value slot, landing with its declaration.
- `crud-nodes.ts`: each top-level `fields` value that is envelope-shaped is
evaluated through `AutomationEngine.evaluateValueEnvelope` (the call the
`assignment` executor makes: one evaluator, one scope, one notion of
malformed); every other value interpolates exactly as the whole-map
`interpolate()` did, so no template spelling changes meaning. A malformed
or faulting envelope fails the node and writes nothing.
- `engine.ts` / `validate-expressions.ts`: the value-slot consumers read the
slot-neutral `FlowValueSlotSchema` and `VALUE_ENVELOPE_REFUSAL`.
- `@objectstack/lint`: an author-time warning points a `{…}` template
expression (arithmetic or one of the six functions) in any value slot at
the envelope. Plain references, the date macros and `$User` paths are not
hinted.
- `template.ts`: the `round()` arity refusal and its docblock prescribe
`round(x * 100) / 100.0`. In CEL `round()` is an int and int / int is
integer division, so `/ 100` drops the decimals there.
- `flows.mdx`: value slots, the envelope in a Create Record example, the
refusal doors, and the dialect table's scale-2 row.
Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ
Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewServed-tier: Read: cards #19938 (body, all 6 comments incl. 5811746886 / 5811795518 / 5816929495) and #11182 (body, all 8 incl. 5805777944 / 5795796051); PR #20205 object, body, 25-file list, full delta vs merge-base 560b724 (2107 diff lines), its 3 commits; at the head: the expression ledger, builtin-node-config.zod.ts, schemaless-node-config.zod.ts, crud-nodes.ts, logic-nodes.ts, engine.ts (valueEnvelopeRefusals / evaluateValueEnvelope), template.ts (interpolate, requireArity), validate-expressions.ts and its meta-guard test, metadata-protocol runtime-authoring-gate.ts, cli validate.ts, flows.mdx, the changeset, dropped-refinements.baseline.json and the build's checkDroppedRefinements, the changeset gates' headers (check-changeset-no-major, check-adr-0087-registration, check-empty-changeset, check-widening-tells, pr-automation WHICH LEVEL). Ran: detached worktrees at the head and at merge-base 560b724, pnpm install --frozen-lockfile, turbo builds of the service-automation / lint / metadata-protocol / cli closures under os-verify-lock (VERDICT command-exit 0 each), a 72-row door probe (12 values x 2 node types x 3 column types; doors FlowSchema.parse, the os validate rule path, registerFlow, a real run over ObjectQL with a recording driver, evaluateRuntimeAuthoringGate) on both trees and diffed, the real ① Derived judgments(a) Ruling D point 1, measured on the built trees (base 560b724 vs head): a valid envelope (b) No door disagrees at this head. The os validate rule path ( (c) Only the TOP-level field value is judged: measured, a nested (d) (e) Ruling D point 2: (f) Q2: (g) The (h) (i) Other published changes: ② Semver level
③ Boundary flagsBlocking: the head cannot land as it stands — it is unmergeable against current main and no CI exists for it. Main moved 5 commits past the merge-base 560b724 (4db1bf1 07:42Z … 1c8b320 08:40Z) before the dev's 08:05Z merge commit, which merged the stale 560b724 (the moving- CI at this head: absent — Implemented-by: VERDICT: PASS |
The merge took main's side of the five os-regen artefacts both sides had changed (content/docs/references/index.mdx, api-surface, declaration-map, export-origins and json-schema.manifest for automation); this commit regenerates them from the merged sources. Against main they differ only by this branch's own additions (FlowValueSlot / FlowValueSlotParsed / FlowValueSlotSchema, VALUE_ENVELOPE_REFUSAL, resolveFlowNodeValueSlots, and the +1 schema count). Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 3 package(s): 19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 4f0f80e5f8a07d1507268fec938e379b78d30d84 && git checkout 4f0f80e5f8a07d1507268fec938e379b78d30d84
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin ab820016b3e9691870e24a9bbb867336ee5e8f72 68123f42859e2fa7e3a8346a664c2e155e3ee0a2 && git checkout -B drift-repro ab820016b3e9691870e24a9bbb867336ee5e8f72 && git merge --no-ff 68123f42859e2fa7e3a8346a664c2e155e3ee0a2
node scripts/docs-audit/affected-docs.mjs --json ab820016b3e9691870e24a9bbb867336ee5e8f72
|
…elds-value-slot # Conflicts: # packages/spec/dropped-refinements.baseline.json
The second merge of main took main's side of content/docs/references/index.mdx (both sides changed it) and resolved the hand-edited dropped-refinements ledger: this branch's three entries on top of main's three new $ne sites, with the header totals recounted from the body (210 schemas, 580 sites). This commit regenerates the index from the merged sources; against main it differs only by this branch's +1 schema (FlowValueSlot). Claude-Session: https://claude.ai/code/session_01CiCTczDo7tGhafXjf61dUJ Co-authored-by: Claude <noreply@anthropic.com>
…elds-value-slot # Conflicts: # packages/spec/dropped-refinements.baseline.json
Contract reviewServed-tier: Delta over record 5854753486 (PASS at 94216fb). Read: that record; the patch-round report 5856232231 on #19938; PR #20205 object (twice, first and last), body, its 8 commits, the 35 check-runs / 12 Actions runs / combined status on 56f7747, the main ruleset's 7 required contexts, the two gate jobs' step lists, the docs-drift-check comment 5854858210 (edited 11:57Z); ① Derived judgments(a) Same change. Both diffs list the same 25 files with the same name-status, +1223 / −215 on each side. Excluding the 6 (b) Generated paths. Against main at 805af4f the PR changes 6 generated files, +35 / −10. The 10 removed lines are the same ten as at the reviewed head, each replaced by a (c) The hand-resolved ledger. Recounted from the file: old base 207 schemas / 574 sites; old head 210 / 577; main parents e7f69db 207 / 574, 3875ae6 207 / 577, 805af4f 207 / 588; new head 210 / 591, and at every one of the six refs the header equals (d) Merge integrity. Three merges, eac6351 (main e7f69db), d283101 (main 3875ae6), 56f7747 (main 805af4f). In each, every non-generated, non-ledger file the merge changed relative to its branch parent equals main's blob exactly (80, 72 and 41 files), none is content-merged or kept branch-side, and no main-side file's blob at the merge differs from main's except the generated / ledger paths named next. Content-merged: only the ledger, in merges 2 and 3 (judged in ① c). Merge 1 kept the branch's five stale generated blobs (the four (e) Docs drift. Re-derived at the head against 805af4f: 23 docs (19 hand-written, 4 release-owned), 35 anchors, 5 anchorless files — the bot's counts exactly. 18 of the 19 hand-written pages are listed only because they spell the literal node type ② Semver levelUnchanged from the earlier record: ③ Boundary flagsBlocking: none. The earlier record's blocking flag (unmergeable, no CI at 94216fb) is resolved by this delta: the three os-regen-merge laps landed, GitHub reads CI at this head: 35 check-runs on 56f7747, all completed — 33 success, 2 skipped ( Implemented-by: VERDICT: PASS |
…elds-value-slot # Conflicts: # packages/spec/dropped-refinements.baseline.json
Contract reviewServed-tier: Delta over record 5856341116 at 56f7747: one new commit, the merge 68123f4 (second parent ab82001, the PR's merge base); read with git fetch/diff/show of refs/pr-review/20205 against origin/main and the GitHub REST API (PR, check-runs, combined status, main's ruleset); ran only git, curl, sha256 hashing and a JSON recount. NOT MEASURED: no build, no test suite, no regeneration of the merge=os-regen paths, no re-run of any derived gate; the prior record's whole-set hash 05efa004e2fe0daa was not reproduced (its normalization is not stated), per-file byte equality decides (a) instead. ① Derived judgments(a) Same change. (b) Merges. One merge commit, 68123f4, merges ab82001 into 56f7747; ab82001 is the merge base. Its diff against its first parent is exactly main's 103 files between 805af4f and ab82001, and every one of those files' head blob equals main's blob except the ledger. The whole-tree diff of head against main is the 25 PR paths only. The six generated paths: main changed none of them between the bases, and the head-vs-main diff equals the reviewed diff (api-surface/automation.json +5/-0, declaration-map/automation.json +2/-0, export-origins/automation.json +5/-0, json-schema.manifest/automation.json +1/-0, references/automation/builtin-node-config.mdx +12/-5, references/index.mdx +5/-5). Every minus line is replaced by its superset line: the two import lines gain FlowValueSlotSchema / FlowValueSlot, "the variable takes" becomes "the slot takes", the two (c) Ledger. The head-vs-main diff of packages/spec/dropped-refinements.baseline.json is exactly the PR's three pairs, automation/CreateRecordConfig [fields.valueType], automation/FlowValueSlot [""], automation/UpdateRecordConfig [fields.valueType], plus the two header totals. Every entry main added or changed since 805af4f (#20223's new data/InlineGridColumn entry and its 12 inlineColumns.element sites across api/AssembledInstalledPackage, api/GetInstalledPackageResponse, api/InstalledPackageAtEitherStage, api/ListInstalledPackagesResponse, api/ObjectDefinitionResponse, data/Field, data/Object, data/ObjectExtension, system/AddFieldOperation, system/ChangeSet, system/CreateObjectOperation, system/MigrationOperation) is present byte-equal in head; no main key lost; keys still sorted; description and the other measured fields equal main. Recount from the head file: 211 entries, 604 sites; the header reads 211 / 604, MATCH, which is what packages/spec/scripts/dropped-refinements.test.ts lines 369-371 pin. A clean (d) CI at 68123f4: 35 check-runs, 33 success, 2 skipped, 0 failure or neutral; mergeable true, mergeable_state clean; combined status Vercel success. The body's closing keywords are Fixes #19938 and Fixes #11182 and no other (#15430 and #19939 appear only as prose references). Both issues are open. The head did not move during the review (68123f4 at start and at the end). ② Semver levelminor, unchanged from the reviewed change: the changeset at head declares ③ Boundary flagsBlocking: none CI at this head: 35/35 completed. Required by main's ruleset, all success: TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard. The other 26 are success (Auto Label, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, Dogfood Regression Gate 1/3 2/3 3/3, Dogfood Verify CLI, Flag docs affected by code changes, No other open PR may claim the same issue, No other open PR may claim the same single-writer path, Part-of PR must not also close its card, Spec property liveness, Test Core 1/6 through 6/6, The card this PR closes must claim this branch, Type Check consumer gates, Type Check debt ledger, Type Check source gates, Type Check workspace, filter) and 2 are skipped (Console Pin Gate, Packed-tarball smoke (opt-in)). Vercel status success. mergeable true, mergeable_state clean. Implemented-by: VERDICT: PASS |
…recision-retire dropped-refinements.baseline.json: header-only conflict with #20205's three automation entries; totals recounted from the merged body (210 schemas, 591 sites). Claude-Session: https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN Co-authored-by: Claude <noreply@anthropic.com>
builtin-node-config.mdx changed on both sides (main's #20205 edited the module; this branch changed its description emission) and os-regen deferred it; regenerated with gen:docs from the merged tree. Claude-Session: https://claude.ai/code/session_01RCEEP3Z95jrpXCeF3it3Y2 Co-authored-by: Claude <noreply@anthropic.com>
…ites and derived reference descriptions (objectstack-ai#20258) Fixes objectstack-ai#12238 Clause-②: no **Draft for the maintainer's voice review — do not merge or ready it.** One card, both halves, per the rulings recorded on the card (comment `5856895911`): 「一张卡全做」 (authored pages AND the generator), 「只改超范围的 52 页」 (only the out-of-range authored pages), 「不加门禁」 (no new `check:*` gate). ## The rule A page's frontmatter `description` is the one line a search result shows under its title. - **Length:** 70–160 characters (under 70 the engine discards it and writes its own snippet; over 160 it is cut mid-sentence). Rewritten authored lines aim at **120–155**. - **Content:** what the page lets you do, in the words a reader would search, ending on why to click. - **Form:** a sentence, not a truncated title. ## Population, before → after Measured on this branch's tree (`4b7d63cc`) against its base `e0f17a3`; `releases/**` is release-owned and out of scope. | group | pages | median before | median after | under 70 | 70–160 | over 160 | |---|--:|--:|--:|---|---|---| | authored (`content/docs/**` minus `references/**`, `releases/**`) | 181 | 117 | 129 | 15 → **0** | 129 → **181** | 37 → **0** | | generated `references/**` | 211 | 28 | 138 | 210 → **0** | 1 → **211** | 0 → **0** | Every one of the 392 pages' frontmatter parses as YAML (js-yaml) with a string `description` in range. **No exceptions remain.** ## Authored half — 52 rows Only the `description:` line changes (`git diff` over the 52 files: 52 insertions, 52 deletions, zero other lines). `title:` / `navTitle:` lines, the 129 in-range pages, `releases/**`, `apps/docs/**` and `meta.json` are untouched. Over-long lines were trimmed toward their original meaning rather than rewritten. Rows 1–15 were under 70; rows 16–52 were over 160. | # | page | before | len | after | len | |--:|---|---|--:|---|--:| | 1 | `concepts/north-star.mdx` | ObjectStack's current product and architecture direction. | 57 | Where ObjectStack is headed: a metadata-native backend that humans and AI agents can both operate safely — its principles, runtime shape and non-goals. | 151 | | 2 | `data-modeling/drivers.mdx` | Configuration reference for supported database drivers | 54 | Connect ObjectStack to PostgreSQL, MySQL, SQLite, MongoDB, Turso or memory — URL-based driver inference, per-driver config keys and dialect caveats. | 148 | | 3 | `getting-started/quick-reference.mdx` | Fast lookup table for all ObjectStack protocols | 47 | Look up any ObjectStack metadata type fast — the key schemas of every protocol category on one page, with common patterns and search tips. | 138 | | 4 | `index.mdx` | Technical documentation for ObjectStack. | 40 | ObjectStack documentation: build business apps from typed metadata that AI agents write and humans verify — start here for guides, concepts and APIs. | 149 | | 5 | `kernel/architecture.mdx` | Deep dive into the ObjectKernel architecture | 44 | How the ObjectKernel boots and runs: its core architecture, the plugin bootstrap lifecycle, and the public kernel API you call from a plugin. | 141 | | 6 | `kernel/events.mdx` | System-wide event bus for loose coupling between plugins | 56 | Every kernel lifecycle hook and data lifecycle hook in ObjectStack — when each fires, its payload and ordering, and which of the two systems to pick. | 149 | | 7 | `kernel/runtime-services/email-service.mdx` | Outbound email delivery and template rendering APIs. | 52 | Send email from a plugin or flow with services.email — send() and sendTemplate(), the result they return, and the typed error codes to handle. | 142 | | 8 | `kernel/runtime-services/queue-service.mdx` | Async queue publish/subscribe and DLQ operations. | 49 | Publish and subscribe to async job queues with services.queue — queue size, purge, and dead-letter listing, replay and cleanup for failed messages. | 147 | | 9 | `kernel/runtime-services/sharing-service.mdx` | Record-level sharing and editability checks. | 44 | Check and manage record-level sharing with services.sharing — read filters, canEdit and canDelete checks, and granting or revoking record shares. | 145 | | 10 | `kernel/services.mdx` | Dependency Injection mechanism for loose coupling between plugins | 65 | How plugins expose and consume services in ObjectStack — register a service, resolve it by name, see the standard services, and replace a core one. | 147 | | 11 | `protocol/index.mdx` | The formal ObjectStack protocol for Data, UI, and System layers | 63 | The ObjectStack protocol specification: how the Data, UI and System layers describe a complete business application as metadata, and its design principles. | 155 | | 12 | `protocol/objectui/actions.mdx` | Buttons, triggers, navigation, and user interaction definitions | 63 | The ObjectUI action protocol: declare buttons, menu items and triggers as metadata that run business logic, navigate, or call external integrations. | 148 | | 13 | `protocol/objectui/concept.mdx` | The philosophy and architecture of metadata-driven user interfaces | 66 | Why ObjectUI defines interfaces as data, not code: forms, dashboards and reports as declarative metadata that any renderer can draw consistently. | 145 | | 14 | `protocol/objectui/index.mdx` | Server-driven UI protocol - Define interfaces as data, not code | 63 | ObjectUI, the server-driven UI protocol: define layouts, forms and dashboards as JSON or YAML metadata that renderers turn into working interfaces. | 147 | | 15 | `protocol/objectui/widget-contract.mdx` | Standard props, events, and lifecycle for ObjectUI components | 61 | Build a custom ObjectUI field widget: the standard props, events and lifecycle it implements, how to register it, and accessibility and theme rules. | 148 | | 16 | `api/declarative-endpoints.mdx` | Expose your app to systems outside the platform by declaring an apis: endpoint as metadata — which channel to pick, the four publish gates, and the obligation that comes with an anonymous endpoint. | 197 | Expose your app to outside systems by declaring an apis: endpoint as metadata — pick the channel, pass the four publish gates, secure anonymous calls. | 150 | | 17 | `api/plugin-endpoints.mdx` | REST endpoints that become available when the corresponding plugin is installed — auth, workflow, automation, views, realtime, notifications, AI, i18n, and file storage. | 169 | REST endpoints each installed plugin adds — auth, workflow, automation, views, realtime, notifications, AI, i18n and files — and how to discover them. | 150 | | 18 | `automation/approvals.mdx` | Route a record for sign-off — who can configure the automation, and the run-identity decision that keeps an approval flow from quietly bypassing row-level security. | 164 | Route a record for sign-off with an approval flow — who may configure it, and the run-identity choice that keeps it from bypassing row-level security. | 150 | | 19 | `automation/connectors.mdx` | Call external systems from flows — plugin-registered connectors, and declarative provider-bound instances (rest / openapi / mcp) authored as pure metadata with reference-based credentials. | 188 | Call external systems from flows with plugin connectors or declarative rest, openapi and mcp instances — pure metadata with reference-based credentials. | 152 | | 20 | `capabilities/index.mdx` | The platform's capabilities in business language — data, views, automation, approvals, permissions, analytics, AI, integrations — each with a real CRM as the running example | 173 | What the platform can do, in business language — data, views, automation, approvals, permissions, analytics, AI and integrations, shown on a real CRM. | 150 | | 21 | `concepts/metadata-lifecycle.mdx` | How metadata flows through Repository → Change Log → Cache → Registry — the canonical event stream that powers Studio HMR, REST writes, and future cloud editing. | 161 | How metadata flows through Repository, Change Log, Cache and Registry — the event stream behind Studio hot reload, REST writes and cloud editing. | 145 | | 22 | `deployment/index.mdx` | Two things deploy on this platform and they move on separate clocks — the platform runtime you operate, and the metadata app you build. Which one you are doing decides which pages in this section are yours. | 206 | Deploy the platform runtime or ship your metadata app — two things on separate clocks. Find out which one you are doing and which pages cover it. | 145 | | 23 | `deployment/publish-and-preview.mdx` | A metadata app is versioned in your catalog while the platform moves on its own release train. Compile the app into an artifact, then pick how it reaches a running platform — installed from the catalog, or pinned as the runtime's boot artifact. | 244 | Compile a metadata app into a versioned artifact, then install it from the catalog or pin it as the runtime's boot artifact to preview and publish. | 147 | | 24 | `deployment/seed-tenancy-repair.mdx` | The automatic repair that stamps organization_id on untenanted seed rows and merges the __global__ autonumber counter — when it runs, what it changes, what it deliberately leaves alone, and the manual remedy for a multi-organization install. | 241 | The automatic repair that stamps organization_id on untenanted seed rows — when it runs, what it changes, and the manual fix for multi-org installs. | 148 | | 25 | `deployment/self-hosting.mdx` | Run a compiled ObjectStack app on your own infrastructure with the official Docker image — plus Compose with Postgres, Kubernetes, and the bare Node.js fallback, including health checks, reverse-proxy wiring, and the secrets you must pin. | 238 | Run a compiled ObjectStack app on your own servers with the official Docker image, Compose and Postgres, or Kubernetes — health checks and secrets too. | 151 | | 26 | `deployment/tenancy-modes.mdx` | The three tenancy postures (single / group / isolated), how OS_TENANCY_POSTURE resolves, the membership policy for new users, and the degraded-tenancy boot guard. | 162 | Choose single, group or isolated tenancy: how OS_TENANCY_POSTURE resolves, the membership policy for new users, and the degraded-tenancy boot guard. | 148 | | 27 | `getting-started/build-with-claude-code.mdx` | The core ObjectStack workflow — Claude Code authors the metadata, you verify in the visual Console, guardrails catch mistakes, and the app you build is itself AI-operable over MCP. | 180 | Build an ObjectStack app with Claude Code: the AI writes the metadata, you verify it in the Console, guardrails catch mistakes, and the app speaks MCP. | 151 | | 28 | `getting-started/how-ai-development-works.mdx` | The division of labor behind ObjectStack — AI authors the typed metadata, you verify in the visual UI, and layered guardrails keep the AI from shipping mistakes. | 161 | How building with ObjectStack splits the work: AI writes the typed metadata, you verify it in the visual UI, and layered guardrails stop its mistakes. | 150 | | 29 | `getting-started/your-first-project.mdx` | Scaffold a standalone ObjectStack project with npm, understand what was generated, extend the data model, call the REST API, and build a deployable artifact — no monorepo checkout, no AI agent required. | 202 | Scaffold a standalone ObjectStack project with npm, extend the data model, call the REST API and build a deployable artifact — no AI agent required. | 148 | | 30 | `permissions/access-recipes.mdx` | Map a concrete access requirement onto the platform's layers — object CRUD, field-level security, row-level security, capabilities, app/nav gating, and the run-identity of automations. | 184 | Map a real access requirement onto the right layer — object CRUD, field-level and row-level security, capabilities, navigation gating and automations. | 150 | | 31 | `permissions/administrator-guide.mdx` | The task-first operations manual for customer system administrators — onboard a tenant in four steps (build the org tree, add people, assign positions, verify), with the 90% rule — daily administration is assigning positions; the capability plumbing ships built-in. | 265 | The permissions manual for tenant administrators: onboard a tenant in four steps, then run daily access by assigning positions — no plumbing needed. | 148 | | 32 | `permissions/attachments-access.mdx` | How access to record attachments is decided — the parent-derived read/create/delete model, authenticated downloads, the enable.files opt-in gate, and the storage-byte lifecycle. Covers sys_attachment and sys_file. | 213 | Who can read, upload or delete a record's attachments: the parent-derived access model, authenticated downloads, the enable.files gate and file cleanup. | 152 | | 33 | `permissions/authorization.mdx` | The one-page map of ObjectStack authorization — the enforcement chain, combination semantics, package provenance, lifecycle coverage, and the CI governance — with its limits — behind "declared" equals "enforced". Stitches ADR-0049/0054/0056/0057/0066/0068/0069/0078/0086 into a single narrative. | 295 | One-page map of ObjectStack authorization — the enforcement chain, how grants combine, package provenance, lifecycle coverage and the CI checks behind it. | 154 | | 34 | `permissions/capabilities.mdx` | How a package DEFINES an authorization capability with defineCapability — the declaration half of ADR-0066 D1 — and how that name travels from source to the sys_capability catalogue to a permission-set grant to a requiredPermissions check. | 239 | Define an authorization capability in a package with defineCapability, then follow it into the catalogue, a permission-set grant and a runtime check. | 149 | | 35 | `permissions/delegated-administration.mdx` | Administration itself as a scoped grant — a business-unit subtree, an action set, and an assignable-set allowlist, with self-escalation structurally impossible (ADR-0090 D12). | 175 | Hand out scoped admin rights: a business-unit subtree, an action set and an assignable-set allowlist, with self-escalation structurally impossible. | 147 | | 36 | `permissions/index.mdx` | Authentication, authorization, and record- and field-level access control in ObjectStack — a cross-protocol capability enforced by the ObjectStack runtime and declared as ObjectQL security metadata. | 198 | Authentication, authorization, and record- and field-level access control in ObjectStack — declared as security metadata and enforced by the runtime. | 149 | | 37 | `permissions/permission-sets.mdx` | The only capability container — object CRUD + FLS + scope depth + capabilities, union-merged across everything a user holds. Covers the built-in sets, assignment tables, access depth, the isDefault suggestion, and delegated-admin scopes. | 237 | Permission sets are the only capability container: object CRUD, field security, scope depth and capabilities, union-merged across what a user holds. | 148 | | 38 | `permissions/permissions-matrix.mdx` | Visual reference for ObjectStack's security model — permission types, the object × permission-set matrix, field-level security, sharing rules, business-unit depth, and the access-matrix snapshot gate | 199 | Visual reference for ObjectStack security — permission types, the object × permission-set matrix, field security, sharing rules and business-unit depth. | 152 | | 39 | `permissions/positions.mdx` | Positions (岗位) are flat capability-distribution groups — users hold positions, positions bind permission sets. The visibility hierarchy lives on business units, never here. Includes the built-in identity positions and the everyone/guest audience anchors. | 254 | Positions are flat groups that hand permission sets to users — the built-in identity positions, the everyone and guest anchors, and where hierarchy lives. | 154 | | 40 | `permissions/profiles.mdx` | The Profile concept was removed by ADR-0090 D2. Baseline access is now authored with the everyone audience anchor, package isDefault suggestions, and ordinary permission sets distributed via positions. | 201 | Profiles were removed. Author baseline access with the everyone audience anchor, package isDefault suggestions and permission sets given via positions. | 151 | | 41 | `permissions/record-view-auditing.mdx` | Who viewed this record, and when — the `read` action in sys_audit_log: its per-object opt-in, the four edges of its scope, and what a view row deliberately does not carry. | 171 | Find out who viewed a record and when: the read action in sys_audit_log, its per-object opt-in, the edges of its scope and what a view row leaves out. | 150 | | 42 | `permissions/sharing-rules.mdx` | Record-level access: the organization-wide default (OWD) baseline per object, the external sharing dial, criteria sharing rules and recipient types, and the RLS-safe analytics read scope. | 187 | Record-level access in ObjectStack: organization-wide defaults per object, the external sharing dial, criteria sharing rules and RLS-safe analytics reads. | 154 | | 43 | `permissions/system-context.mdx` | The authoritative table of every platform behaviour keyed off `ExecutionContext.isSystem` — what an elevated write gets, what it loses, and what the flag deliberately does NOT do. Built by census over the whole repo, not by recall. | 231 | Every platform behaviour that ExecutionContext.isSystem changes — what an elevated write gets, what it loses, and what the flag deliberately does not do. | 153 | | 44 | `permissions/tenant-audit-census.mdx` | The authoritative enumeration of every application-surface write call site against a tenancy-enabled object — how many thread an execution context, how many thread none, and how much of the population a static instrument can decide at all. Built by census over the whole repo, not by recall. | 291 | A census of every write call site against a tenancy-enabled object — which pass an execution context, which pass none, and what static checks can decide. | 153 | | 45 | `ui/actions.mdx` | Declarative buttons with server-side behavior — define once, bind to lists, records, and navigation, permission-check on both surfaces, and optionally expose to AI. | 164 | Declare buttons as metadata with server-side behavior — bind them to lists, records and navigation, permission-check both sides, and expose them to AI. | 151 | | 46 | `ui/audience-based-interfaces.mdx` | The same data serves different audiences. Give end users a curated app/page and keep builder surfaces — Studio, raw object tables, automation config — out of their view. Separate consumer and builder paths by default. | 217 | Serve one dataset to different audiences: give end users a curated app and keep builder surfaces like Studio and raw tables out of their view by default. | 153 | | 47 | `ui/create-vs-edit-form.mdx` | The new-record form asks 5 fields; the full edit form shows 40 grouped into sections. Derive both from one flat field set; only hand-shape the create form when layout or flow genuinely diverges. | 194 | Derive a short create form and a full sectioned edit form from one flat field set, and hand-shape the create form only when its layout truly differs. | 149 | | 48 | `ui/field-grouping-and-order.mdx` | The data model is a flat field set, but forms need sections. Where grouping actually lives — semantic field.group vs form sections vs a table's row grouping — and why those three "groups" are different things. | 209 | Where form sections really come from: field.group versus form sections versus table row grouping — three different groups over one flat field set. | 146 | | 49 | `ui/forms.mdx` | Render any FormView either publicly (anonymous, /f/:slug) or internally (authed operators, /forms/:name). The same metadata drives both, with URL prefill, configurable post-submit behavior, and declarative open-form actions. | 224 | Render any FormView publicly at /f/:slug or internally at /forms/:name from one metadata source, with URL prefill, post-submit behavior and form actions. | 153 | | 50 | `ui/public-data-collection.mdx` | Expose one form to anonymous visitors (web-to-lead, contact-us, intake) without opening the underlying base. Authorization is derived from the form's own declaration; only whitelisted fields are accepted. | 204 | Collect web-to-lead, contact and intake submissions from anonymous visitors through one public form, without opening the underlying data to guests. | 147 | | 51 | `ui/react-pages.mdx` | Author a page body as real React (kind:'react') or as constrained JSX that is parsed and never executed (kind:'html') — the two source-authoring tiers, and how to choose | 169 | Write a page body as real React or as constrained JSX that is parsed but never executed — the two source-authoring tiers and how to choose between them. | 152 | | 52 | `upgrading.mdx` | ObjectStack upgrades come in two halves that run on separate clocks — the platform runtime and your metadata app. Which one you are doing, what each one moves, and where the per-major checklists live. | 200 | Upgrade ObjectStack in two halves on separate clocks — the platform runtime and your metadata app. What each moves, and where each major's checklist lives. | 155 | ## Generator half — `packages/spec/scripts/build-docs.ts` **Before:** every module page was written `description: TITLE protocol schemas`, every category overview `description: Complete reference for all TITLE schemas` — 210 of 211 generated pages under 70. **Derivation** (new `packages/spec/scripts/lib/page-description.ts`, pinned by `scripts/page-description.test.ts`, 18 cases). No `packages/spec/src/**` file is edited: the rule reads the module's leading doc block through the existing `findModuleDocBlock` — the same block `renderFileDescription` already renders as the page's opening. 1. **Lead** — the doc block's prose paragraphs before its first list, table, fence or tag, after an optional title line. Markdown and `{@link}` are flattened to text; citation-only parentheticals (`(#NNN)`, `[ADR-NNNN …]`), bare URLs, one-word run-in labels and the `Implements P0 requirement …` boilerplate are dropped; a sentence that only introduced a list keeps its clause before the last comma or dash. 2. **Fit** — whole sentences while the total stays ≤ 160; later paragraphs join only while it is still under 70; then the title line is prefixed if that fits. A single sentence over 160 is cut at the longest clause boundary that keeps ≥ 70 characters and leaves no bracket open — only when none exists, at a word boundary with an ellipsis. 3. **Complete** — a lead still under 70 is followed by the page's schema names: `… Reference for A, B and N more: every property with its type and default.` 4. **Fallback** — a module with no doc block gets `TITLE schemas of the ObjectStack CATEGORY: A, B and N more — each property with its type, default and a TypeScript example.` Names are listed while they fit, the rest counted. 5. **Category overviews** — `The ObjectStack CATEGORY in N reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example.` The value is emitted double-quoted (`JSON.stringify`, a subset of YAML's double-quoted style), because doc-block prose carries `: ` and quotes. Everything is a pure function of the source, so `check:docs` stays a plain regenerate-and-compare. Each `gen:docs` run prints one tally line. **Which rule wrote the 196 module pages:** 115 from the doc block alone, 19 doc block + schema names, **62 on the schema-name fallback** (their module has no module-level doc block — e.g. `data/object`, `api/contract`, `kernel/plugin`). Plus 14 category overviews from rule 5. Two doc-block pages end on the word-boundary ellipsis (`data/validation`, `marketplace/package`): their opening sentence has no clause boundary that keeps ≥ 70 characters. They are in range. ### Sample — 10 regenerated pages | page | before | after | len | rule | |---|---|---|--:|---| | `references/data/object.mdx` | Object protocol schemas | Object schemas of the ObjectStack Data Protocol: ApiMethod, ApiOperation, Index and 13 more — each property with its type, default and a TypeScript example. | 156 | schema names | | `references/ui/view.mdx` | View protocol schemas | View protocol schemas — the view metadata type and its three persisted body spellings. | 86 | doc block | | `references/api/dispatcher.mdx` | Dispatcher protocol schemas | Defines how the ObjectStack HttpDispatcher routes incoming API requests to the correct kernel service based on URL prefix matching. | 131 | doc block | | `references/automation/control-flow.mdx` | Control Flow protocol schemas | Structured control-flow constructs — the native + AI-authored flow model: a loop container, a parallel block, and structured try/catch/retry. | 141 | doc block | | `references/kernel/cluster.mdx` | Cluster protocol schemas | Defines the runtime semantics required for ObjectStack to behave correctly when more than one Node.js process is involved. | 122 | doc block | | `references/security/rls.mdx` | Rls protocol schemas | Implements fine-grained record-level access control inspired by PostgreSQL RLS and Salesforce Criteria-Based Sharing Rules. | 123 | doc block | | `references/api/error-code-ledger.mdx` | Error Code Ledger protocol schemas | Error-Code Ledger. Reference for ErrorCode, ProvenanceWaiver, StandardSynonymWaiver: every property with its type and default. | 126 | doc block + schema names | | `references/system/object-storage.mdx` | Object Storage protocol schemas | Object Storage Protocol. Reference for AccessControlConfig, BucketConfig, FileMetadata, LifecycleAction and 11 more: every property with its type and default. | 158 | doc block + schema names | | `references/kernel/plugin.mdx` | Plugin protocol schemas | Plugin schemas of the ObjectStack Kernel Protocol: Plugin — each property with its type, default and a TypeScript example. | 122 | schema names | | `references/data/index.mdx` | Complete reference for all data protocol schemas | The ObjectStack Data Protocol in 29 reference pages: every schema in @objectstack/spec with its properties, types, defaults and a TypeScript example. | 149 | category index | ### Exact hunks in `build-docs.ts` (for objectstack-ai#15403's rebase) objectstack-ai#15403 remains open; it will edit the **title** emission. This PR does not touch the title line (`md += \`title: ${zodTitle}\n\``, base line 478) or anything else in the title path. Hunks, in base-file line numbers: - `@@ -64,0 +65,6` — import of `lib/page-description`. - `@@ -457,0 +464,3` — the `descriptionSources` tally, after `PAGE_SECTION_LEVEL`. - `@@ -465 +474,2`, `@@ -470 +480` — the module source is read once into `source` and handed to `renderFileDescription` (behaviour unchanged). - `@@ -476,0 +487,9` — the `modulePageDescription(...)` call, just above the frontmatter. - `@@ -479 +498` — **the one description line** of module pages. - `@@ -912 +931` — the one description line of category overviews. - `@@ -1102,0 +1122,8` — the tally's `console.log`, before `flush`. `content/docs/references/**` is regenerated by `gen:docs`, never hand-edited: against `origin/main`, the only changed lines under it are 210 `description:` lines (the root `references/index.mdx` was already in range and is unchanged). ## Acceptance - [x] every `content/docs/**` description outside `releases/**` is 70–160 characters — 392 of 392, no exceptions - [x] ~~a `check:*` gate enforces the range~~ — dropped by the ruling 「不加门禁」 - [x] descriptions read as sentences, not as truncated titles (authored: hand-written; generated: sentence-fitted from the doc block, two ellipsis cuts named above) - [x] the rule and the full before/after table are in this body; the PR stays **draft** for the maintainer's voice review ## Changeset `skip-changeset`: nothing here ships. `packages/spec` publishes `files: [dist, json-schema, liveness, prompts, llms.txt, README.md, src/**/*.zod.ts, CHANGELOG.md, api-surface, spec-changes.json]`; `scripts/**` is not in it. Measured after a real build: `modulePageDescription` / `categoryIndexDescription` have 0 hits across every shipped path, while the positive control `ObjectSchema` has 56 hits in `dist`. `content/docs/**` belongs to no package. ## Verification (on `4b7d63cc`, after merging `origin/main` via `os-regen-merge.sh`) - `pnpm --filter @objectstack/spec build`, then `check:generated` — every gate ✓, including `check:docs` ("226 generated files in sync"). - `dispatch-gates --commands` derived 94 families. `--ran` with recorded exit codes: **94 derived, 92 run, 2 NOT-MEASURED, 0 UNRUN**. All 92 exit 0. - NOT MEASURED: `check:dual-build-cjs-loads` and `check:type-check-debt`'s `--re-measure`. Both exit 3 because they need the full `packages/*` build closure. This diff changes no package source they read; CI builds that closure. - `pnpm --filter @objectstack/spec typecheck` (tsc + `check:scripts-typecheck` + `check:test-typecheck`) — exit 0. - vitest: `scripts/page-description.test.ts` 18/18; neighbours `file-description`, `references-banner`, `category-title`, `root-index`, `category-index`, `schema-section` — 3 files (local) + 4 files (repo), 68 + 146 tests pass. - Merge: `main`'s objectstack-ai#20205 changed `automation/builtin-node-config` on both sides. It was regenerated from the merged tree in its own commit (`4b7d63cc`); its body is byte-identical to `origin/main` apart from the description line. ## Acceptance notes - 62 spec modules carry no module-level doc block, so their pages use the schema-name fallback. Writing those doc blocks is a `packages/spec/src/**` edit and was out of this card's surface on purpose (clause-② path limb). The tally line printed by `gen:docs` is the running count. - Some doc-block leads read as internal notes rather than reader copy (e.g. `kernel/metadata-protection`: "Phase 1 introduces the item-level lock …"). The rule reproduces the source's own first sentence faithfully; improving those needs a `src` docblock edit, not a generator change. Size: 265 files, +801 / −266 = 1,067 changed lines, generated files included — under the 5,000-line threshold. Seat `domain:devx#2`, dispatched by `session_018mA64scZ8fmpiPkrHVAwXj`; implemented in `session_01RCEEP3Z95jrpXCeF3it3Y2`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- _Generated by [Claude Code](https://claude.ai/code/session_01RCEEP3Z95jrpXCeF3it3Y2)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
…laces are its currency's (ADR-0049) (objectstack-ai#20251) Part of objectstack-ai#19992 Clause-②: no Retires `currencyConfig.precision` under ADR-0049 enforce-or-remove, in the direction triage set (`5853208433`: 「**Direction unchanged:** remove, per ADR-0049 with the ADR-0087 retirement entry, under ruling 乙's principle that a currency's decimal places are the currency's. The aliases go with the key.」) and ruling 乙 on objectstack-ai#19910 (`5805782503`: 「a currency's decimal places are the currency's, not a setting」). objectstack-ai#19992 remains open for its folded family site (`5854612946`): the FIELD-level `precision` ("Total digits") that nothing reads. That key is untouched here apart from its form help text; objectui's Studio inspector writes it, so retire-versus-enforce is still an open choice for that card. ## What changes for an author | before (17.4) | after | | --- | --- | | `currencyConfig: { precision: 2, currencyMode: 'fixed', defaultCurrency: 'USD' }` parses | refused at `currencyConfig` as `unrecognized_keys`, with the prescription below | | `currencyConfig.decimals` / `currencyConfig.scale` refused with *Did you mean → `precision`* | refused with the reason, and no rename suggested | | `CurrencyConfigSchema.parse({})` → `{"precision":2,"currencyMode":"dynamic","defaultCurrency":"CNY"}` | → `{"currencyMode":"dynamic","defaultCurrency":"CNY"}` | | authored `precision` contradicting a fixed currency's ISO 4217 digits → `custom` issue at `currencyConfig.precision` | the check is gone with the key | | field designer help text for the field-level `precision`: "Decimal places (e.g., 2 for $10.50)" | "Total digits" (the key's describe; the object designer's row already said so) | ### Refusal texts, verbatim (rendered from `src` at this head) `precision`: ```text Unrecognized key(s) on this currency configuration: `precision`. • `currencyConfig.precision` was removed in @objectstack/spec 17.5.0 (ADR-0049 enforce-or-remove) — no renderer or runtime ever read it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; apply them by hand. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute. ``` `decimals` (and `scale`, identical but for the key name): ```text Unrecognized key(s) on this currency configuration: `decimals`. • `currencyConfig.decimals` is not a currency configuration key, and nothing replaces it: a currency amount's decimal places are its currency's ISO 4217 minor unit (2 for USD, 0 for JPY, 3 for KWD), which every display face derives from the currency itself, so there is no decimal-places setting to declare. Do not move the number to the field-level `precision`: that key is the amount's TOTAL digit count (a DECIMAL(18,2) amount declares `precision: 18`), not its decimal places. Delete the key. Until this shape was closed these were dropped silently — the field was still created, minus whatever the key was meant to constrain, protect or compute. ``` The trailing sentence is the schema's existing `history` (`FIELD_HISTORY`), appended by `strictObject`; it is not new text. ## Premises measured (Zone 2) 1. **No reader, anywhere** — held. Instrument: `git grep currencyConfig` (non-test) plus indirect spellings (a config held in a variable, then `.precision`), with a lit control. - objectstack `packages/**` + `examples/**` + `apps/**` at `4df101c3`: 0 readers of `precision`; the control `currencyConfig?.currencyMode` IS read (`service-analytics/src/plugin.ts:1101`). - objectui at the pin `f8a9d0fb` and at `main` `25c7d584e`: 0 readers. `CurrencyField.tsx:82` derives width as `currencyFractionDigits(currency)` on both; its `:74` comment says it never read the key. Control: `currencyMode` / `defaultCurrency` reads 7 (pin) / 10 (main). - cloud `main` `48d7066`: 0 `currencyConfig` mentions; the instrument fires on that ref (`defaultCurrency` in `connector-stripe`). 2. **Producers.** Authored: three `examples/app-showcase` objects (`account`, `field-zoo`, `semantic-zoo`), now edited. Studio inspector: none (objectui's own `ObjectFieldInspector.currencyScale-10221.test.tsx` pins that it writes the field-level key, never `currencyConfig.precision`). **Stored rows and built artifacts: nearly all of them** — the removed `.overwrite()` baked `precision: 2` into parse output, so every persisted currency config carries it unwritten. That population decides the route: a D2 conversion replayed at rest, not a bare refusal. - What a stored field experiences after upgrade: the stored-row and artifact seams replay `currency-config-precision-removed` (`includeRetired`), strip the key, and serve the row canonical; it then parses. Pinned end to end through `applyConversionsToStoredItem('object', …)` in `currency-precision-iso4217.test.ts` — the row as stored is refused by today's door, the converted row parses, and the field-level `precision: 10` survives. 3. **Aliases** — the schema is a `strictObject`, and its `aliases` are rejection-with-suggestion, not renames. With the target gone they would point at a refusal (and `alias-integrity` audits that). Precedent for a natural spelling with no landing key is `FieldSchema`'s `currency` guidance, so `decimals` / `scale` get `guidance` entries with the same reason and no command (no conversion strips them: the closed shape always refused them, so no stored row carries them). Pinned: code, path, key, text, and the absence of "Did you mean". 4. **`currencyPrecisionContradiction`** lost its only reader, and `currencyFractionDigits` was read only by it: both are removed (neither was exported from a public entry — no barrel row, no `api-surface/` row). `CURRENCY_FRACTION_DIGITS` stays: `shared/value-domain.zod.ts:160` reads its key set for the `iso_4217_currency` value domain. The module docblock now says so and keeps the CLDR provenance. 5. **Docs** — every published sentence that stated the rule is corrected: `data-modeling/field-types.mdx`, `validation-rules.mdx`, `fields.mdx`, `getting-started/common-patterns.mdx` (two `os:check` blocks), `protocol/objectql/types.mdx`, the published skill `skills/objectstack-data/rules/field-types.md`, and the generated references (`references/data/field.mdx`, `object.mdx`, `system/migration.mdx`, `shared/value-domain.mdx`). ## The retirement kit - Schema: `precision` deleted from `CurrencyConfigSchema`; `CURRENCY_CONFIG_DECIMAL_PLACES_GUIDANCE` carries the three prescriptions; the `.superRefine` and `.overwrite` are removed. Stale comments that named the twin (`FieldSchema.precision`, the objectstack-ai#20011 note, the `.overwrite` precedent notes) now say it is gone. - ADR-0087: D2 conversion `currency-config-precision-removed` (toMajor 18, `retiredFromLoadPath`, strips the key under every field's `currencyConfig` on `objects` and `objectExtensions`, 2-notice fixture), wired into step 18's `conversionIds` with the rationale extended; `RETIRED_KEYS_BY_MAJOR[18]` gains `data/CurrencyConfig:precision` (one entry file, regenerated). - Generated / ledgers: `authorable-surface/data.json` row removed deliberately (gate (a) tripwire; gate (c) proof 4 — guidance route — then passes); `authorable-defaults/data.json` loses `CurrencyConfig:precision = 2` (regenerated); `dropped-refinements.baseline.json` loses the 13 sites the removed `.superRefine` produced (`data/CurrencyConfig` itself plus 12 embeddings) — the refinement was removed with its key, the projection did not learn anything. - Form + i18n: `field.form.ts` help text; the `en` bundle regenerated, `zh-CN` / `ja-JP` / `es-ES` set to the object designer row's existing translations (总位数 / 総桁数 / Total de dígitos). - `spec-changes.json` / the upgrade guide do not move: neither carries any protocol-18 entry yet (the precedent `metric-filters-removed` is absent too), and `check:spec-changes` / `check:upgrade-guide` read up to date. - Changeset: `@objectstack/spec` minor + `@objectstack/platform-objects` patch, `**BREAKING**`, FROM → TO, and `adr-0087: registered currency-config-precision-removed` (`check:adr-0087-registration` green). ## Absence half — no tree-scoped text pin, on purpose `precision` does not leave the tree: it stays the field-level total-digit count on every numeric field. What is retired is a key in a position (`fields.NAME.currencyConfig.precision`), which a grep either matches everywhere or, scoped down, only where its author already knew to look. The `dashboard-chart-structure-refusal.test.ts` precedent covers this shape; the rationale block is in the test file. Standing in its place: `tsc` (the key is off `CurrencyConfig`'s input type — Leg A below) and the closed parse door (Leg B below). ## Proofs (one-off, nothing left behind) - **Leg A — reverse verification against the rebuilt `.d.ts`.** `node scripts/ablation-replace.mjs` put `precision: 2` back into `account.object.ts` (anchor 1 → 0, blob `58a9af7e` → `700705a8`), then `tsc --noEmit` on the showcase: exit 1, `account.object.ts(71,25): error TS2353: Object literal may only specify known properties, and 'precision' does not exist in type '{ currencyMode?: …`. Restored: blob == HEAD, `git diff HEAD` empty. Control (committed tree): showcase `typecheck` exit 0. - **Leg B — ablation of the refusal.** Re-declared `precision` on the schema (blob `7d36200c` → `86fdbf2d`), then the two pin files: **9 failed | 279 passed** — every retirement pin went red; the alias pins stayed green (they pin the guidance, which the ablation leaves in place) and so did the parse-output and conversion pins (independent of the key's declaration). Restored: blob == HEAD, `git diff HEAD` empty; tree clean after both legs (0 bytes of `git diff HEAD --stat` + `git status --porcelain`). Direction observed: red, as expected. ## Verification (head `36819398`; patch round on `71ea994d` below) - `pnpm --filter @objectstack/spec build` exit 0; `check:generated` exit 0 (15 of 15 up to date, after the one stale `check:docs` was regenerated with `--fix`). - `pnpm --filter @objectstack/spec typecheck` exit 0 (includes `check:test-typecheck`, so the `@ts-expect-error` in the pin file is a used directive). - `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2`: **546 files passed, 16062 tests passed, 2 todo**. - Consumer suites (contract-face fixture triage): `@objectstack/example-showcase` typecheck exit 0 (after its dependency closure built); `@objectstack/platform-objects` typecheck exit 0 and tests **55 files / 911 passed**; `@objectstack/service-analytics` `currency-mode-relay.test.ts` + `query-dataset.test.ts` **51 passed**; `pnpm check:i18n` exit 0 (9 packages in sync, after the gate's own prerequisite closure). - Gate union: `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived **120** commands on this head; all run, exit codes on disk; `--ran` reconciles **119 run, 1 NOT-MEASURED, 0 UNRUN**. - `check:skill-examples` (covers the two edited `os:check` blocks): first refused on its prerequisite (exit 3, `client-react` unbuilt); after `pnpm --filter '@objectstack/client-react...' build` it exit 0 — "259 prose examples type-check across 3 surface(s)", 109 files. - NOT MEASURED: `check:dual-build-cjs-loads` — reason: its prerequisite is every package's `dist/` (a full `pnpm build`), and this diff changes no package entry or `exports`. - Not run locally: repo-wide `pnpm lint` (CI's run). - Console Pin Gate premise: objectui at the pin `f8a9d0fb` and at `main` imports neither removed helper nor `CurrencyConfigParsed`, and writes no `currencyConfig` literal carrying `precision` (multi-line scan over every file mentioning `currencyConfig`: 0 hits; control `currencyConfig: { … defaultCurrency … }` literals: 5 files at the pin, 13 at `main`). - The showcase `^...` closure build, the i18n prerequisite closure and every heavy run went through `scripts/pm/os-verify-lock.sh`. - Merged `origin/main` (`3cb84d08`) with `scripts/pm/os-regen-merge.sh`: conflicts stacked both sides (objectstack-ai#20217's `defaultCurrency` describe kept beside this removal; objectstack-ai#20085's `view-item-owner-hidden-removed` kept beside the new conversion, in both registries); every sibling entry measured present after the merge, same counts as `origin/main`. - **Patch round (merge conflict with objectstack-ai#20223), final head `71ea994d`.** - Merged `origin/main` twice with `scripts/pm/os-regen-merge.sh`: `0d3ec471` (merge `1e6f29b2`, regeneration `0b2e3964`), then `e4621867` (merge `71ea994d`). No rebase, no force-push. - Both conflicts were in `packages/spec/dropped-refinements.baseline.json`, which is hand-maintained by rule (no `gen:` script; `build-schemas.ts` gates it). Both sides are stacked: objectstack-ai#20223's 12 `inlineColumns.element` sites and `data/InlineGridColumn` root are kept, this PR's 12 `currencyConfig` sites and `data/CurrencyConfig` root stay removed, and objectstack-ai#20205's three automation entries are kept. The header totals were recounted to 210 schemas / 591 sites, and `gen:schema` accepts the ledger (exit 0). - Interaction with objectstack-ai#20223: no contradiction. Its field/column currency-scale refusals name neither `currencyConfig` nor `precision`, and both sides prescribe 「delete the key; decimal places are the currency's ISO 4217 minor unit」. - Readings on `71ea994d` (locked): - spec build exit 0; `check:generated` exit 0 (15/15); spec typecheck exit 0; - touched suites 6 files / 350 passed; full spec `--project local` **549 files / 16138 passed, 2 todo**; - `check-adr-0087-registration`, `check-changeset-no-major` and `check:migration-registry` (265 semantic / 215 retired-key / 199 retired-def) all exit 0. - The derived gate list is byte-identical to the one reconciled at `36819398`. - CI on `71ea994d`: 35 check runs, 33 success, 2 skipped, 0 failing. ## Scope notes for the reviewer - **Tier H.** `skills/objectstack-data/rules/field-types.md` is on this diff because it taught the retired key in its field-type table and code sample; leaving it would ship a published skill that teaches a refused key. The edit is a pure removal: file 328 → 327 lines; `objectstack-data` markdown 3717 → 3716; all `SKILL.md` files 4402 → 4402. It makes this PR Tier H. - Outside the claim's listed surface, each because this change would otherwise leave it false: the `value-domain.zod.ts` docblock (cited the removed function), one comment in `conversions/registry.ts` (named the removed key's bounds), the three showcase objects, `fields.mdx` / `common-patterns.mdx` / `types.mdx`, the platform-objects translation bundles, and the generated rows above. - `packages/spec/liveness/field.json` is **not** edited. The `currencyConfig` row covers the container, asserts nothing about `precision`, and stays `live` (`defaultCurrency` is read). The container is in `undrilled-containers.baseline.json`, so no per-key row existed to remove. objectstack-ai#20217 had just re-cited that row. ## Acceptance notes - `docs/qa/platform-checklist/areas/records-forms.json` item `records-forms.field-type-constraints` still describes `f_currency` as `scale 2 currencyConfig{precision 2}`. Both halves are stale now: the `scale` half since an earlier card retired it from currency, the `precision` half since this one. Its SCALE gap probe targets a key the currency type refuses. Internal QA prose, not published. carrier: the next checklist-author sweep; 承接者:无. - Standalone `field` metadata rows are outside every field conversion's reach (the stored-row seam has no `fields` stack collection), this one included, the same as its precedents. Observation only; no producer of such rows was measured. - Two semantic ledger entries of earlier protocol-18 migrations (`18.field-scale-precision-integer-refused`, `18.ui-form-field-precision-scale-integer-refused`) say `CurrencyConfigSchema.precision` "is a different surface". That is a scope remark about those migrations, historically accurate, and left as recorded. ## 维护者速读(草稿) **改了什么**:货币字段配置里的 `currencyConfig.precision`(「小数位」)被删除。现在写这个键会在保存/发布时被拒绝,并提示直接删掉;它的两个近义写法 `decimals`、`scale` 也给出同样的说明。已经存进数据库或打进构建产物里的旧配置,读取时会自动去掉这个键,不会报错。字段设计器里「精度」一栏的提示文字从「小数位数」改成「总位数」。 **为什么改**:这个键从来没有任何界面或运行时读取过——金额显示几位小数一直由币种本身决定(美元 2 位、日元 0 位、科威特第纳尔 3 位)。作者(包括 AI)以为设了小数位,实际什么都没发生。裁定乙已定原则「币种的小数位属于币种,不是一个设置项」,分诊定了删除方向。 **风险与代价(含回滚)**:这是破坏性收窄:仍在源码里写这个键的应用,升级后会在发布时报错,需要删掉这个键(`os migrate meta --from 17` 会列出要改的地方)。已存数据不受影响(读取时自动转换)。回滚:revert 本 PR 即可,已存数据没有被改写。本 PR 改到了对外发布的技能包 `skills/`,因此属于 Tier H,需要您批准才能合并。 **席位意见**: **你要做的**:审阅后在本 PR 上给出批准(APPROVED review),或告诉席位把 `skills/` 的改动拆成单独的 PR。 --- _Generated by [Claude Code](https://claude.ai/code/session_01Rjy9MeetSfq34PKn81CRiN)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #19938
Fixes #11182
Clause-②: yes
One branch, one PR, spec commit first, one merge: maintainer ruling B on #19938 (record
5816929495, 「19938 同意」), which folds the #11182 engine half into this change. #11182 ruling D (5805777944, 「11182 D 其他同意」) governs the content.What changes
A value in a
create_recordorupdate_recordnode'sfieldsmap may now be a CEL value envelope,{ dialect: 'cel', source: '…' }, under the same shape and dialect rules theassignmentnode'sassignmentsmap already has. The slot is declared in the expression ledger and evaluated by the executor in the same merge, so it is never declared without being evaluated. A plain string infields.*is still a{token}template and means what it meant in 17.x. No spelling changes meaning (ruling D).Commit 1 (
de7c28942): spec, the contract half (#19938)flow-node-expression-paths.ts): two newvaluerows,create_record.fields.*andupdate_record.fields.*. The census grows from five rows to seven. The prose that called the assignment map the only value slot is corrected, and so is the shippedpredicateSlotRefusalsentence that named it as the onlyvalue-role spelling.builtin-node-config.zod.ts):CreateRecordConfigSchema/UpdateRecordConfigSchemafieldsvalues takeFlowValueSlotSchema. One factory builds every value slot's contract, so the shape rule is stated once. It is declared above the CRUD schemas:OS_EAGER_SCHEMAS=1runs every lazy factory at module load, and a schema declared further down would be in its temporal dead zone there.VALUE_ENVELOPE_REFUSALreads "A value carrying adialectkey is read as an expression envelope, and this one is not a valid CEL value envelope." The publishedASSIGNMENT_VALUE_ENVELOPE_REFUSALis kept and is the same string, andAssignmentExpressionValueSchema's dialect message is neutral too. A refused field value is no longer told it is "an assignment value".LEDGER_DECLARED_NODE_CONFIG_SCHEMAScarries both CRUD contracts. Their descriptors publishfieldsasadditionalProperties: true, exactly likeassignment, so the marker rides the spec Zod.resolveFlowNodeValueSlots: every authored value of a value slot, strings included, located by the ledger's own walk. The lint hint uses it, so lint never re-spells the path shapes.CreateRecordConfig,UpdateRecordConfig,FlowValueSlot) are declared, with the ledger header totals 207 → 210 and 574 → 577, the build's own reading.config-expression-ledger.test.ts.Commit 2 (
8e37b80ff): engine, the executor half (#11182)crud-nodes.ts, theresolveFieldValuesfunction): each top-levelfieldsvalue that is envelope-shaped goes throughAutomationEngine.evaluateValueEnvelope, the call theassignmentexecutor makes (one evaluator, one CEL scope, one notion of malformed). Every other value goes throughinterpolate()exactly as the whole map used to. A malformed envelope, or one that faults on the live values, fails the node and writes nothing.engine.tsvalueEnvelopeRefusalsand lintcheckDeclaredValueread the slot-neutralFlowValueSlotSchema/VALUE_ENVELOPE_REFUSAL. Comments inengine.ts,logic-nodes.ts,node-executor.zod.tsandvalidate-expressions.tsno longer callassignments.*the only value slot.objectstack validatewarns (never errors) when any value slot holds a{…}template expression, meaning arithmetic or a call to one of the six functions, and points it at the envelope. The warning states the one conversion trap:/ 100becomes/ 100.0.template.tsdocblock and the shippedround()arity refusal now prescriberound(x * 100) / 100.0. Measured on this tree: CEL answers1234forround(x * 100) / 100and1234.57for/ 100.0atx = 1234.5678, while the template dialect answers1234.57for both./ 100.0is right in both dialects. The arity pin intemplate-functions.test.tsasserts the new prescription and the measured reason.flows.mdx): value slots, the envelope in the Create Record example, the three refusal doors (the stale "faults at run time, tracked in spec/formula:ExpressionSchemaaccepts anast-only envelope that no engine can evaluate — it validates, it registers, it faults at run time #15430" paragraph is corrected), the dialect table's scale-2 row, and a CEL-envelope row.@objectstack/spec,@objectstack/service-automationand@objectstack/lint, withClause-②: yes (widening)(Q3, the seat's reading). It names what newly passes, what newly refuses, and the nested and literal rule.skills/objectstack-automation/SKILL.md(Tier H) is untouched, per ruling D point 2.The merge of
origin/main(94216fb72) went throughscripts/pm/os-regen-merge.sh. It resolved without conflicts,check:generatedstayed at 15/15 current afterwards, and the branch's delta against main contains only this change.Why the hint covers only template expressions
The hint covers arithmetic and the six functions, where CEL is a strict superset and the only conversion trap is stated in the warning itself. It does not cover:
{var}/{var.path}reference: CEL adds nothing, and an absent key would flip fromundefinedto a fault;NOW()/TODAY(): CEL'snow()/today()are timestamps with nostring(timestamp), and the string form is the v18 carrier's ([v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939) to add;$User.*: the flow's CEL scope binds no user.Hinting any of these would steer authors toward metadata that the runtime honours but that makes the value worse. Census, a heuristic scan of object-literal
fields/assignmentsblocks: the hint fires on 0 flow sites outside tests in objectstack (94216fb72) and on 2 in HotCRM (2f7b232, the twoquote-generation.flow.tsmoney fields #11182 measured).Measured: the 42-row probe grid, base against after
The grid is 7 values × {create_record, update_record} × {number, text, json} columns. The doors are
FlowSchema.safeParse, theos validatepipeline (normalizeStackInput→ unknown-key lints →ObjectStackDefinitionSchema→runAuthoringRules('validate')),registerFlow, and a run over a real ObjectQL with a recording driver.455dcc060) reproduces the prior report exactly. Every envelope passes every door and is written as a literal object: text and JSON columns report success, and the number column is refused by the data engine.price * 2writes42on the number, text and JSON columns).source, non-parsing CEL, atemplatedialect) are refused at validate (error/expression-invalid) and atregisterFlow, located atconfig.fields.FIELDwith the slot-neutral sentence.FlowSchema.parseis unchanged, because node config is an open record.metadata-protocol), not measured before and measured now throughsaveMetaItemover the protocol's stub-engine harness:fields.*envelope goes from SAVED to422 INVALID_METADATAwithexpression-invalidat the node.{round(price * 100) / 100}template saves with the hint as an advisory, in all three value slots.assignmentcontrol rows were refused before and after; only the sentence changed.dialectis now an envelope; the changeset states the rule and the escape (bind it to a variable and write'{thatVariable}'). No flow in this repo or HotCRM writes an envelope-shaped object intofields. The heuristic scan found 0 outside tests; its control leg found 14 envelope-shapedassignmentsvalues in objectstack tests.Reverse verification (one-off, from the committed state)
interpolate()at both sites throughscripts/ablation-replace.mjs: anchor 2 → 0, bloba292a5ac9181→1d6328c8d76d. The executor test went from 26/26 to 8 failed / 18 passed: the six evaluation pins and two run-time-fault pins went red, and the preservation and registration pins stayed green because registration is ledger-driven. The restore was proven: the blob equals HEAD andgit diff HEADis empty.Verification at
94216fb72node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderived 110 commands.--ranreports 110 derived, 110 run, 0 NOT-MEASURED, 0 UNRUN. Three gates first refused with exit 3 (prerequisite not met) and passed after the builds they asked for:check:skill-examples,check:dual-build-cjs-loadsandcheck:type-check-debt, the last one re-run after direct rebuilds because the ablation restores had touched mtimes.@objectstack/spec: 541 files, 15924 tests@objectstack/service-automation: 146 files, 1757 tests@objectstack/lint: 109 files, 4209 tests@objectstack/metadata-protocol: 189 files passed, 3 skipped (2700 tests)typecheckis green for all four.examples/app-showcase/test/predicate-write-bulk-intent.test.ts(17/17, parses withUpdateRecordConfigSchema) andpackages/cli/src/flow-node-undeclared-field-write.integration.test.ts(7/7, drivesregisterCrudNodes).Acceptance notes
registerFlowhas no ADR-0112 envelope. Every expression refusal there throws one aggregated plainErrorwith nocodeorstatus, and this is pre-existing and door-wide. The registration pins therefore assert the located message substance (node, slot, path, sentence). Theos validatepins assert the rule idexpression-invalidand severityerror, and the publish gate answers422/INVALID_METADATA.validate-expressions.test.tsmeta-guard:grammar. It is an import-specifier artefact, not a receiver. The guard's scanRULE_CODE.matchAll(/\b([a-z][\w$]*)\??\.[A-Za-z_$]/g)readsgrammar.jsinside'./flow-template-grammar.js'as a receiver, the same artefact it already excuses asscope,fieldsandguards. The import is already a named import, so the construct the scanner misreads is the module path itself. My new locals were renamed instead of excused: the value-slot loop reuses the excusedfound(the same resolver-result shape), and the token scan uses aRegExp.execloop with no lowercase receiver. What the guard checks for real receivers is unchanged. The engine commit was amended so that this fix sits inside it and each commit is green on its own.template-functions.test.ts: the arity-refusal prescription pin fortemplate.ts;validate-expressions.test.ts: the meta-guard entry above;crud-fields-value-envelope.test.tsandvalidate-expressions.fields-value-slot.test.ts.flow-field-expression-scale.integration.test.ts:23still callsround(x * 100) / 100"the CEL-identical authoring pattern" in a test comment. The oracle it runs is the template dialect, where that is correct. Carrier: [v18] retire the{var}template dialect in flow assignment slots: refuse at registration with per-spelling remedies (the C half of #11182 ruling D, on the v18 train) #19939.dropped-refinements.baseline.json's unpinnedmeasured.refinementSitesThatDidProjectreads 369 while the build measures 409. It was already stale before this change and is left as found.42at base as well. This carries the prior report's observation forward.Generated by Claude Code