Repository navigation
Commit 0e13561
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
- .changeset
- scripts
- skills/objectstack-data
- evals
- references
- rules
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
539 | 539 | | |
540 | 540 | | |
541 | 541 | | |
542 | | - | |
| 542 | + | |
| 543 | + | |
| 544 | + | |
| 545 | + | |
| 546 | + | |
| 547 | + | |
| 548 | + | |
| 549 | + | |
| 550 | + | |
| 551 | + | |
| 552 | + | |
| 553 | + | |
| 554 | + | |
| 555 | + | |
| 556 | + | |
| 557 | + | |
| 558 | + | |
| 559 | + | |
| 560 | + | |
| 561 | + | |
| 562 | + | |
| 563 | + | |
| 564 | + | |
| 565 | + | |
| 566 | + | |
| 567 | + | |
| 568 | + | |
543 | 569 | | |
544 | 570 | | |
545 | 571 | | |
| |||
750 | 776 | | |
751 | 777 | | |
752 | 778 | | |
753 | | - | |
| 779 | + | |
| 780 | + | |
| 781 | + | |
| 782 | + | |
| 783 | + | |
| 784 | + | |
| 785 | + | |
| 786 | + | |
| 787 | + | |
| 788 | + | |
| 789 | + | |
| 790 | + | |
| 791 | + | |
| 792 | + | |
| 793 | + | |
| 794 | + | |
| 795 | + | |
| 796 | + | |
| 797 | + | |
| 798 | + | |
| 799 | + | |
| 800 | + | |
| 801 | + | |
| 802 | + | |
754 | 803 | | |
755 | 804 | | |
756 | 805 | | |
| |||
821 | 870 | | |
822 | 871 | | |
823 | 872 | | |
824 | | - | |
| 873 | + | |
| 874 | + | |
| 875 | + | |
| 876 | + | |
| 877 | + | |
| 878 | + | |
| 879 | + | |
| 880 | + | |
| 881 | + | |
| 882 | + | |
| 883 | + | |
825 | 884 | | |
826 | 885 | | |
827 | 886 | | |
| |||
1099 | 1158 | | |
1100 | 1159 | | |
1101 | 1160 | | |
1102 | | - | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
1103 | 1175 | | |
1104 | 1176 | | |
1105 | 1177 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
75 | 75 | | |
76 | 76 | | |
77 | 77 | | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
78 | 86 | | |
79 | 87 | | |
80 | 88 | | |
| |||
175 | 183 | | |
176 | 184 | | |
177 | 185 | | |
178 | | - | |
| 186 | + | |
179 | 187 | | |
180 | 188 | | |
181 | 189 | | |
| |||
275 | 283 | | |
276 | 284 | | |
277 | 285 | | |
278 | | - | |
279 | | - | |
280 | | - | |
281 | | - | |
282 | | - | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
283 | 305 | | |
284 | 306 | | |
285 | 307 | | |
| |||
290 | 312 | | |
291 | 313 | | |
292 | 314 | | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
293 | 318 | | |
294 | 319 | | |
295 | 320 | | |
| |||
309 | 334 | | |
310 | 335 | | |
311 | 336 | | |
312 | | - | |
313 | | - | |
314 | | - | |
315 | | - | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
316 | 345 | | |
317 | 346 | | |
318 | 347 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
133 | 163 | | |
134 | 164 | | |
135 | 165 | | |
0 commit comments