Skip to content

feat(spec,automation): create_record / update_record fields.* accept the CEL value envelope — declared and evaluated together - #20205

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-19938-fields-value-slot
Sep 27, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-19938-fields-value-slot

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

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_record or update_record node's fields map may now be a CEL value envelope, { dialect: 'cel', source: '…' }, under the same shape and dialect rules the assignment node's assignments map 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 in fields.* 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)

  • Expression ledger (flow-node-expression-paths.ts): two new value rows, create_record.fields.* and update_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 shipped predicateSlotRefusal sentence that named it as the only value-role spelling.
  • Value contract (builtin-node-config.zod.ts): CreateRecordConfigSchema / UpdateRecordConfigSchema fields values take FlowValueSlotSchema. 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=1 runs every lazy factory at module load, and a schema declared further down would be in its temporal dead zone there.
  • Q2, the seat's reading: the refusal sentence is slot-neutral. VALUE_ENVELOPE_REFUSAL reads "A value carrying a dialect key is read as an expression envelope, and this one is not a valid CEL value envelope." The published ASSIGNMENT_VALUE_ENVELOPE_REFUSAL is kept and is the same string, and AssignmentExpressionValueSchema's dialect message is neutral too. A refused field value is no longer told it is "an assignment value".
  • Ratchet channel: LEDGER_DECLARED_NODE_CONFIG_SCHEMAS carries both CRUD contracts. Their descriptors publish fields as additionalProperties: true, exactly like assignment, 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.
  • Generated artefacts are regenerated, never hand-edited: api-surface, export-origins, declaration-map, json-schema manifest and reference docs. The three new dropped-refinement sites (CreateRecordConfig, UpdateRecordConfig, FlowValueSlot) are declared, with the ledger header totals 207 → 210 and 574 → 577, the build's own reading.
  • Pins: the census, the fields entries, the value-slot resolver, the CRUD value contract (accept, preserve, refuse at the field's path, slot-neutral sentence), and the ratchet pin in config-expression-ledger.test.ts.

Commit 2 (8e37b80ff): engine, the executor half (#11182)

  • Executor (crud-nodes.ts, the resolveFieldValues function): each top-level fields value that is envelope-shaped goes through AutomationEngine.evaluateValueEnvelope, the call the assignment executor makes (one evaluator, one CEL scope, one notion of malformed). Every other value goes through interpolate() exactly as the whole map used to. A malformed envelope, or one that faults on the live values, fails the node and writes nothing.
  • Consumers: engine.ts valueEnvelopeRefusals and lint checkDeclaredValue read the slot-neutral FlowValueSlotSchema / VALUE_ENVELOPE_REFUSAL. Comments in engine.ts, logic-nodes.ts, node-executor.zod.ts and validate-expressions.ts no longer call assignments.* the only value slot.
  • Author-time hint (ruling D point 1): objectstack validate warns (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: / 100 becomes / 100.0.
  • Wrong guidance fixed (ruling D point 2): the template.ts docblock and the shipped round() arity refusal now prescribe round(x * 100) / 100.0. Measured on this tree: CEL answers 1234 for round(x * 100) / 100 and 1234.57 for / 100.0 at x = 1234.5678, while the template dialect answers 1234.57 for both. / 100.0 is right in both dialects. The arity pin in template-functions.test.ts asserts the new prescription and the measured reason.
  • Docs (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: ExpressionSchema accepts an ast-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.
  • Changeset: minor for @objectstack/spec, @objectstack/service-automation and @objectstack/lint, with Clause-②: 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 through scripts/pm/os-regen-merge.sh. It resolved without conflicts, check:generated stayed 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:

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 / assignments blocks: the hint fires on 0 flow sites outside tests in objectstack (94216fb72) and on 2 in HotCRM (2f7b232, the two quote-generation.flow.ts money 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, the os validate pipeline (normalizeStackInput → unknown-key lints → ObjectStackDefinitionSchema → runAuthoringRules('validate')), registerFlow, and a run over a real ObjectQL with a recording driver.

  • Base (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.
  • After:
    • The 18 template and literal rows are byte-identical to base.
    • The 6 valid-envelope rows are evaluated (price * 2 writes 42 on the number, text and JSON columns).
    • The 18 malformed rows (no source, non-parsing CEL, a template dialect) are refused at validate (error / expression-invalid) and at registerFlow, located at config.fields.FIELD with the slot-neutral sentence.
    • FlowSchema.parse is unchanged, because node config is an open record.
  • Runtime publish gate (metadata-protocol), not measured before and measured now through saveMetaItem over the protocol's stub-engine harness:
    • A malformed fields.* envelope goes from SAVED to 422 INVALID_METADATA with expression-invalid at the node.
    • A valid envelope still saves.
    • A {round(price * 100) / 100} template saves with the hint as an advisory, in all three value slots.
    • The assignment control rows were refused before and after; only the sentence changed.
  • Nested values (mechanism assumption 3): only the top-level value of a field is judged. An envelope-shaped object nested in a JSON value or an array is data, is interpolated as before, and is written verbatim, which the executor test pins. A top-level JSON value that is itself an object with a string dialect is 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 into fields. The heuristic scan found 0 outside tests; its control leg found 14 envelope-shaped assignments values in objectstack tests.

Reverse verification (one-off, from the committed state)

  • The executor half was reverted to the old whole-map interpolate() at both sites through scripts/ablation-replace.mjs: anchor 2 → 0, blob a292a5ac9181 → 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 and git diff HEAD is empty.
  • The lint hint was short-circuited. The lint test went to 4 failed / 22 passed, all four of them the hinted cases, and the restore was proven.
  • The two ledger rows were deleted. The spec pins went to 6 failed / 127 passed (census, the two slot declarations, the two field resolutions, the value-slot resolver), and the restore was proven.

Verification at 94216fb72

  • node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derived 110 commands. --ran reports 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-loads and check:type-check-debt, the last one re-run after direct rebuilds because the ablation restores had touched mtimes.
  • Package suites, all command-exit 0:
    • @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)
  • typecheck is green for all four.
  • The other direct consumers of the changed exports are green: examples/app-showcase/test/predicate-write-bulk-intent.test.ts (17/17, parses with UpdateRecordConfigSchema) and packages/cli/src/flow-node-undeclared-field-write.integration.test.ts (7/7, drives registerCrudNodes).

Acceptance notes

  • registerFlow has no ADR-0112 envelope. Every expression refusal there throws one aggregated plain Error with no code or status, and this is pre-existing and door-wide. The registration pins therefore assert the located message substance (node, slot, path, sentence). The os validate pins assert the rule id expression-invalid and severity error, and the publish gate answers 422 / INVALID_METADATA.
  • One excuse was added to the validate-expressions.test.ts meta-guard: grammar. It is an import-specifier artefact, not a receiver. The guard's scan RULE_CODE.matchAll(/\b([a-z][\w$]*)\??\.[A-Za-z_$]/g) reads grammar.js inside './flow-template-grammar.js' as a receiver, the same artefact it already excuses as scope, fields and guards. 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 excused found (the same resolver-result shape), and the token scan uses a RegExp.exec loop 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.
  • File surface beyond the claims' lists, each a test of a claimed file:
    • template-functions.test.ts: the arity-refusal prescription pin for template.ts;
    • validate-expressions.test.ts: the meta-guard entry above;
    • two new test files: crud-fields-value-envelope.test.ts and validate-expressions.fields-value-slot.test.ts.
  • Observations, not filed:

Generated by Claude Code

…-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>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 94216fb725b92eb71f0d129130b72057a5496b38

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 objectstack validate --json over all four example stacks at the head, the PR's new and changed test files at the head (64 + 26 + 1 + 133 passed), a driver-free merge-tree of the head onto current main, a read-only grep census of HotCRM at 2f7b2326e. NOT MEASURED: any HotCRM flow run; objectstack validate at the base (base CLI closure not built; the head result alone excludes a new failure); the Studio / REST / MCP wire path (the gate function was driven directly, not saveMetaItem over HTTP); the execute-time contract parse of a malformed envelope already stored in sys_metadata; whole-package suites and every derived check:* gate family (not re-run by rule); the merge queue's rebuilt generation.

① Derived judgments

(a) Ruling D point 1, measured on the built trees (base 560b724 vs head): a valid envelope { dialect: 'cel', source: 'price * 2' } in fields.* is EVALUATED at run time on both nodes and all three columns (number, text, json each receive 42, run success true); at the base the same rows wrote the literal object into text and json with success true and were refused by the data engine on the number column. The 30 rows for a sole-token template '{price}', a text template 'Total: {price}', the literal 42, a JSON literal with a nested envelope-shaped object, and an array of envelope-shaped members are byte-identical between base and head on every door and at run time (identical JSON rows). A template expression '{round(price * 100) / 100}' writes 21 on both trees. A malformed envelope (no source, non-parsing CEL, template dialect) never reaches the executor at the head: refused at registration. An envelope that parses but faults on the live values fails the run loudly (fields.COL: value expression failed to evaluate as CEL … source: …) and writes nothing. The per-key resolveFieldValues is interpolate() per entry for every non-envelope value, and interpolate() on a plain object is exactly that per-entry recursion (template.ts:397-421), so the preservation holds by construction as well as by measurement. PASS.

(b) No door disagrees at this head. The os validate rule path (runAuthoringRules('validate'), rule expression-invalid, severity error, located at config.fields.FIELD), AutomationEngine.registerFlow (one aggregated refusal naming node 'w' (create_record) create_record field value at config.fields.FIELD), the metadata-protocol publish gate (evaluateRuntimeAuthoringGate → 422 / INVALID_METADATA with the same error/expression-invalid issue) and the run time all draw the envelope line with the same imported predicate isExpressionEnvelopeShaped and the same shape rule FlowValueSlotSchema, and every one of the 72 rows agrees across them. FlowSchema.parse accepts every row on both trees because node config is an open record — it is not a door for this slot (nor for assignments.* today), so it declares nothing the runtime does not honour. The one ledgered class of disagreement is the published JSON Schema for CreateRecordConfig / UpdateRecordConfig / FlowValueSlot, which is wider than the Zod (the .superRefine does not project): declared in dropped-refinements.baseline.json and stamped x-dropped-refinements on the artifact, the same class AssignmentValue already sits in; not a new window. PASS.

(c) Only the TOP-level field value is judged: measured, a nested { dialect: 'cel', source } inside a JSON literal or an array is interpolated as data and written verbatim on both trees. A top-level JSON literal that merely carries a string dialect key (probed: { dialect: 'en-US', label: 'x' }) IS misread as an envelope at the head — and the consequence is a loud refusal at every door (two expression-invalid errors, registration refused, 422), never a silent rewrite; at the base it was written verbatim. A top-level literal that happens to be a valid CEL envelope is evaluated instead of stored. The changeset states the rule ("Only the top-level value of each field is judged…", "A JSON column whose intended literal value is itself an object with a string dialect key is now read as an envelope") and the escape (bind it to a variable, write '{thatVariable}'), and names the measured census (0 in this repo and HotCRM; my grep of HotCRM src at 2f7b2326e and of examples/ at the head finds no envelope-shaped fields value). Judged safe on the #14149 precedent: the edge is the same one assignments.* accepted, and it is loud. PASS.

(d) {var} keeps its 17.x meaning: the 30 template/literal rows are byte-identical; the four example stacks validate at the head with exit 0 (app-crm 9 warnings, app-multi-package 3, app-showcase 84, app-todo 7; 0 errors each). The hint is severity warning (probe: validate warning, publish gate advisory only, registerFlow OK, run identical; splitBySeverity puts it in advisories and os validate exits 0 without --strict). It fires on 0 sites in the four example stacks at the head and on 0 in-repo non-test flows; no existing in-repo flow newly fails validation. HotCRM at 2f7b2326e carries exactly the two quote-generation money sites the ruling measured (round(... * 100) / 100), which would receive the warning and no error. PASS.

(e) Ruling D point 2: template.ts docblock (the scale-2 pattern now reads round(x * 100) / 100.0 with the measured integer-division reason) and the shipped round() arity refusal (requireArity, "for N-decimal rounding write round(x * 100) / 100.0 … / 100 drops the decimals there") both prescribe / 100.0; the flows.mdx dialect row for create_record / update_record field values prescribes {round(x * 100) / 100.0} and says why; template-functions.test.ts pins the prescription and the measured 1234 vs 1234.57. No remaining "CEL idiom" / "identical in CEL" claim in either file. skills/**: 0 files in the diff. PASS.

(f) Q2: VALUE_ENVELOPE_REFUSAL is slot-neutral ("A value carrying a dialect key…"); ASSIGNMENT_VALUE_ENVELOPE_REFUSAL is kept as a published export and is the same string; AssignmentValueSchema, AssignmentExpressionValueSchema, AssignmentValue, AssignmentValueParsed, AssignmentExpressionValue(Parsed) all remain exported; api-surface/automation.json: 5 added, 0 removed. Measured: a refused field value's message contains no "assignment". PASS.

(g) The grammar meta-guard excuse is a genuine import-specifier artefact: at the head the only occurrences of grammar in validate-expressions.ts are the named import from './flow-template-grammar.js' (line 119) and the docblock's flow-template-grammar.ts (line 146) — the scan /\b([a-z][\w$]*)\??\.[A-Za-z_$]/g reads grammar.j / grammar.t there; no local is named grammar. Every other receiver the change introduces is either uppercase (TEMPLATE_*_RE, SAFE_EXPRESSION_RE, FLOW_TEMPLATE_VALUE_FUNCTIONS) or an already-excused local (found, issues); READ_SURFACES / NOT_A_SINGLE_SHAPE / the tabled set are untouched, so the guard is unchanged for real receivers. The guard test passes at the head (1 passed, 336 skipped under -t). PASS.

(h) dropped-refinements.baseline.json: its header says it is hand-edited on purpose with no gen: script, and that adding a site fails build-schemas.ts until the line moves with it. The gate is the spec build itself (checkDroppedRefinements: undeclared / miscounted / repaired / vanished / unreasoned over entries), which runs in the required Build Core and TypeScript Type Check jobs; the three new entries (automation/CreateRecordConfig fields.valueType, automation/UpdateRecordConfig fields.valueType, automation/FlowValueSlot "") are exactly what the new superRefine sites require, and the head's spec build succeeded with them (turbo cache HIT keyed on this exact tree, then the PR's spec tests green). Correction to the PR's Acceptance note: NONE of the measured totals is pinned — readDroppedRefinementsBaseline reads entries only — so the edited 207→210 / 574→577 are informational prose exactly like the stale refinementSitesThatDidProject 369 the note leaves as found. Non-blocking. PASS.

(i) Other published changes: ASSIGNMENT_VALUE_ENVELOPE_REFUSAL's VALUE changed (stated in the changeset, with the warning that matching on the old literal text breaks); AssignmentExpressionValueSchema's dialect error text is neutral now; predicateSlotRefusal's shipped sentence gained "a create_record / update_record node's fields map" (not named in the changeset; wording only); CreateRecordConfigSchema / UpdateRecordConfigSchema fields values are FlowValueSlotSchema (input type still unknown; JSON Schema gains the description and xExpression: 'value'); LEDGER_DECLARED_NODE_CONFIG_SCHEMAS +2 keys; FLOW_NODE_EXPRESSION_PATHS +2 rows (census 5→7); new exports VALUE_ENVELOPE_REFUSAL, FlowValueSlotSchema / FlowValueSlot / FlowValueSlotParsed, resolveFlowNodeValueSlots; the lint's new advisory rides the existing rule id expression-invalid at warning. No other accept set or export moves. PASS.

② Semver level

Clause-②: yes (widening) with minor for @objectstack/spec, @objectstack/service-automation and @objectstack/lint is consistent with the gates' stated rules: the level axis of check-changeset-no-major requires a declared yes to grade at least one moved package minor or above (three do); the WHICH LEVEL prose grades an additive widening of a published surface minor and the commit type (feat() does not lower it; no major; no BREAKING banner, so check-adr-0087-registration asks for no disposition marker; at most one arm is claimed. The PR body carries Clause-②: yes; the arm lives in the changeset. The changeset names what newly passes (a valid envelope as a top-level fields value, evaluated; before, written verbatim or refused by the data engine) and what newly refuses (a top-level object with a string dialect that is not a valid CEL value envelope — missing / empty / whitespace source, ast-only, template or cron dialect, non-parsing CEL — at every door, located at config.fields.FIELD), states that this is the malformed-envelope edge the assignments map accepted when it gained the envelope (the #14149 edge, not cited by number), and states the nested / literal rule and its escape. The wider read — that a JSON literal carrying a string dialect is thereby refused — is inside that stated edge and matches the ruling record's Q3 reading; it is not a (narrowing) claim. Omissions, wording-only: the predicateSlotRefusal text change and the advisory's rule id. Judged: correct level, correct arm, complete on the two questions asked.

③ Boundary flags

Blocking: 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-origin/main hazard); a driver-free merge-tree of 94216fb onto 1c8b320 conflicts on content/docs/references/index.mdx (generated; main's 4db1bf1 retired ScheduleState and re-counted), and api-surface/automation.json / json-schema.manifest/automation.json text-merge but must be regenerated per §10/§11. GitHub read the PR as mergeable: false / dirty and builds no merge ref for a conflicting PR, so the pull_request workflows never ran: 0 check-runs, 0 Actions runs on the head. The remedy is the os-regen-merge lap (merge current main, check:generated --fix, push) — which produces a NEW head, so this record certifies 94216fb only and the new head needs its own record and its own green.
Non-blocking: (1) commit order is as ruled — de7c289 (spec, parent 455dcc0 on main) then 8e37b80 (engine), then the merge; the four Acceptance-note items hold: registerFlow's plain aggregated Error with no ADR-0112 code/status is pre-existing and door-wide (measured); the grammar excuse is an artefact (① g); the extra files are tests of claimed files plus the baseline the build requires; the three observations are pre-existing — with the correction in ① h that no measured total in the baseline is pinned. (2) skills/objectstack-automation/SKILL.md:236 still prescribes / 100 — Tier H, deliberately untouched under ruling D point 2, so the wrong guidance stays published there until its own draft lands. (3) After the re-merge, api-surface/automation.json will differ from the head's (ScheduleState gone on main) — expected, regenerated, not this PR's doing. (4) Three --force-with-lease reshapes before any PR existed are declared and met the five criteria. (5) The advisory reuses rule id expression-invalid for a warning; existing practice in the same rule, but a gate keyed on that id at any severity would count it. (6) Draft PR, no labels, assignee os-sales, auto-merge not armed, 1438 changed lines.

CI at this head: absent — GET /commits/94216fb72…/check-runs returns total_count 0 (read at 09:15Z and again at 09:38Z), GET /actions/runs?head_sha= returns 0, the five check-suites (vercel, fly-io, claude, cloudflare-workers-and-pages, objectstack-fleet) are queued with 0 runs, and the only commit status is Vercel: success; none of the seven required contexts (Lint & Repo Gates, TypeScript Type Check, Test Core, Dogfood Regression Gate, Build Core, Temporal Conformance, Governed Surface Queue Guard) has started, because the PR is unmergeable (see Blocking). Not red — not run. Every sibling draft PR opened in the same minutes (#20204, #20207–#20210) carries 16–36 check-runs. Closing keywords: the body closes exactly #19938 and #11182 (Fixes #19938, Fixes #11182; no other closing keyword).

Implemented-by: claude/issue-19938-fields-value-slot
Reviewed-by: session_01CiCTczDo7tGhafXjf61dUJ

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>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 27, 2026
@github-actions

github-actions Bot commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/lint, @objectstack/service-automation, @objectstack/spec, touching 35 documentable anchor(s). ⚠️ 5 changed file(s) yielded no anchor (packages/spec/api-surface/automation.json, packages/spec/declaration-map/automation.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.

19 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json ab820016b3e9691870e24a9bbb867336ee5e8f72.

⛔ 4 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

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

Coarse fallback — 136 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 ab820016b3e9691870e24a9bbb867336ee5e8f72 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 4f0f80e5f8a07d1507268fec938e379b78d30d84 — the merge of head 68123f42859e2fa7e3a8346a664c2e155e3ee0a2 into base ab820016b3e9691870e24a9bbb867336ee5e8f72, 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 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

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs ab820016b3e9691870e24a9bbb867336ee5e8f72 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…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
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 56f77479e557cac2c17e3cc4f5a26455317178bb

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); .gitattributes merge=os-regen lines at the head; dropped-refinements.baseline.json at 560b724, 94216fb, e7f69db, 3875ae6, 805af4f, 56f7747 and its pin packages/spec/scripts/dropped-refinements.test.ts; check-generated.ts's GATED ledger and the lint.yml / ci.yml steps that run its gates; main's 21 first-parent commits 560b724..805af4f (211 files) and its 6 commits since (to 3cb84d0). Ran, read-only in the shared checkout on refs/pr-review/20205: per-file normalized-diff and blob comparison of git diff 560b724c9 94216fb72 against git diff 805af4f29 56f77479e (25 files); a Python recount of the ledger at the six refs; a per-merge blob-origin check of the three merges and the two regen commits; a semantic grep of main's diff between the bases; a driver-free git merge-tree --write-tree 3cb84d084 56f77479e; node scripts/docs-audit/affected-docs.mjs --json 805af4f29… at HEAD 56f7747 in a detached worktree under the scratchpad (the shared checkout's HEAD is 1c8b320, older than the base, where the prescribed invocation --json 56f77479e… returns 0 docs / 0 anchors, so the head's own tree was needed; the bot's merge commit 0fae3a0 has the same tree since the base is its ancestor) and a grep plus spot-read of the 23 pages it lists. NOT MEASURED: any build, test run or check:generated (not re-run by rule); the declaration-map artefact's currency, which no CI step at this head checks (see ① b); the merge queue's rebuilt generation; HotCRM; the semantic judgments of the earlier record, carried over unchanged because every non-generated, non-ledger blob is identical to the one it read.

① 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 merge=os-regen paths and the ledger, the 18 remaining files (the changeset, flows.mdx, 5 lint files, 8 service-automation files, 5 spec source/test files) have a byte-identical normalized diff each (index lines dropped, hunk offsets blanked), identical head blobs (94216fb vs 56f7747) and identical base blobs (560b724 vs 805af4f), so main did not touch any of them; the whole-set normalized-diff hash is 05efa004e2fe0daa on both sides. Files whose diff differs: content/docs/references/index.mdx (generated; main re-counted the base from 1538 to 1522 schemas, the PR's +1 / 74 to 75 hunk is the same) and packages/spec/dropped-refinements.baseline.json (the ledger, ① c). The four automation.json artefacts have identical normalized diffs on blobs that moved with main. PASS.

(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 + line that carries the old content plus this PR's addition: the two import lists gain FlowValueSlotSchema / FlowValueSlot, the envelope description reads "the slot" for "the variable", the two fields rows gain the value contract, index.mdx's three count lines go 1522 to 1523 / 74 to 75 and its builtin-node-config schema list gains FlowValueSlot. No main-side line is dropped. Currency at this head, from its CI: Check generated reference docs are in sync with the spec, Check the export-origins baseline resolves as recorded, Check the authorable key surface is recorded and nothing vanished, Check spec-changes.json …, Check the protocol upgrade guide …, Check the meta-url-spelling data module is current (all in Type Check · source gates, success), Check @objectstack/spec public API surface (check:api-surface, Type Check · consumer gates, success), and the spec build with its json-schema.manifest ratchet (Build Core, Type Check · workspace, success). check:declaration-map has no CI step at this head; the artefact's currency rests on the fact that no spec generator changed on main between the bases (git diff --name-only 560b724c9 805af4f29 -- packages/spec/scripts, non-test: empty), its hunk is byte-identical to the one that passed at 94216fb, and the dev's check:generated 15/15 reading, which I did not re-run. PASS.

(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 len(entries) and sum(sites). The PR's added (schema, site) pairs are identical at both heads: automation/CreateRecordConfig fields.valueType, automation/UpdateRecordConfig fields.valueType, automation/FlowValueSlot "". Main added 14 sites and removed none between the bases (the manifest.datasets.element.filter / measures.element.filter family under four Package projections, the $ne family under three); all 14 are present at the new head, no main site is missing, none resurrected, and the new head's entry set equals main's union the PR's three exactly; keys and sites stay sorted; the other header fields (zod 4.4.3, refinementSitesThatDidProject 369, refinementSitesWithNoJsonFormToCompare 0) and the description are byte-equal to main's. Correction to the earlier record's ① h: the two totals ARE pinned — packages/spec/scripts/dropped-refinements.test.ts, "the committed ledger … its header totals match its body", present since before the old base, included by spec's vitest scripts/**/*.test.ts and run in Test Core (success at this head) — so the dev's recount is a gate, not prose, and it is right. PASS.

(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 automation.json artefacts and index.mdx, the os-regen driver's doing) and 9670123 regenerated exactly those (−17 / +8: the ScheduleState lines main retired in #20194 leave the automation artefacts, index.mdx re-counts); merge 2 kept index.mdx and 7af0516 regenerated it (−5 / +5). Both regen commits touch only generated paths. Main's 21 first-parent commits between the bases touch 211 files, none of the PR's 18 source files; in the PR's neighbourhood they touch packages/lint/src/index.ts (a new validateMappingTargetFields export, #20150), service-automation/README.md (the retired GET /api/v1/automation route, #19543), spec/src/automation/execution.zod.ts (ScheduleState retirement) and three unrelated lint rules; a grep of main's whole diff for predicateSlotRefusal, FlowValueSlot, VALUE_ENVELOPE_REFUSAL, evaluateValueEnvelope, valueEnvelopeRefusals, isExpressionEnvelopeShaped, resolveFlowNodeValueSlots, FLOW_NODE_EXPRESSION_PATHS, LEDGER_DECLARED_NODE_CONFIG_SCHEMAS, resolveFieldValues, crud-nodes, validate-expressions, fields.* and "value slot" hits only the data protocol's $ne "one-value slot" comparand text (#20204), unrelated. No merged main change touches flow-node-expression-paths.ts, the fields.* value slot, predicateSlotRefusal or the engine half. PASS.

(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 create_record / update_record (an anchor from LEDGER_DECLARED_NODE_CONFIG_SCHEMAS's new keys) or registerCrudNodes (permissions/system-context.mdx); automation/flows.mdx is listed via FlowValueSlotSchema and is the PR's own edit, unchanged since the reviewed head. A grep of the other 22 pages for dialect, envelope, assignments, round(, / 100, "only value", "value slot", expression-invalid, AssignmentValue, interpolat finds no page that calls fields.* template-only, names the assignment map the only value slot, or prescribes / 100. Spot-read: kernel/runtime-services/examples.mdx:95 writes fields: { line_count: '{totals.line_count}', amount_total: '{totals.total}' } on an update_record node — plain-string templates, which mean at this head exactly what they meant; automation/hooks.mdx:34-37 says an update_record node's fields is structural config os validate checks for "template dialects, declared expression slots", which the declared slot makes more true, not false; releases/v17/17-4.mdx:382 ("An assignment value may be a CEL envelope, validated at registerFlow …") is release-owned history and still true. No listed page states something this PR made false. PASS.

② Semver level

Unchanged from the earlier record: minor for @objectstack/spec, @objectstack/service-automation and @objectstack/lint, Clause-②: yes (widening), the body carries Clause-②: yes. The changeset blob at 56f7747 is the one the earlier record read; Check Changeset is green at this head; no main change between the bases touches any file of this PR or its published surface (① a, ① d), so nothing in the delta moves the level or the arm.

③ Boundary flags

Blocking: 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 mergeable: true / clean, and the head has a full green run.
Non-blocking: (1) The dev's note — every spec PR that adds or removes a dropped-refinement site re-conflicts on the ledger's header totals when main moves — is real but not blocking for THIS head: at the time of reading main has moved 6 commits past the base (to 3cb84d0, 12:55Z) and none of them touches the ledger, any PR file or the PR's generated files; a driver-free git merge-tree --write-tree 3cb84d084 56f77479e is conflict-free (tree dfef258a6). It becomes a conflict again only when a main commit changes a site count; the queue admits this head as it stands. (2) check:declaration-map has no CI step at this head, so the declaration-map artefact's currency is not CI-measured (① b); a gap in the gate farm, not in this PR. (3) The earlier record's ① h ("none of the measured totals is pinned") is reversed by ① c: the two totals are pinned by dropped-refinements.test.ts; the earlier verdict is unaffected because the totals were right there too. (4) Draft PR, labels documentation, size/xl, tests, tooling, auto-merge not armed; the ready-for-review flip is the seat's. (5) skills/objectstack-automation/SKILL.md:236 still prescribes / 100 (Tier H, deliberately untouched under ruling D point 2), carried forward.

CI at this head: 35 check-runs on 56f7747, all completed — 33 success, 2 skipped (Console Pin Gate, Packed-tarball smoke (opt-in), both skip by design). All 7 required contexts in the main ruleset are success: TypeScript Type Check, Test Core (and its 6 shards), Dogfood Regression Gate (and its 3 shards), Build Core, Temporal Conformance (live PG + MySQL), Lint & Repo Gates, Governed Surface Queue Guard. The rest: Auto Label, Build Docs, Check Changeset, Check Documentation Links, Check PR Size, 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, The card this PR closes must claim this branch, Type Check · consumer gates / debt ledger / source gates / workspace, filter — all success. 12 pull_request workflow runs, 11 success, 1 skipped (Pack Smoke, opt-in). Combined commit status: Vercel success. mergeable: true, mergeable_state: clean, merge commit 0fae3a0, read at the start and again at the end; the head did not move during this review. Closing keywords: the body closes exactly #19938 and #11182 (Fixes #19938, Fixes #11182; no other closing keyword), and the three claim guards are green.

Implemented-by: claude/issue-19938-fields-value-slot
Reviewed-by: session_01CiCTczDo7tGhafXjf61dUJ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 27, 2026 13:39
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Sep 27, 2026
…elds-value-slot

# Conflicts:
#	packages/spec/dropped-refinements.baseline.json
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 68123f42859e2fa7e3a8346a664c2e155e3ee0a2

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. git diff --name-status of 68123f4 against ab82001 lists the same 25 paths as 56f7747 against 805af4f: 18 source/test/docs/changeset files, 6 merge=os-regen paths, the ledger. Each of the 18 files' diff, normalized (index line dropped, hunk offsets blanked), is byte-identical old vs new; whole-set sha256-16 is cf83039fc9a069dc at both heads, and the raw unnormalized whole diff of the 18 is 5a90b32a395523dd at both. The 18 blobs at 68123f4 equal the 18 blobs at 56f7747, since main touched none of them between the two bases. No file differs, nothing to read.

(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 fields rows gain the value description, index counts move 1522 to 1523 and 74 to 75. No main line dropped, and nothing stale, because main's automation regen inputs did not change. Nine main commits arrived: 6d2571f (#19957), 93cfc3f (#20222), 615c468 (#20224), fc91239 (#19637), 66e266c (#20226), 3cb84d0 (#20227), c02fa12 (#20218), ca753c0 (#20223), ab82001 (#20232). None touches packages/spec/src/automation, packages/services/service-automation or packages/lint/src; none touches flow-node-expression-paths.ts, builtin-node-config.zod.ts, crud-nodes.ts, engine.ts, logic-nodes.ts, node-executor.zod.ts, validate-expressions.ts, template.ts or flows.mdx; main's delta contains no predicateSlotRefusal, FlowValueSlot, evaluateValueEnvelope, VALUE_ENVELOPE_REFUSAL or LEDGER_DECLARED_NODE_CONFIG_SCHEMAS text. Only ca753c0 (#20223, refuse scale on a currency inline grid column) intersects the PR's paths, and only on the ledger. Judgment: no merged main change touches this PR's semantics.

(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 git merge-tree of the two parents conflicts only on the two totals (PR 210/591 against main 208/601); the hand resolution 211/604 is main's 208 plus 3 and 601 plus 3, and the head differs from the auto-merge tree in nothing else.

(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 level

minor, unchanged from the reviewed change: the changeset at head declares @objectstack/spec, @objectstack/service-automation and @objectstack/lint minor with Clause-②: yes (widening), and the merge adds nothing of this PR's own. Main's own ! commits (#19957, #19637, #20227, #20223) carry their own changesets and are not this PR's level.

③ Boundary flags

Blocking: none
Non-blocking: the prior record's whole-set hash 05efa004e2fe0daa was not reproduced because its normalization is not stated; per-file byte equality of the 18 normalized diffs and blob equality of the 18 files across the two heads decide (a) instead. Two check-runs are skipped, Console Pin Gate and Packed-tarball smoke (opt-in); neither is in main's ruleset required list. origin/main has already moved past the merge base (e0f17a3 at review time) while mergeable_state is still clean at this head, so this ledger may re-conflict again if main's next landing touches it.

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: claude/issue-19938-fields-value-slot
Reviewed-by: session_01CiCTczDo7tGhafXjf61dUJ

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit e462186 Sep 27, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-19938-fields-value-slot branch September 27, 2026 15:47
os-zhuang pushed a commit that referenced this pull request Sep 27, 2026
…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>
os-litant pushed a commit that referenced this pull request Sep 27, 2026
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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment