Skip to content

Commit 0e13561

Browse files
objectstack-fleet[bot]os-bobclaude
authored
docs(skills): objectstack-data carries the hosted build agent's metadata-authoring judgment (cloud#3659 A, port 1 of 3) (#31223)
Part of #31155 — card 1 of 4 of the objectstack-ai/cloud#3659 ruling-A split; this PR is the first per-domain port (objectstack-data). The automation and ui ports follow as their own PRs on the same card, so the card stays open. Clause-②: no Metadata-protocol: none ## 维护者速读 - 改了什么:把云端私有搭建语料里「schema 表达不了的判断」搬进公开的 `objectstack-data` skill(布尔字段给默认值不设必填、人员字段用 `user` 且默认可选、动态默认值是保存时生效、状态用 select 不建表、审批/流水不建对象、负责人就是平台 `owner_id`、按需求选 `sharingModel`、状态机要声明 `initialStates`、汇总字段的限定词进 `filter`、迟声明的汇总用 `os migrate summary-nulls` 回填、种子值可虚构不可畸形且 `os validate` 先拒、标签跟随用户语言),外加 8 条 eval 用例;完整的 43+11 条规则映射表见 issue 评论与下文。 - 为什么改:cloud#3659 裁决 A ——所有搭建渠道只认发布的 skills + spec。本 PR 只搬第 3 类(判断),第 1 类(复述 spec)只映射到 spec 路径,第 2 类(运行时陷阱)列成 enforce-or-remove 清单交 spec 席(已立 #31224),不写散文。 - 风险与回滚:纯文档 + eval 夹具,无运行时改动;token 棘轮四行按实测钉住(ceiling 上调,裁决授权见脚本注释)。发布的 skill 正文全英文(审批人要求),eval 的提示语按裁决豁免。回滚 = revert 本 PR。 - 先看:`skills/objectstack-data/rules/relationships.md`(Roll-up 段重写、五个示例的 `sharingModel`)、`skills/objectstack-data/SKILL.md`(Relationship Patterns 新增三行、Field Type Selection 三条、posture 段)、`scripts/check-skills-token-ratchet.mjs`(四行 ceiling 与注释)。 - 两分钟验证:`node scripts/check-skills-token-ratchet.mjs` 预期全绿且 objectstack-data 四行 headroom 0。 - 需要你做的:Tier H 受管面(`skills/**`),需要你的 APPROVED review 后由席位落地;本 PR 保持 draft。 ## What changes **Language scope (approver os-zhuang, CHANGES_REQUESTED 2026-11-07T23:41Z, verbatim: 「skills 对外发布的版本应该全部用英文」; scope ruled by the account owner, verbatim 「同意」, to the seat's proposal):** every sentence a customer agent READS in the shipped skill text is English — the two example phrases in `SKILL.md` and the roll-up passage in `rules/relationships.md` were reworded to English or language-neutral wording teaching the same judgment, and every non-eval file this PR touches scans clean of CJK. Two exemptions: (a) eval prompt strings in `skills/**/evals/*.json` (they test "labels follow the user's language" and never enter an agent's context); (b) the existing `objectstack-i18n` Chinese example data, untouched. `objectstack-data` gains the metadata-authoring judgment the hosted build agent carried privately and the published skill did not yet state — only **class 3** of the seat's triage (judgment the schema cannot express). Class 1 (restates the spec) is mapped to its spec path; class 2 (parses clean, dead at runtime) is listed as enforce-or-remove for the spec lane (filed as #31224); hosted-only and orchestration rules stay in cloud. - `SKILL.md` § Object Definition — choose the `sharingModel` posture from the request (rule 16b); the skill's own examples now take that posture (below). - `SKILL.md` § Field Type Selection — a yes/no flag is `{ type: 'boolean', defaultValue: false }`, never `required` (18); a person on the record is `type: 'user'`, optional unless asked (process-modeling); a dynamic default resolves on save, described as such (19d). - `SKILL.md` § Relationship Patterns — three rows: status is a `select`, never a table (select-options); approvals / routing / audit trails are not objects (process-modeling); the owner is the platform `owner_id`, `ownership: 'none'` only for catalogs and junctions (16). - `SKILL.md` § Validation Patterns — a `state_machine` declares `initialStates` whenever the status has a real start (17); the UPDATE-only mechanic sentence is not restated (the spec describe carries it). - `rules/relationships.md` § Roll-up Summaries — the `filter` paragraph is rewritten: pick the function from the words, every qualifier goes in `filter` (dropped = a plausible wrong total), one child feeds several totals, a rate is a formula over a working summary; "The recompute runs only on a child write" — a summary declared after its rows exist is backfilled by `os migrate summary-nulls --apply` (`--recompute-undefined-on-empty OBJECT.FIELD` for one declared late), never by editing a child (15). Five example objects take the posture the SKILL.md paragraph teaches: `public_read_write` on `account`, `contact`, `invoice` and the `project_assignment` junction, `public_read` on the `category` table; the conditional-rules `invoice` in `SKILL.md` likewise. - `rules/validation.md` § State Machine Validation — the canonical example declares `initialStates: ['draft']`, so it no longer undercuts the rule. - `rules/naming.md` — Critical Rules 6: labels follow the user's language (21). - `references/seeds.md` — "Fictional is fine; malformed is not", and `os validate` / `os build` refuse a malformed seed value first, at the record's field (19). - Evals: `objects-fields-relationships.json` cases 10–14, `hooks-security-seeds-datasources.json` cases 14–16 (renumbered after the cases `main` landed, twice) — the equivalents of cloud's `boolean-never-required`, `label-language` and `people-lookups-optional` text pins, plus the roll-up qualifier, owner, status-select and sharing-posture teachings. - `scripts/check-skills-token-ratchet.mjs` — four rows pinned AT the measurement (below). - `.changeset/31155-port-authoring-rules-data.md` — `@objectstack/skills` patch. Same-file pay-downs came first: in `SKILL.md` the `unique`-validation blockquote restated `rules/validation.md` § Uniqueness and is a one-line pointer now, and the `secret` / `user` parentheticals in Field Type Selection restated `rules/field-types.md` rows 42–43 and 96 and are gone; in `rules/relationships.md` the `engagement` example and the "aggregate only a subset" lead-in were restated by the kept `procurement_receipt` example, the `filter` code comment's second line is said by the paragraph, and the child-field half of the `filter` sentence left the prose (it is enforce-or-remove row #31224 D); in `rules/validation.md` the "(removed from the spec)" aside restated § Uniqueness. ### Token ratchet — four raises, authorised by the ruling that orders the text in Convention `ceil(utf8 bytes / 4)`, stamp line excluded on `SKILL.md`; bases are `origin/main` (fe97d85 and after), not the old merge-base. Raise path: maintainer ruling 2026-10-27, verbatim and untranslated, 「A:先削重复,余额按实测提高」 (ruling record 6513306068); the ruling of record that orders this text into the published skills is objectstack-ai/cloud#3659 letter A, maintainer 「A」 (ruling comment 6682024726): "How to write valid, working metadata lives in the public skills … The private corpus's rules of that kind move there." | file | priced bytes (`main` → head) | tokens | ceiling | |:--|:--|:--|:--| | `skills/objectstack-data/SKILL.md` | 24308 → 26658 (+2350 net) | 6077 → 6665 | 6078 → 6665 (headroom 1 absorbed; +587) | | `skills/objectstack-data/rules/relationships.md` | 15489 → 16505 (+1016 net) | 3873 → 4127 | 3873 → 4127 (headroom 0; +254) | | `skills/objectstack-data/references/seeds.md` | 6240 → 6985 (+745) | 1560 → 1747 | 1566 → 1747 (headroom 6 absorbed; +181) | | `skills/objectstack-data/rules/validation.md` | 10992 → 11044 (+52 net) | 2748 → 2761 | 2756 → 2761 (headroom 8 absorbed; +5) | Bundle: ratcheted (authored) count 182910 → 183952 (+1042 tokens; 15 absorbed by the four rows' headroom), ceiling sum 190832 → 191859 (+1027); eval fixtures are measured, not ratcheted. ### Line readings (against `origin/main` 007fdb1) Per edited file: `SKILL.md` 465 → 494 (+29); `rules/relationships.md` 405 → 419 (+14); `rules/validation.md` 328 → 329 (+1); `rules/naming.md` 21 → 25 (+4); `references/seeds.md` 163 → 175 (+12); `evals/objects-fields-relationships.json` 173 → 263 (+90); `evals/hooks-security-seeds-datasources.json` 135 → 165 (+30). Whole package, all `SKILL.md` summed: 4520 → 4549 (+29). Whole shipped tree under `skills/`: 20037 → 20217 (**+180 net lines**, inside the dispatch's +250 budget; evals are 120 of them). ## No-loss mapping table (private rule id → class → skills file and section → regression kept) The v1 ledger is card comment 6711215678 on #31155; its deltas after the contract review are card comment 6711710289. This is the v2 table — both corpora, every rule: Source corpus read at objectstack-ai/cloud `main` (2026-11-07): `packages/service-ai-studio/src/skills/metadata-authoring-skill.ts` (101,410 bytes, 43 rules), `solution-design-skill.ts` (52,885 bytes, 11 rules), `authoring-rule.ts` (19,183 bytes, the delivery machinery — no rules of its own). Every rule of both corpora is listed below; nothing is dropped from the ledger. **Triage classes** (the seat's refinement of the card's item 1, on AGENTS.md contract-first and 「声明即强制」): - **CLOUD** — orchestration or hosted-only (blueprint design, build loop, tool routing, confirm gate, drafts / NOT LIVE, plan tiers). Stays in cloud as the overlay (card item 2). - **SPEC** — restates what `packages/spec` already defines or refuses. Not ported as prose; the spec path is the carrier. A `.describe()` that lacks the sentence an agent needs is listed as a *describe gap*. - **E/R** — a trap that parses clean but is dead or wrong at runtime. Not written as prose; listed as *enforce-or-remove* for the spec lane. - **STATED** — the published skill already states it; mapped to the existing text, no new prose. - **PORT** — judgment the schema cannot express. Ported to the named skill and section. **PR 1 = this PR (objectstack-data)**; PR 2/3 = the later per-domain PRs on this card (automation, ui). The "regression kept" column names the cloud pin that hardened the rule (tests under `packages/service-ai-studio/src/skills/__tests__/` and `__tests__/authoring-rules.test.ts`) and where its equivalent lives after the move. Spec paths are under `packages/spec/src/`. ## `metadata_authoring` corpus | id | rule (short) | class | target → spec path / skill file § section / existing text | regression kept | |:--|:--|:--|:--|:--| | confirm-before-change | confirm gate, `changes_proposed` envelope | CLOUD | hosted tool contract | `authoring-rules.test.ts` "does not defer the rules that govern every turn" — stays in cloud | | base-first | writable package, `set_active_package` | CLOUD | hosted (ADR-0070 base) | stays in cloud | | version-boundary | what this tier can author | CLOUD | the restriction profile — card 2 (#31156) / card 3 (cloud#3713) | `authoring-rules.test.ts` version-boundary describes — stay in cloud | | new-app-blueprint | a new app goes through `propose_blueprint` | CLOUD | orchestration | stays in cloud | | edit-one-diff | one `apply_edit` per request | CLOUD | orchestration | stays in cloud | | auto-surfaced | tool auto-creates list view + nav; view kinds; never downgrade a named kind | CLOUD (auto-surfacing) + SPEC (`ui/view.zod.ts` view-type enum: `kanban`, `gallery`, `calendar`, `gantt`) + STATED (objectstack-ui `SKILL.md` § View Types rows `gallery`, `gantt`) | the "never silently downgrade a named view kind to a plain list" clause is PORT → objectstack-ui, PR 3 (one sentence under View Types) | no cloud pin names it; PR 3 adds a ui eval case | | flow-node-types | node `type` is the engine's vocabulary; a tool-named node = `NO_EXECUTOR` | E/R + STATED | **enforce-or-remove:** `os validate` / `os build` refuses a flow node `type` outside the composed registry's node executors (`automation/flow.zod.ts` node `type` is an open string by contract; the registry is the authority — ADR-0018). STATED: objectstack-automation `SKILL.md` § Flow Node Types + § Valid-but-silently-wrong. `test_flow` / `toggle_flow` half → CLOUD | `authoring-rules.test.ts` corpus integrity — stays in cloud | | flow-node-tier | the tier's node list | CLOUD | restriction profile | stays in cloud | | add-view-not-convert | "add a kanban view" = a NEW distinctly-named view coexisting; convert only on an explicit ask | PORT → objectstack-ui, PR 3 (`rules/list-views.md`, beside the kanban section: distinct name encodes the kind, e.g. `OBJECT.status_kanban`; never retype `OBJECT.list`). The tool-side refusal of an in-place kind flip → CLOUD | `kanban-new-group-field-disclosure.test.ts` "reachable from the ADD A VIEW rule" — PR 3 ui eval case; the cloud test stays pointing at the moved text | | capabilities | tool list | CLOUD | hosted | stays in cloud | | 1 | check before creating | CLOUD | orchestration | — | | 2 | read the schema first; kanban / gallery / gantt config; dashboard total needs a stored number; group-by over a multi-value field | CLOUD (mandate) + SPEC (`ui/view.zod.ts` `KanbanConfigSchema.groupByField` REQUIRED, `gallery.coverField`, `gantt.startDateField/endDateField/titleField`) + STATED (objectstack-ui `rules/list-views.md` kanban + gantt sections; objectstack-data `rules/field-types.md` `formula` row: "virtual: neither aggregatable nor a dataset or cube dimension") | **enforce-or-remove (verify):** a dashboard `dimensions` over a multi-value field (`multiselect` / `checkboxes` / `tags` / `multiple: true`) — `ui/dashboard.zod.ts` cannot see the field map; `os validate` should refuse it | no cloud pin — the `analytics-inline-vs-dataset.json` ui eval already covers the stored-number half | | 3 | describe before changing | CLOUD | orchestration | — | | 3b | snake_case, no leading digit — types, fields AND option values | SPEC | `data/field.zod.ts` field `name` regex `/^[a-z_][a-z0-9_]*$/`; `SelectOptionSchema.value` is `SystemIdentifierSchema` (refused at parse). STATED: objectstack-data `rules/naming.md` Rules table | `authoring-rules.test.ts` rule 3b block — stays in cloud (the spec refusal is the regression) | | 4 | field types from the user's words | STATED | objectstack-data `SKILL.md` § Field Type Selection; `rules/field-types.md` "When to Use" column | — | | 5 | propose an initial field set | CLOUD | design orchestration | — | | 6 | explain before executing | CLOUD | — | — | | 7 | changes are confirmed | CLOUD | — | — | | 8 | warn about data loss on destructive ops | CLOUD (chat warning) + STATED (objectstack-data `SKILL.md` § Schema evolution: drop / tighten = `os migrate apply --allow-destructive`) | — | — | | 9 | self-correct validation errors | CLOUD | — | — | | 10 | verify and self-correct, `unservedApps` | CLOUD | hosted `verify_build` | — | | 11 | complete the whole request | CLOUD | — | — | | 12 | never claim what you did not build | CLOUD | — | — | | 13 | a button / operation is `action` metadata, not a view; `objectName` binding; a write body must call `ctx.api…update`; `language: 'js'`; `capabilities: ['api.write']`; multi-step → `type: 'flow'` | SPEC (`ui/action.zod.ts`: `objectName` regex; body `language` 'expression' = pure formula vs 'js' = sandboxed; `capabilities`) + STATED (objectstack-ui `rules/actions.md` examples carry `objectName`, `language: 'js'`, `capabilities: ['api.write']`, `ctx.api.object(...)`) | PORT → objectstack-ui, PR 3: two sentences under § Actions — "an operation that DOES something to a record is an `action`, never a view; a body that only RETURNS the new values writes nothing — call `ctx.api.object(OBJ).update` with `api.write`" | no cloud text pin; PR 3 ui eval case | | 13-tier | the tier's action types | CLOUD | restriction profile | — | | 14 | record-change flow: trigger on the START node config; `triggerType` one string; `record-after-write` for create-or-update; `previous` null on the create leg; never an array of trigger types; bare CEL condition; option VALUE never label; `fields` / `filter` keys; no `recordId`; CEL value envelopes, no `{…}` tokens; `isoDate` / `current_user` envelopes; edges connect everything | SPEC: `automation/flow.zod.ts` refuses top-level `triggerType` / `trigger` with the start-node prescription (lines ~1086–1111); node configs are `strictObject` (an invented `recordId` / `fieldValues` / `conditions` key is refused at parse); `automation/save-notice-placement.ts` notices an array `triggerType`; `automation/flow-template-token.ts` refuses `{…}` tokens at save. STATED: objectstack-automation `SKILL.md` § Trigger Types (`record-after-write` = "create OR update — one flow, both events"), § Trigger Configuration, § Flow Variables, § Filter operands, § Valid-but-silently-wrong, line "`current_user != null ? current_user.id : null`"; objectstack-formula `SKILL.md` § 5 (`previous` is `null` on insert) | **enforce-or-remove:** an action node reachable from no edge (orphan) — `flow.zod.ts` refuses dangling edges but not a node no edge reaches; refuse it. **PORT → objectstack-automation, PR 2:** the value-vs-label mapping sentence ("the user speaks the label; the stored value is the option `value` — map it in the condition AND every `fields` entry") is one clause in § Valid-but-silently-wrong if the existing line proves thin | `flow-value-envelope.test.ts` (9 cases) pins the envelope teaching — the spec refusal (`flow-template-token.ts`) and the automation skill's Flow Variables section are its equivalent; the cloud test stays pointing at the moved text | | 14b | time-relative automation: `timeRelative` on the start node; `offsetDays` vs `withinDays`; `filter` INSIDE the descriptor; daily cadence only; write the zone; `runAs: 'system'`; never a date-equality condition; clock-arming honesty | SPEC: `automation/time-relative-trigger.zod.ts` (object + dateField required, exactly one windowing mode, `schedule` / `runAs` misplacement prescriptions). STATED: objectstack-automation `references/examples-flows.md` § Time-relative triggers (offsetDays T-60/30/7 vs withinDays, daily default, "Date EQUALITY never matches … build warns `flow-date-equality-filter`"); `SKILL.md` § Failure routing & `runAs` (a `runAs: 'system'` sweep). CLOUD: the clock-arming warnings (`CLOCK_TRIGGER_*`) | **enforce-or-remove (three):** ① a sibling `filter` beside `timeRelative` on the start config (read by nothing) — refuse in the start node config; ② a `schedule` beside `timeRelative` finer or coarser than daily — refuse (`time-relative-trigger.zod.ts` only documents the default); ③ a `timeRelative` / `schedule` flow with `runAs: 'user'` — refuse at the flow level (today only `.describe()` prose, 122–125). **PORT → objectstack-automation, PR 2:** write the user's wall-clock hour with `timezone` (`schedule: { type: 'cron', expression: '0 9 * * *', timezone: 'Asia/Shanghai' }`), never a silent UTC conversion — spec `describe` gap: `timezone` is not described on the schedule descriptor (`automation/execution.zod.ts` names it only on run state) | `flow-value-envelope.test.ts` "rule 14b writes a time-relative flow value the same way" — covered by the same equivalent as 14 | | 14b-tier | the tier's record-driven way out | CLOUD | restriction profile | — | | 14c | approval is a NODE: `approvers: [{ type, value }]`, approver types, `behavior`; out-edges labelled `approve` / `reject`; `manager` resolves only when the directory is filled; a system write-back needs `runAs: 'system'` downstream | SPEC: `automation/approval.zod.ts` (`ApproverType`, `ORG_MEMBERSHIP_LEVELS`, `ApprovalDecision = approve/reject`, the `manager` `.describe()` + `os lint approval-approvers-may-resolve-empty`). STATED: objectstack-automation `references/state-machines-and-approvals.md` (approver table incl. `org_membership_level` "matches nobody" warning, `behavior` row, labelled edges in the example) | **PORT → objectstack-automation, PR 2:** one sentence — the approval service writes the outcome back as a SYSTEM write, so a record-change flow reacting to it declares `runAs: 'system'` | no cloud text pin; PR 2 eval case | | 15 | a bare `summary` never computes; function from the words; qualifier → `filter`; filter keys on the CHILD, option values; child links back; blueprint derivation; no backfill (`rollup_never_computed`); rate after summary | **E/R:** `data/field.zod.ts` — `type: 'summary'` without `summaryOperations` parses and is runtime-dead (reads 0 everywhere); refuse it (today only `function` without `field` is refused, `refuseSummaryWithoutField`). **E/R:** `summaryOperations.filter` keys judged against the CHILD object — filed for the spec lane as objectstack#31224 D (the child-field half of the sentence left the prose in the review round; the value-vs-label half stays); a summary whose child has no relationship back (`summary_unresolved_relationship`). CLOUD: blueprint `conditions` / `rollupsNeedingOperations`. STATED: `rules/relationships.md` § Roll-up Summaries shape + `relationshipField` auto-detect; `references/lint-rules.md` `rollup/missing-summary` | **PORT → objectstack-data, PR 1 (this PR):** `rules/relationships.md` § Roll-up Summaries — "Pick the function from the words, and put every qualifier in `filter`" (dropped = a plausible wrong total; one child feeds several totals; a rate is a formula over a working summary) and "The recompute runs only on a child write" (the backfill is `os migrate summary-nulls --apply`, `--recompute-undefined-on-empty OBJECT.FIELD` for a summary declared late) | eval `objects-fields-relationships.json` case 12 (must_contain `type: 'summary'`, `function: 'count'`, `filter: { status: 'completed' }`, `type: 'formula'`; must_not_contain the label `已完成` in the filter) | | 16 | the platform `owner_id` already exists — never a hand-rolled 负责人 lookup; `ownership: 'user'` is the default; `ownership: 'none'` for catalogs / junctions | SPEC: `data/object.zod.ts` `ownership` JSDoc (user default, `owner_id` injection, `org` / `none` "catalog / junction tables") | **PORT → objectstack-data, PR 1:** `SKILL.md` § Relationship Patterns row "Record owner" | eval case 13 (must_contain `owner_id`, `sharingModel: 'private'`; must_not_contain a lookup to `sys_user` / `user`, `ownership: 'user'`) | | 16b | `sharingModel` defaulted; override only for a deliberate posture; `controlled_by_parent` constraints | SPEC: `data/controlled-by-parent-master.ts` (exactly one required `master_detail`; refused at parse). STATED: `SKILL.md` § Object Definition — `sharingModel` required at publish (`security-owd-unset`), the four-value table | **PORT → objectstack-data, PR 1:** `SKILL.md` § Object Definition — "Choose the posture from the request, not by habit" (business object → `public_read_write`; sensitive personal data → `private`; reference data → `public_read`; distinct from `ownership`) | eval `hooks-security-seeds-datasources.json` case 16 (must_contain `sharingModel: 'private'` and `'public_read'`; must_not_contain `public_read_write`) | | 17 | a `state_machine` also declares `initialStates` | SPEC: `data/validation.zod.ts` `initialStates` `.describe()` (the mechanic, verbatim). STATED: `rules/validation.md` types table row | **PORT → objectstack-data, PR 1:** `SKILL.md` § Validation Patterns — add it whenever the status has a real start; only genuine entry states, never a terminal one; option values not labels (the UPDATE-only mechanic sentence was dropped in the review round as a class-1 restatement); `rules/validation.md`'s canonical example now declares `initialStates: ['draft']` | eval case 11 (must_contain `type: 'state_machine'`, `initialStates: ['draft']`) | | 18 | never mark a boolean `required`; `defaultValue: false`; "must be accepted" is a validation rule; `storage.notNull` and backfill | **E/R:** `data/field.zod.ts` — `required: true` on a `boolean` / `toggle` parses, can never fire, and a form that reads it as truthy refuses the unchecked state; refuse it at parse with the `defaultValue` prescription. STATED: `SKILL.md` § Schema evolution + `field.zod.ts` `required` / `storage.notNull` `.describe()` (ADR-0113) | **PORT → objectstack-data, PR 1:** `SKILL.md` § Field Type Selection bullet (flag = `{ type: 'boolean', defaultValue: false }`, never `required`; "must be accepted" = `validations[]`; the default lands imported rows on a real `false`) | `boolean-never-required.test.ts` (2 cases, both corpora) → eval case 10 (must_contain `type: 'boolean'`, `defaultValue: false`; must_not_contain `required: true`); the cloud test stays pointing at the moved text | | 19 | sample data fictional, never malformed; producer-side repair | CLOUD (the `create_seed` / `apply_blueprint` repair, `seedRepairs`) | **PORT → objectstack-data, PR 1:** `references/seeds.md` § Seed best practices — "Fictional is fine; malformed is not", with the authoring-time gate named (`os validate` / `os build` refuse a malformed value first, `packages/cli/src/utils/seed-record-values.ts`) | eval `hooks-security-seeds-datasources.json` case 14 (must_contain `defineSeed`, `+86 138`, `@example.com`; must_not_contain `****`, `'N/A'`) | | 19b | a refused `apply_blueprint` built nothing | CLOUD | hosted | — | | 19c | say whether seed rows landed (`drafted` / `published`) | CLOUD | hosted | — | | 19d | a dynamic default fires on SAVE; never promise a form pre-fill | SPEC: `data/field.zod.ts` `defaultValue` `.describe()` ("applied on INSERT") | **PORT → objectstack-data, PR 1:** `SKILL.md` § Field Type Selection bullet — describe it as save-time; the form's pre-display is a client capability you cannot promise | eval case 15 (must_contain `defaultValue: 'current_user'`; must_not_contain `会自动预填`) | | 19e | 「刷新一下」 is a forbidden diagnosis | CLOUD | chat diagnosis | — | | 19f | a board over a just-created group field opens all-empty; the 「未分类」 lane is last; disclose | PORT → objectstack-ui, PR 3 (`rules/list-views.md` beside the `Uncategorized` note, which today covers only an unlisted value: a field added now has no value on any existing row, so every record lands in the trailing lane; adding a field never backfills) | `kanban-new-group-field-disclosure.test.ts` (7 cases) → PR 3 ui eval case; the cloud test stays pointing at the moved text | | 20 | answer in the user's language | CLOUD | chat | — | | 21 | labels follow the user's language | **PORT → objectstack-data, PR 1:** `rules/naming.md` Critical Rules item 6 | | `label-language.test.ts` (2 cases, both corpora) → eval case 14 (must_contain `label: '供应商'`, `label: '公司名称'`; must_not_contain English labels) and case 10 (`label: '是否完成'`); the cloud test stays pointing at the moved text | | 22 | ask when ambiguous | CLOUD | chat | — | ## `solution_design` corpus | id | rule (short) | class | target → spec path / skill file § section / existing text | regression kept | |:--|:--|:--|:--|:--| | plan-first | propose → confirm → apply → verify | CLOUD | orchestration | `authoring-rules.test.ts` inline pins — stay in cloud | | scale-and-scope | bias small, phase big systems | CLOUD | blueprint design (card item 2) | — | | dashboards-second-beat | dashboards only when asked; widget `aggregate` / `measure`; calendar `startDateField` | CLOUD (when to include a dashboard; the blueprint's `aggregate` / `measure` vocabulary is the blueprint schema's — `ui/dashboard.zod.ts` removed `aggregate` / `valueField` from the authorable spec) + SPEC (`ui/view.zod.ts` calendar `startDateField`) + STATED (objectstack-ui `evals/analytics-inline-vs-dataset.json`) | — | — | | hard-rules | mandatory confirm gate; publish state; labels / language | CLOUD; the labels clause = rule 21 | see 21 | `label-language.test.ts` case 1 → eval case 14 | | sample-data | `apply_blueprint` auto-seeds; `seededLive`; real-data import; attached spreadsheet brief | CLOUD | hosted importer and blueprint | — | | select-options | status is a `select` on the main object, never a table; every select ships non-empty `options`; stage-field option `kind` | SPEC: `data/field.zod.ts` `options` — a `select` / `radio` with neither `options` nor a `picklist` is refused at parse; `SelectOption.kind` `.describe()` + `data/object.zod.ts` refuses a `stageField` option without a `kind`. STATED: `rules/field-types.md` Selection table | **PORT → objectstack-data, PR 1:** `SKILL.md` § Relationship Patterns row "Record status / lifecycle" | eval case 11 (must_not_contain a lookup to an `order_status` table) | | process-modeling | roll-ups declared in the blueprint (`conditions`); never objects for approvals / workflows / audit trails; person fields `{ type: 'user' }` for roles other than the owner; checkbox flags never required; people lookups optional; a `lookup` without `reference` | CLOUD (blueprint `conditions`, `rollupsNeedingOperations`); 15 / 18 as above; SPEC: `data/field.zod.ts` relationship `reference` REQUIRED on `lookup` / `master_detail` (refused at parse); STATED: `rules/field-types.md` `user` row (person picker, `sys_user`), eval `objects-fields-relationships.json` case 2 (`type: 'user'`, never a lookup to a `user` object) | **PORT → objectstack-data, PR 1:** `SKILL.md` § Relationship Patterns rows "Approval, review, routing, audit trail" and "Record owner"; § Field Type Selection bullet "A person on the record … optional unless the request says otherwise" | `people-lookups-optional.test.ts` (2 cases) → eval case 15 (must_not_contain `{ type: 'user', required: true }`, a `reference` to `sys_user` / `user`); `boolean-never-required.test.ts` case 1 → eval case 10 | | process-approval-flow | proactively draft the approval flow after the blueprint | CLOUD (orchestration; its shape pointers are 14 / 14c / 17) | — | — | | honest-deferred | an unbuilt rule goes in `assumptions` | CLOUD | blueprint honesty | — | | extend-not-duplicate | `targetApp`, `iterate_or_new` | CLOUD | orchestration | — | | granular-fallback | small change skips the blueprint | CLOUD | orchestration | — | ## Coverage the private rules carry, and where its equivalent is - `skills/__tests__/boolean-never-required.test.ts`, `label-language.test.ts`, `people-lookups-optional.test.ts` pin TEXT of both corpora. Their equivalents are the eval cases above (objects 10, 13, 14; hooks 14–16); the cloud tests stay in cloud pointing at the moved text until card 4 (cloud#3714) rewires the corpus. - `skills/__tests__/flow-value-envelope.test.ts` pins rule 14 / 14b's envelope teaching, half of which is spec-enforced at save (`flow-template-token.ts`) and half stated in objectstack-automation § Flow Variables. Stays in cloud; PR 2 decides whether an automation eval case is still owed once the one PORT clause lands. - `skills/__tests__/kanban-new-group-field-disclosure.test.ts` pins 19f and its reachability from add-view-not-convert — PR 3 (objectstack-ui). - `skills/__tests__/data-explorer-answer-shape.test.ts` pins `data-explorer-skill.ts`, which is outside this card's corpus (an `ask`-side skill). - `__tests__/authoring-rules.test.ts` pins the corpus INTEGRITY (byte-for-byte text, tiers, index lines, version boundary) — delivery machinery, stays in cloud. - `scripts/dev-local/golden-journey.mjs` drives the hosted build journey (signup → build → iterate → edit → boundary) and asserts existence in the DB and HTTP; it exercises no rule text (grep for rule ids / topics: zero). Stays in cloud. The `evals/` harness (`agent-e2e`, structural score, hosted-chat) scores produced metadata, not rule text — stays in cloud. ## Enforce-or-remove list for the seat (spec lane) 1. `data/field.zod.ts`: `type: 'summary'` without `summaryOperations` parses and is runtime-dead (rule 15). 2. `data/field.zod.ts`: `required: true` on a `boolean` / `toggle` parses and can never fire; a truthy-reading form turns it into "must be true" (rule 18). 3. `automation/flow.zod.ts`: an action node reachable from no edge parses and never runs (rule 14, "edges connect everything"). 4. `automation/builtin-node-config.zod.ts` start node: a `filter` beside `timeRelative` is read by nothing (rule 14b) — verify the start config refuses the sibling key; if it does, this row is SPEC. 5. `automation/time-relative-trigger.zod.ts` / the start `schedule`: a cadence finer or coarser than daily beside `timeRelative` re-fires or skips thresholds (rule 14b). 6. `automation/flow.zod.ts`: a `timeRelative` / `schedule` flow with the default `runAs: 'user'` has no identity and its data steps are refused at run time (rule 14b) — today `.describe()` prose only. 7. `data/field.zod.ts` / `os validate`: `summaryOperations.filter` keys and select values judged against the CHILD object; a summary whose child has no relationship back (rule 15). 8. `ui/dashboard.zod.ts` / `os validate`: a `dimensions` entry over a multi-value field is no chart axis (rule 2) — verify. 9. Spec describe gap: the schedule descriptor's `timezone` key (rule 14b) — described only on run state in `automation/execution.zod.ts`. ## Conflicts with spec on `main` None found: every ported sentence agrees with `packages/spec` at 8ef4d30. Two cloud spellings are the blueprint schema's, not the spec's, and were NOT ported: `summaryOperations.conditions: [{ field, op, value }]` (spec: `filter`, a where-map) and dashboard widget `aggregate` / `measure` (spec removed `aggregate` / `valueField` from the authorable dashboard surface). ## Tests - Eval fixtures are JSON the maintainers score; they parse (`python3 -c "json.load"` over both files; ids contiguous and unique — objects 1–14, hooks 1–16 — mine after `main`'s). No test pins the ported prose at a layer closer than the eval — the cloud text pins stay in cloud pointing at the moved text (table above). - No runtime behaviour changed, so no ablation is owed (docs and fixtures only). ## Acceptance notes - Contract review 6711429709 (FAIL on 0fb4902) — every must-fix and should-fix is in this head: `origin/main` merged (merge commit 9bed005, no rebase); the eval conflicts resolved by renumbering mine after `main`'s (and again at 8fdef4f when `main` landed hooks cases 12–13 under #31201); `rules/relationships.md` re-measured and re-pinned against `main`'s re-locked 15489 bytes; the "No backfill" paragraph and eval 12 rewritten around `os migrate summary-nulls`; the class-1 `initialStates` mechanic sentence dropped and the canonical example given `initialStates: ['draft']`; the child-field half of the `filter` sentence withdrawn (enforce-or-remove, #31224 D); the `os validate` / `os build` clause in `references/seeds.md`; the sharing-posture examples aligned; this table's numbers corrected. - Example alignment, two deliberate exceptions: `project` in `rules/relationships.md` stays `private` because `main`'s new eval case 8 pins `project` as `private`; `employee` stays `private` as personal data — both are the posture paragraph's own cases, not habit. - The seat's refinement narrows the card's item 1 ("port … the stages-clean-but-runtime-dead traps"): traps are NOT written as prose here; they are the enforce-or-remove list in the mapping table, filed for the spec lane as #31224. Recorded as a deviation from the card's wording, by the dispatcher's order. - Not ported, by class: the enforce-or-remove rows and 1 describe gap for the seat; 2 cloud spellings that are the blueprint schema's, not the spec's (`summaryOperations.conditions`, dashboard `aggregate` / `measure`). - Later PRs on this card: objectstack-automation (14 value-vs-label clause, 14b `timezone`, 14c system write-back) and objectstack-ui (add-view-not-convert, 13 action-not-view + inert body, 19f empty board disclosure, auto-surfaced "never downgrade a named view kind"). - Observed on `main` while rebuilding: after merging `main` into a worktree installed before the merge, `@objectstack/lint` did not build (`Cannot find module '@objectstack/metadata-core'`) until `pnpm install` was re-run — the AGENTS.md §9 refresh, not a defect; noted so the next reader does not file it. ## Verification Local subset on head 901533a (merge of `origin/main` 7c523aa at 2d6a84d, then one commit); the current head 370ddbb adds only a GitHub-reported-conflict merge of `origin/main` 007fdb1 (8fdef4f, one eval fixture renumbered) and the changeset's matching id line, re-checked in scope per AGENTS.md §10 — token ratchet :: exit 0 (the four rows unchanged at 6665 / 4127 / 1747 / 2761), `check:role-word` :: exit 0, CJK scan 0 lines, both eval fixtures parse with contiguous ids (1–14, 1–16): every family `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derived for this diff — 64 families; exit codes captured before any pipe; `--ran` reconciliation: `✓ dispatch-gates --ran: 64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).`. - `node scripts/check-skills-token-ratchet.mjs` :: exit 0 — `skills/objectstack-data/SKILL.md is 6665 tokens (ceiling 6665; headroom 0)`, `rules/relationships.md is 4127 tokens (ceiling 4127; headroom 0)`, `references/seeds.md is 1747 tokens (ceiling 1747; headroom 0)`, `rules/validation.md is 2761 tokens (ceiling 2761; headroom 0)`; `--self-test` :: exit 0. - `pnpm check:role-word` :: exit 0. `pnpm --filter @objectstack/spec run check:skill-docs` :: exit 0 · `check:skill-refs` :: exit 0. `pnpm check:dts-closure` :: exit 0, `pnpm --filter @objectstack/lint run check:doc-formula-expressions` :: exit 0, `pnpm check:lean-entry-closure` :: exit 0 — each after the prerequisite build under the shared verify lock (`turbo run build --filter=@objectstack/spec... --filter=@objectstack/formula... --filter=@objectstack/lint... --filter=@objectstack/objectql...`, `VERDICT command-exit 0 · held the lock 126s`). - `pnpm check:pm-dispatch-gates` :: exit 0 (run with `nohup` + `tail --pid`). - The other 56 derived families (changeset gates, closing-keyword parity, nul-bytes, governed-merges prose, skill-compatibility, skill-frame-sync, skill-identifier-liveness, published-files, declaration mirrors, self-test wiring, ratchet-remedy-authority, …) :: exit 0 each. - NOT MEASURED locally: `pnpm check:dual-build-cjs-loads` :: exit 3 — PREREQUISITE NOT MET, it loads every package's `dist/` (a full `pnpm build`, which CI owns). Read from the PR's `Lint & Repo Gates` check-run. - CJK scan of every non-eval `skills/**` file this PR touches (Python, the CJK Unified / punctuation / full-width ranges): 0 lines on this head. Every family CI runs on the PR is read from its check-runs (`Lint & Repo Gates`, `TypeScript Type Check`, `Test Core`, `Build Core`, `Governed Surface Queue Guard`, the ui lanes), not re-run here; at report time they are `in_progress` on 370ddbb. Not run, with the reason: `test:repo` (CI's `Test Core`; the diff changes no test file). No `dogfood` battery: no `!` in the title and no `(narrowing)` arm. No ablation: no tested runtime behaviour changed. --- _Generated by [Claude Code](https://claude.ai/code/session_012dn2AdARJtc4RgABPH7PHk)_ <!-- signed: repo:cloud#1 · os-bob · session_012dn2AdARJtc4RgABPH7PHk --> --------- Co-authored-by: os-bob <os-bob@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
1 parent 0647dab commit 0e13561

9 files changed

Lines changed: 294 additions & 32 deletions

File tree

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@objectstack/skills': patch
3+
---
4+
5+
docs(skills): objectstack-data carries the metadata-authoring judgment the hosted build agent learned — booleans, people fields, dynamic defaults, process modelling, roll-up qualifiers, seed values, labels, sharing posture
6+
7+
Clause-②: no
8+
Metadata-protocol: none
9+
10+
Card 1 of the cloud#3659 ruling A split: one source of metadata-authoring knowledge for every build channel. This is the `objectstack-data` port — the judgment the schema cannot express and the published skill did not yet state. `SKILL.md`: a yes/no flag is `{ type: 'boolean', defaultValue: false }` and never `required` ("must be accepted" is a validation rule); a person on the record is `type: 'user'`, optional unless asked; a dynamic default resolves on save and is described as such; record status is a `select` with a `state_machine`, never a status table; approvals, routing and audit trails are not objects; the record owner is the platform `owner_id`, never a hand-added lookup, and `ownership: 'none'` is only for catalogs and junctions; the `sharingModel` posture is chosen from the request; a `state_machine` declares `initialStates` whenever the status has a real start. `rules/relationships.md`: pick the roll-up function from the words, put every qualifier in `summaryOperations.filter` (dropped, it is a plausible wrong total), a rate is a formula over a working summary, and the recompute runs only on a child write — a summary declared after its rows exist is backfilled by `os migrate summary-nulls --apply`; its examples take the posture the paragraph teaches. `rules/validation.md`: the canonical `state_machine` example declares `initialStates`. `rules/naming.md`: labels follow the user's language. `references/seeds.md`: seed values may be fictional, never malformed — `os validate` / `os build` refuse a malformed one first. Eight eval cases pin the teachings (objects-fields-relationships 10–14, hooks-security-seeds-datasources 14–16).

‎scripts/check-skills-token-ratchet.mjs‎

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -539,7 +539,33 @@ export const CEILINGS = new Map([
539539
// 6123 -> 6078: re-locked at the landed count. The rule index's `rules/lifecycle.md`
540540
// entry names the save notice (+16); the `lifecycle` property row dropped the
541541
// bounding-policy clause that entry and `rules/lifecycle.md` already carry (-25).
542-
['skills/objectstack-data/SKILL.md', 6078],
542+
// 6078 -> 6665 (#31155, card 1 of the cloud#3659 ruling-A split): the
543+
// metadata-authoring judgment the hosted build agent carried privately, now
544+
// the one published source — the `sharingModel` posture chosen from the
545+
// request; a yes/no flag is `defaultValue: false` and never `required`; a
546+
// person on the record is `type: 'user'`, optional unless asked; a dynamic
547+
// default resolves on save; three Relationship Patterns rows (status is a
548+
// `select`, never a table; approvals and audit trails are not objects; the
549+
// owner is the platform `owner_id`, `ownership: 'none'` only for catalogs and
550+
// junctions); a `state_machine` declares `initialStates`. Paid down first,
551+
// two restatements in this file: the `unique`-validation blockquote restated
552+
// `rules/validation.md` § Uniqueness and is a one-line pointer now; the
553+
// `secret` / `user` parentheticals restated `rules/field-types.md` rows 42-43
554+
// and 96 and are gone; the contract-review round then cut the `initialStates`
555+
// mechanic sentence (a restatement of `validation.zod.ts`'s describe) and
556+
// aligned the conditional-rules `invoice` example with the posture paragraph;
557+
// the approver's round then put the two example phrases into English (the
558+
// shipped skill text is English-only; eval prompts are exempt).
559+
// Arithmetic in priced bytes (stamp line excluded), against `main`'s 24308:
560+
// 24308 -> 26658 (+2350 net) -> ceil(26658 / 4) = 6665; headroom was 1
561+
// token, so the ceiling moves +587, pinned AT the measurement. Authorised by
562+
// the ruling of record that orders this text into the published skills:
563+
// objectstack-ai/cloud#3659, letter A, maintainer 「A」 (ruling comment
564+
// 6682024726, 2026-11-06): "How to write valid, working metadata lives in the
565+
// public skills … The private corpus's rules of that kind move there". The
566+
// raise path is the standing 2026-10-27 ruling, verbatim and untranslated:
567+
// 「A:先削重复,余额按实测提高」 (ruling record 6513306068).
568+
['skills/objectstack-data/SKILL.md', 6665],
543569
['skills/objectstack-formula/SKILL.md', 5997], // -53 (was 6055), then -5 A2′ (was 6002)
544570
['skills/objectstack-i18n/SKILL.md', 6333], // -11 (was 6349), then -5 A2′ (was 6338)
545571
// 12705 -> 12984 (2026-08-31 app-repo-principles raise, see the block above).
@@ -750,7 +776,30 @@ export const CEILINGS = new Map([
750776
// +201 tokens on a landed 3672, 4 absorbed by headroom, ceiling +197. No
751777
// repeated passage to cut first (measured: no sentence repeats). Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
752778
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
753-
['skills/objectstack-data/rules/relationships.md', 3873],
779+
// 3873 -> 4127 (#31155, card 1 of the cloud#3659 ruling-A split): the
780+
// Roll-up Summaries `filter` paragraph is REWRITTEN, not appended to — pick
781+
// the function from the words, every qualifier goes in `filter` (dropped, a
782+
// plausible wrong total), one child feeds several totals, a rate is a
783+
// formula over a working summary; and the recompute runs only on a child
784+
// write, so a summary declared after its rows exist is backfilled by
785+
// `os migrate summary-nulls --apply`, never by editing a child. Five example
786+
// objects take the posture the new SKILL.md paragraph teaches
787+
// (`public_read_write` on account / contact / invoice / the junction,
788+
// `public_read` on the category table). Paid down first: the `engagement`
789+
// signup/click example and the "aggregate only a subset" lead-in (both
790+
// restated by the kept `procurement_receipt` example), the `filter` code
791+
// comment's second line (the paragraph says it), and the child-field half of
792+
// the `filter` sentence (an enforce-or-remove row for the spec lane,
793+
// #31224 D, not prose). The approver's round then put the roll-up passage's
794+
// example phrases into English (the shipped skill text is English-only).
795+
// Arithmetic in bytes against `main`'s re-locked 15489:
796+
// 15489 -> 16505 (+1016 net) -> ceil(16505 / 4) = 4127; headroom was 0, so
797+
// the ceiling moves +254, pinned AT the measurement. Authorised by the ruling
798+
// of record that orders this text into the published skills:
799+
// objectstack-ai/cloud#3659, letter A, maintainer 「A」 (ruling comment
800+
// 6682024726); raise path the 2026-10-27 ruling 「A:先削重复,余额按实测提高」
801+
// (ruling record 6513306068).
802+
['skills/objectstack-data/rules/relationships.md', 4127],
754803
// NEW FILE (#14296 item 1 = A, condition (b)): the entry's Security & Access
755804
// Control block moved here whole. Pinned at its landed count — no headroom,
756805
// because a split that arrives with budget is a raise wearing a new path.
@@ -821,7 +870,17 @@ export const CEILINGS = new Map([
821870
// 3109 -> 2756: RE-LOCK at the landed count (#19715, card item 4). A 5-line
822871
// table of contents (+155 tokens) landed inside the row's 508 tokens of
823872
// headroom, so no raise; pinned AT the measurement — zero headroom.
824-
['skills/objectstack-data/rules/validation.md', 2756],
873+
// 2756 -> 2761 (#31155): the canonical `state_machine` example gains
874+
// `initialStates: ['draft']` so the example no longer undercuts the rule
875+
// SKILL.md § Validation Patterns teaches (ordered by the contract review on
876+
// PR #31223). Paid down first: the "(removed from the spec)" aside in the
877+
// no-`unique` list item, restated by § Uniqueness below it (-24 bytes).
878+
// Arithmetic in bytes: 10992 -> 11044 (+52 net) -> ceil(11044 / 4) = 2761;
879+
// headroom was 8 tokens, so the ceiling moves +5, pinned AT the
880+
// measurement. Same authority as the three rows above: cloud#3659 letter A
881+
// (ruling comment 6682024726); raise path 「A:先削重复,余额按实测提高」
882+
// (ruling record 6513306068).
883+
['skills/objectstack-data/rules/validation.md', 2761],
825884

826885
// objectstack-platform
827886
['skills/objectstack-platform/references/plugin-hooks.md', 2628],
@@ -1099,7 +1158,20 @@ export const CEILINGS = new Map([
10991158
// tokens at headroom 0, ceiling +267. No repeated passage to cut first
11001159
// (measured: no sentence repeats). Maintainer ruling 2026-10-27 on the decision card 「skills 修复卡的教学和 eval 都撞 token 天
11011160
// 花板,怎么付?」, tap A, verbatim and untranslated: 「A:先削重复,余额按实测提高」.
1102-
['skills/objectstack-data/references/seeds.md', 1566],
1161+
// 1566 -> 1747 (#31155, card 1 of the cloud#3659 ruling-A split): one
1162+
// paragraph under Seed best practices — a seed row passes the same field
1163+
// validation as a user's insert, so a masked or placeholder value never
1164+
// lands and the table loads short; `os validate` / `os build` refuse it
1165+
// first, at the record's field; invent a well-formed fictional value,
1166+
// never downgrade the field type. No repeated passage to cut first
1167+
// (measured: the file said nothing about seed values). Arithmetic in bytes:
1168+
// 6240 -> 6985 (+745) -> ceil(6985 / 4) = 1747; headroom was 6 tokens, so
1169+
// the ceiling moves +181, pinned AT the measurement. Authorised by the ruling
1170+
// of record that orders this text into the published skills:
1171+
// objectstack-ai/cloud#3659, letter A, maintainer 「A」 (ruling comment
1172+
// 6682024726); raise path the 2026-10-27 ruling 「A:先削重复,余额按实测提高」
1173+
// (ruling record 6513306068).
1174+
['skills/objectstack-data/references/seeds.md', 1747],
11031175
['skills/objectstack-platform/references/bootstrap.md', 1351],
11041176
// 3108 -> 3287: `os deploy`, the managed-package dev loop — when to deploy
11051177
// rather than publish a release, its entry points, how it picks the

‎skills/objectstack-data/SKILL.md‎

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,14 @@ the publish door rejects it before that:
7575
| `public_read_write` | everyone reads and writes |
7676
| `controlled_by_parent` | inherited from the master record (`master_detail` children); a row with no (optional) master is its owner's |
7777

78+
Choose the posture from the request, not by habit: a business object a team
79+
shares is `public_read_write` (`private` would hide every colleague's records
80+
from everyone else); `private` fits personal or sensitive data only its owner
81+
should see (HR reviews, pay, personal notes); `public_read` fits reference data
82+
everyone reads and the owner maintains (price lists, policies, notices). This
83+
axis is distinct from `ownership`, which decides whether `owner_id` exists at
84+
all (Relationship Patterns below).
85+
7886
**Important optional properties:**
7987

8088
| Property | Default | Description |
@@ -175,7 +183,7 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
175183

176184
export const Invoice = ObjectSchema.create({
177185
name: 'invoice',
178-
sharingModel: 'private',
186+
sharingModel: 'public_read_write',
179187
fields: {
180188
status: Field.select({
181189
options: [
@@ -275,11 +283,25 @@ lowercase option values — incorrect/correct examples: [rules/naming.md](./rule
275283
### Field Type Selection
276284

277285
The full table, per-type config and traps: [rules/field-types.md](./rules/field-types.md).
278-
From here, three to get right: credentials are `secret` (reversible, **encrypted-at-rest**
279-
via the registered `ICryptoProvider`, masked on read, fail-closed, ADR-0100) — never
280-
`password`, which on a generic object is **plaintext at rest**; `user` is a person picker
281-
(a lookup specialized to `sys_user`, stored identically to `lookup`); `formula` takes CEL in
282-
`expression` (`F\`...\`` from `@objectstack/spec`; see **objectstack-formula**).
286+
From here, three to get right: credentials are `secret` (**encrypted-at-rest**, ADR-0100) —
287+
never `password`, which on a generic object is **plaintext at rest**; `user` is the person
288+
picker; `formula` takes CEL in `expression` (`F\`...\`` from `@objectstack/spec`; see
289+
**objectstack-formula**).
290+
291+
- A yes/no flag (`is_*`, `has_*`, "done?", "active?") is `{ type: 'boolean', defaultValue: false }`
292+
and never `required`: a checkbox always holds one of its two values, so
293+
`required` can only read as "must be true", and a form that reads it that way
294+
refuses the unchecked state — the one state a new record starts in. "Must be
295+
accepted" (terms, a confirmation) is a `validations[]` rule, not `required`.
296+
The default also lands seeded or imported rows that omit the column on a real
297+
`false` instead of a null that only looks unchecked.
298+
- A person on the record (assignee, approver, reviewer) is `type: 'user'` with
299+
no `reference`, and optional unless the request says otherwise — the platform
300+
already stamps the owner and `created_by`.
301+
- A dynamic default (`defaultValue: 'current_user'`, `'NOW()'`, a CEL envelope)
302+
resolves when the record is SAVED with the field empty. Describe it as
303+
save-time; whether a create form pre-displays it is a client capability you
304+
cannot promise.
283305

284306
### Relationship Patterns
285307

@@ -290,6 +312,9 @@ via the registered `ICryptoProvider`, masked on read, fail-closed, ADR-0100) —
290312
| Many-to-Many (simple) | multi-value `lookup` (`multiple: true`) — an **array column** of ids |
291313
| Many-to-Many (with attributes) | Junction object with two `lookup` fields |
292314
| Hierarchical | `tree` field (self-reference) |
315+
| Record status / lifecycle | a `select` on the business object (plus a `state_machine` validation) — never a separate status / stage / step table |
316+
| Approval, review, routing, audit trail | not objects: an approval is a flow node (**objectstack-automation**) and its trail is platform history; model only the people it references (approver, reviewer), as `user` fields beside the owner |
317+
| Record owner | the platform's `owner_id` (`ownership: 'user'`, the default) — never a hand-added "owner" / "person in charge" lookup, a second owner that owner-scoped visibility and "My records" views never read; `ownership: 'none'` only for a shared catalog or a pure junction table |
293318

294319
> **`multiple: true` lookup ≠ junction object.** A multi-value lookup
295320
> (`{ type: 'lookup', reference: 'x', multiple: true }`) is stored and read as an
@@ -309,10 +334,14 @@ via the registered `ICryptoProvider`, masked on read, fail-closed, ADR-0100) —
309334
The **complete** set of types (`ValidationRuleSchema` discriminators): `script`,
310335
`state_machine`, `format`, `cross_field`, `json_schema`, `conditional`.
311336

312-
> **There is NO `unique` validation type** (removed from the spec).
313-
> Enforce uniqueness — including composite — with a **unique index**, and state
314-
> its scope (ADR-0120):
315-
> `indexes: [{ fields: ['department', 'email'], unique: 'organization' }]`.
337+
A `state_machine` rule also declares `initialStates` whenever the status has a
338+
real starting point — without it a record can be created already `approved` /
339+
`paid` and walk past the process. List only the genuine entry states (usually
340+
one: `draft` / `pending` / `new`), never a terminal one; keys and values are
341+
option `value`s, never labels.
342+
343+
> **There is NO `unique` validation type** — uniqueness is a unique **index**
344+
> with its scope (ADR-0120; [rules/validation.md § Uniqueness](./rules/validation.md#uniqueness--use-unique-indexes)).
316345
317346
See [rules/validation.md](./rules/validation.md) for all types and examples.
318347

‎skills/objectstack-data/evals/hooks-security-seeds-datasources.json‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,36 @@
130130
"must_contain": ["condition:", "record.annual_revenue != null", "record.annual_revenue > 1000000"],
131131
"must_not_contain": []
132132
}
133+
},
134+
{
135+
"id": 14,
136+
"prompt": "Seed eight demo members for the gym app. For privacy, mask the phone numbers like `138****4207` and put `N/A` where we have no email.",
137+
"expected_output": "A `defineSeed(Member, { env: ['dev', 'test'], records: [...] })` whose every value is fictional but WELL-FORMED: `+86 138 0013 8000`-style phones, `liyy@example.com`-style emails, option VALUES for selects. Explains that a seed row passes the same field validation as a user's insert, so a masked phone or an `N/A` email is refused and never lands — the table loads short or empty while bookings point at members that do not exist. Does not downgrade the `phone` field to `text` to make the rows load.",
138+
"files": [],
139+
"assertions": {
140+
"must_contain": ["defineSeed", "+86 138", "@example.com"],
141+
"must_not_contain": ["****", "'N/A'", "type: 'text', label: 'Phone'"]
142+
}
143+
},
144+
{
145+
"id": 15,
146+
"prompt": "报销单:申请人默认是当前用户,还有一个审批人和一个复核人。",
147+
"expected_output": "`applicant`, `approver` and `reviewer` are `type: 'user'` fields (the person picker — no `reference`, never a `lookup` to a user object you invent); `applicant` carries `defaultValue: 'current_user'`; none is `required`, since the request does not ask for it and the platform already stamps the owner and `created_by`. Describes the default as SAVE-time — 「保存后申请人会自动填为当前用户;表单里这一栏可能显示为空,留空提交即可」 — and does not promise the create form pre-fills it.",
148+
"files": [],
149+
"assertions": {
150+
"must_contain": ["type: 'user'", "defaultValue: 'current_user'"],
151+
"must_not_contain": ["reference: 'sys_user'", "reference: 'user'", "{ type: 'user', required: true }", "会自动预填"]
152+
}
153+
},
154+
{
155+
"id": 16,
156+
"prompt": "Two objects: performance reviews that only the reviewed person's manager and HR may see, and a price list everyone can read but only finance maintains.",
157+
"expected_output": "`performance_review` is `sharingModel: 'private'` (owner-only by default, widened to the manager and HR with sharing rules / RLS in a permission set, rules/security.md); `price_list` is `sharingModel: 'public_read'` (everyone reads, the owner writes) with finance as its owners. Neither is `public_read_write`, the posture for an ordinary shared business object — the posture is chosen from the request, not by habit. No `permissions` key on the object.",
158+
"files": [],
159+
"assertions": {
160+
"must_contain": ["sharingModel: 'private'", "sharingModel: 'public_read'"],
161+
"must_not_contain": ["sharingModel: 'public_read_write'", "permissions:"]
162+
}
133163
}
134164
]
135165
}

0 commit comments

Comments
 (0)