Repository navigation
Commit a66c9ff
skills(objectstack-api): teach import
<!-- claude-projects-attribution: {"github_login":"os-warren"} -->
_Requested by **Warren** · [project
thread](https://claude.ai/code/project/chan_01U5RdnAAmkSQmZbqh6FtqZ8?thread=cmsg_01U5RdnAAmkSQmZbqh6FtqZ83Exa2w6Et9nBGUexBDabLx)_
Fixes #26299
Clause-②: no
Metadata-protocol: none
## 维护者速读
- 改了什么:对外发布的 `skills/objectstack-api` 补三段教学,各落在卡点名的章节:`SKILL.md`
公开表单段加「匿名表单不声明文件字段」(lint 规则 `public-form-file-field`、发布门、旧表单提交时的 `400
VALIDATION_FAILED`);派生操作表后加导入 `mapping` 条目的 `applyOn: 'always' |
'create'`;错误信封段加流程门的 `400 FLOW_FAILED` 的
`details`(`FlowFailureDetailsSchema`:`errorMessage` / `summary` /
`innerRefusal`)与渲染顺序。`evals/endpoints-auth-routes.json` 加一题导入 eval;该
eval 文件在 `scripts/check-skills-token-ratchet.mjs` 的天花板行按实测抬 1042 → 1234。
- 为什么改:#26184 技能缺口审计 § objectstack-api 三行(API1–API3)均为「未教、无 eval」:写导入作业的
AI 作者每次重跑 upsert 都会覆盖管理员手改的列;客户端作者只能显示「flow
failed」而不是真正的拒收原因;公开表单作者会声明一个访客永远填不了的文件字段。
- 风险与回滚:只改已发布技能文本与一条棘轮行,无运行时代码;每句教学都对照 `main` 上的 spec 与实现逐行核过(见
Acceptance notes)。回滚 = revert 本 PR。
- 先看:① `skills/objectstack-api/SKILL.md` 三个 hunk(行
104–111、269–275、372–380);②
`skills/objectstack-api/evals/endpoints-auth-routes.json` 新第 6 题;③
`scripts/check-skills-token-ratchet.mjs` 的 `endpoints-auth-routes.json`
行(1042 → 1234,引你 2026-10-27 的裁决)。
- 两分钟验证:`node scripts/check-skills-token-ratchet.mjs | grep
objectstack-api` — 预期 `SKILL.md 5128 / 6314`、`endpoints-auth-routes.json
1234 / 1234 (+0)`。
- 需要你做的:Tier H(`skills/**`):审阅通过后 approve,或直接 Merge when ready。
## What changes
Three teachings the skills gap audit (#26184, Part A § objectstack-api,
rows API1–API3) measured missing from the published `objectstack-api`
package, each placed in the section the card names, in the fewest lines
an AI author needs:
1. **Public (anonymous) Form Endpoints** — a form open to anonymous
intake declares no file field: a section field whose object type is in
`FILE_REFERENCE_TYPES` (`image`, `file`, `avatar`, `video`, `audio`) is
refused by the gating lint rule `public-form-file-field` at `os
validate` / `os build` / `os lint` and at the view's publish; a form
published before that rule meets the same refusal at submit (flat `400
VALIDATION_FAILED`, `fields[].code: 'invalid_value'`); the prescription
is a signed-in form.
2. **API Methods (Operations)** — after the derived-verbs table: `POST
/data/{object}/import` takes `mapping` as `{ Column: field }` or as
`FieldMappingEntry[]`, and only the entry form carries `applyOn`
(`'always'` default; `'create'` writes the column only when the row is
created, so a re-run `upsert` leaves an admin's edit alone; a
create-only column is still read for matching; any other value is `400
INVALID_REQUEST`).
3. **Error Envelopes & the Code Ledger** — a flow door's `400
FLOW_FAILED` carries `error.details` shaped by
`FlowFailureDetailsSchema` (`errorMessage?`, `summary?`, `innerRefusal?`
as `{ code, message }` in the wire spelling), and a client renders a
refused run as a record form renders a refused save: top-level
`userMessage`, then `details.innerRefusal`, then `message`; absent keys
mean the run produced none.
One eval fixture (id 6) for the import `applyOn` in
`evals/endpoints-auth-routes.json`, in the file's existing shape.
**Token ceiling (ruling A).** `scripts/check-skills-token-ratchet.mjs`
prices the eval file shrink-only at 1042 with 3 tokens of headroom on
`main`. The maintainer ruled on the seat's decision card 「skills 修复卡的教学和
eval 都撞 token 天花板,怎么付?」 with the tap, verbatim and untranslated,
「A:先削重复,余额按实测提高」. Paid down first: the file's five fixtures share no
sentence, so there was no duplicate to cut (0 bytes). The fixture adds
+780 bytes (4156 → 4936 bytes, 1039 → 1234 tokens); the 3 tokens of
headroom are absorbed and the row moves 1042 → 1234 (+192), pinned AT
the measurement with zero headroom. `SKILL.md` fits inside its existing
ceiling (4691 → 5128 of 6314) and its row does not move. The file
surface widened to that one ratchet row under the seat's amended claim.
**Line readings** (os-dev.md 290–291, whole file before → after):
| File | Before | After | Δ |
|:--|--:|--:|--:|
| `skills/objectstack-api/SKILL.md` | 417 | 444 | +27 (three paragraphs
of 8 / 7 / 9 lines, each with its blank separator) |
| `skills/objectstack-api/evals/endpoints-auth-routes.json` | 55 | 65 |
+10 |
| `skills/objectstack-api/references/_index.md` (generator-owned,
untouched) | 46 | 46 | 0 |
| **Package** (`SKILL.md` + `references/**/*.md` + `rules/**/*.md`) |
**463** | **490** | **+27** |
Token readings in the ratchet's convention (`ceil(utf8 bytes / 4)`):
`SKILL.md` 4691 → 5128 (ceiling 6314); eval file 1039 → 1234 (ceiling
1042 → 1234). Nothing is removed: the card names no stale lines, and no
line in the package duplicated what the three paragraphs teach (0 hits
on `main` for `applyOn`, `innerRefusal`, `FLOW_FAILED`, `file field`).
## Why
The audit's acceptance is 「one fix card per skill package」; this is the
objectstack-api card. Who pays today: an AI author of import jobs, who
overwrites a column an admin owns on every re-import; a client author,
who shows "flow failed" instead of the real reason; a public-form
author, who declares a field no visitor can fill and meets the refusal
only at publish.
## Tests
- `pnpm --filter @objectstack/spec check:skill-docs` — exit 0 (`✅ Skill
docs in sync`; frontmatter untouched).
- `pnpm --filter @objectstack/spec check:skill-refs` — exit 0 (`✅ 9
generated files in sync`; `references/_index.md` is generator-owned and
unchanged — no `gen:skill-refs` needed).
- `pnpm --filter @objectstack/spec check:skill-examples` — exit 0 after
`pnpm --filter @objectstack/spec build` and `pnpm --filter
@objectstack/client-react... build` under the verify lock (✅ self-test:
flags a bare `any` parameter / variable / property / return / alias /
cast in a ✅ 264 prose examples type-check across 3 surface(s) — every
marked block parsed, so tsc ran the SEMANTIC pass on all of them); the
first run exited 3 (PREREQUISITE NOT MET: no
`packages/client-react/dist` declarations) and measured nothing (needs a
built `packages/spec/dist`; the diff adds no `os:check` block).
- `node scripts/check-skills-token-ratchet.mjs` — exit 0 after the raise
(`81 authored bundle file(s) within their ceilings`); before the raise
it refused exactly this file (`1234 tokens; the ratchet ceiling is 1042
(over by 192)`), which is the reading the row quotes. `--self-test`: 79
cases pass.
- `pnpm check:skill-identifier-liveness` — exit 0 (Leg 1: 490 citations
over 69 files; Leg 2: 8 registered sections, 0 gaps). `pnpm
check:skill-top-level-keys`, `check:skill-compatibility`,
`check:skill-frame-sync` — exit 0.
- `npx eslint --no-inline-config scripts/check-skills-token-ratchet.mjs`
— exit 0; `pnpm check:nul-bytes` — exit 0.
- The eval fixture parses (`JSON.parse`), and its `must_contain` tokens
appear in its own `expected_output`.
- No package build or `pnpm test` is owed: the diff touches no
`packages/**` source (local-verification ①/② empty); the one script
edited is a gate whose own suite is its `--self-test` (run above).
## Acceptance notes
Every sentence is read off `main` at `756220c4`, not off the card's
paraphrase:
- `applyOn` — `packages/spec/src/api/export.zod.ts:174`
(`FieldMappingEntrySchema.applyOn:
ColumnApplyOnSchema.default('always')`),
`packages/spec/src/data/column-apply-on.zod.ts` (`create | always`); the
import door reads it only from the entry-array form of `mapping`
(`packages/rest/src/import-prepare.ts:610–627`, `createOnlyFields`), the
record shorthand cannot carry it, and a value outside the pair is `400
INVALID_REQUEST` (same file, 620–626); the runner leaves create-only
targets out of the update patch
(`packages/core/src/utils/import-runner.ts:246`).
- `FLOW_FAILED` — `packages/spec/src/api/automation-api.zod.ts:585–602`
(`FlowFailureDetailsSchema`: `errorMessage?`, `summary?`,
`innerRefusal?`), `FlowInnerRefusalSchema` (`code` in the wire spelling,
`message`); the envelope's top-level `userMessage` is
`ApiErrorSchema.userMessage`
(`packages/spec/src/api/contract.zod.ts:93`); the doors (trigger,
resume, `/actions`, declared `type: 'flow'` endpoints) and the render
order `userMessage` → `details.innerRefusal` → `message` are the runtime
CHANGELOG's own consumer note for that change
(`packages/runtime/CHANGELOG.md:52–63`).
- Anonymous-form file field —
`packages/lint/src/validate-public-form-file-fields.ts` (rule id
`public-form-file-field`, `FILE_REFERENCE_TYPES` = `image`, `file`,
`avatar`, `video`, `audio` from
`packages/spec/src/data/field-value.zod.ts:306`), wired `tier:
'gating'`, `commands: ALL`, `surfaces: CLI_AND_RUNTIME`, `runtimeTypes:
['view']` in `packages/lint/src/authoring-rules.ts:1574–1596`; the
submit door's refusal for forms published before the rule is
`packages/rest/src/form-routes.ts:650–663` (`code: 'invalid_value'`,
folded into the flat `400 VALIDATION_FAILED` with `fields[]`). The
seat's premise read did not locate this in `packages/spec/src/api`; it
lives in `@objectstack/lint`, so the paragraph names the lint rule
rather than a spec symbol.
- Scope held to the card's 「Wanted」: three paragraphs, one eval, one
ratchet row. Not taught (deliberately, outside the Wanted): the seed
dataset's `SeedColumn.applyOn` twin (objectstack-data's surface), the
resume door's `ResumeFailureDetailsSchema` keys, and the `errorMessage`
/ `summary` keys beyond naming them.
- The commit's trailer pair is model-free as the pre-push hook requires;
the harness-written attribution lines at the top of this body are kept
as the platform wrote them.
- PR body layout: `.github/PULL_REQUEST_TEMPLATE.md` landed on `main`
after this card was dispatched (`756220c4`); this body carries the
os-dev.md section order (`Fixes` / declaration lines / `## 维护者速读` / `##
What changes` / `## Tests` / `## Acceptance notes` / `## Verification`)
and adds the template's `## Why` and `## Human Minimum` so every
template heading is present.
## Verification
<details><summary>Derived gate list and local run (`node
scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` on `3add67c7`; exit codes captured before
any pipe)</summary>
| Command | Exit | Reading |
|:--|--:|:--|
| `node scripts/check-ci-filter-parity.mjs` | 0 | pass |
| `node scripts/check-closing-keyword-parity.mjs` | 0 | pass |
| `node scripts/check-closing-keyword-parity.mjs --self-test` | 0 | pass
|
| `node scripts/check-comment-mask-corpus.mjs` | 0 | pass |
| `node scripts/check-declaration-mirrors.mjs` | 0 | pass |
| `node scripts/check-declaration-mirrors.mjs --self-test` | 0 | pass |
| `node scripts/check-doc-route-spelling.mjs --advisory` | 0 | pass |
| `node scripts/check-doc-route-spelling.mjs --self-test` | 0 | pass |
| `node scripts/check-dts-emitted.mjs --self-test` | 0 | pass |
| `node scripts/check-scripts-symbol-anchors.mjs` | 0 | pass |
| `node scripts/check-scripts-symbol-anchors.mjs --self-test` | 0 | pass
|
| `node scripts/check-self-test-wired.mjs` | 0 | pass |
| `node scripts/check-self-test-wired.mjs --self-test` | 0 | pass |
| `node scripts/check-self-test-workflow-commands.mjs` | 0 | pass |
| `node scripts/check-self-test-workflow-commands.mjs --self-test` | 0 |
pass |
| `node scripts/check-skills-token-ratchet.mjs` | 0 | pass |
| `node scripts/check-skills-token-ratchet.mjs --self-test` | 0 | pass |
| `node scripts/check-whole-set-label-write.mjs` | 0 | pass |
| `node scripts/check-whole-set-label-write.mjs --self-test` | 0 | pass
|
| `node scripts/ci/assert-turbo-remote-cache.mjs --self-test` | 0 | pass
|
| `node scripts/ci/prune-turbo-cache.mjs --self-test` | 0 | pass |
| `node scripts/pm/bare-root-worklist.mjs --self-test` | 0 | pass |
| `pnpm --filter @objectstack/lint run check:doc-formula-expressions` |
0 | pass |
| `pnpm --filter @objectstack/spec run check:skill-docs` | 0 | pass |
| `pnpm check:agent-test-spelling` | 0 | pass |
| `pnpm check:bash32-floor` | 0 | pass |
| `pnpm check:cli-command-ids` | 0 | pass |
| `pnpm check:corpus-claim-drift` | 0 | pass |
| `pnpm check:cross-package-test-inputs` | 0 | pass |
| `pnpm check:doc-authoring` | 0 | pass |
| `pnpm check:driver-memory-census` | 0 | pass |
| `pnpm check:dts-closure` | 0 | pass |
| `pnpm check:dual-build-cjs-loads` | 3 | NOT MEASURED — PREREQUISITE
NOT MET: 33 dual-build packages have no dist/ (needs a repo-wide `pnpm
build`; the diff touches no package) — CI's run is the reading |
| `pnpm check:entry-guard` | 0 | pass |
| `pnpm check:gitlink-declared` | 0 | pass |
| `pnpm check:lean-entry-closure` | 0 | pass |
| `pnpm check:merge-driver` | 0 | pass |
| `pnpm check:nul-bytes` | 0 | pass |
| `pnpm check:parse-guard` | 0 | pass |
| `pnpm check:pm-dispatch-gates` | 0 | run detached (fast tier, 1901
cases); verdict: ✓ dispatch-gates self-test (fast tier): 1901 cases
pass; 7 slow section(s) deferred. |
| `pnpm check:pm-governed-merges` | 0 | pass |
| `pnpm check:pm-skill-id-lint` | 0 | pass |
| `pnpm check:pnpm-filter-targets` | 0 | pass |
| `pnpm check:pnpm-flag-spellings` | 0 | pass |
| `pnpm check:ratchet-remedy-authority` | 0 | pass |
| `pnpm check:refd-timer-probe` | 0 | pass |
| `pnpm check:role-word` | 0 | pass |
| `pnpm check:skill-compatibility` | 0 | pass |
| `pnpm check:skill-frame-sync` | 0 | pass |
| `pnpm check:skill-identifier-liveness` | 0 | pass |
| `pnpm check:sourcemap-no-sources-content` | 0 | pass |
| `pnpm check:watch-hint-literal` | 0 | pass |
</details>
<details><summary>`--ran` reconciliation</summary>
```
Run reconciliation — 52 derived, 51 run, 1 NOT-MEASURED, 0 UNRUN.
- pnpm check:dual-build-cjs-loads [recorded exit 3 on line 33 — PREREQUISITE NOT MET]
✓ dispatch-gates --ran: 52 derived famil(ies) accounted for — 51 run, 1 NOT-MEASURED (1 DERIVED from a recorded exit 3).
```
</details>
<details><summary>Token ratchet before and after the raise</summary>
```
before: ✗ check-skills-token-ratchet: skills/objectstack-api/evals/endpoints-auth-routes.json is 1234 tokens; the ratchet ceiling is 1042 (over by 192). The published c…
after: ✓ check-skills-token-ratchet: skills/objectstack-api/SKILL.md is 5128 tokens (ceiling 6314; headroom 1186). Its release-stamp line (`version: "18.1.0"`, 20 bytes) is written by the version script, not an author, and is not priced.
after: ✓ check-skills-token-ratchet: skills/objectstack-api/evals/endpoints-auth-routes.json is 1234 tokens (ceiling 1234; headroom 0).
```
</details>
## Human Minimum
- [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic.
(no schema touched)
- [x] 2. Every name I added uses the casing for its kind.
- [x] 3. No formatter ran; eslint passes on the one script touched
(`pnpm lint` is CI's run).
- [x] 4. One issue, one branch off an up-to-date `main`, one pull
request.
- [x] 5. A change that publishes carries a changeset, and both
declaration lines are filled in here and in the changeset. (`skills/**`
carries no `package.json`; nothing is released → `skip-changeset`; the
declaration lines are above)
- [x] 6. `packages/spec` touched: its generated artifacts are
regenerated and committed. (not touched)
- [x] 7. No hand edits to `content/docs/references/`,
`packages/*/CHANGELOG.md` or `content/docs/releases/`.
- [x] 8. Title, body and comments are in English (the 维护者速读 block and
the quoted ruling are the repo's own Chinese conventions).
- [x] The *Before Submitting* list in CONTRIBUTING.md is done.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_01JQwxbaQ2Qeznx5DrAfybLP
Co-authored-by: objectstack-fleet[bot] <332303061+objectstack-fleet[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>applyOn, read FLOW_FAILED's innerRefusal, and the anonymous-form file refusal (#26392)1 parent 7c70f91 commit a66c9ff
3 files changed
Lines changed: 55 additions & 1 deletion
File tree
- scripts
- skills/objectstack-api
- evals
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
658 | 658 | | |
659 | 659 | | |
660 | 660 | | |
661 | | - | |
| 661 | + | |
| 662 | + | |
| 663 | + | |
| 664 | + | |
| 665 | + | |
| 666 | + | |
| 667 | + | |
| 668 | + | |
| 669 | + | |
| 670 | + | |
| 671 | + | |
| 672 | + | |
| 673 | + | |
| 674 | + | |
| 675 | + | |
| 676 | + | |
| 677 | + | |
| 678 | + | |
662 | 679 | | |
663 | 680 | | |
664 | 681 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
101 | 101 | | |
102 | 102 | | |
103 | 103 | | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
104 | 113 | | |
105 | 114 | | |
106 | 115 | | |
| |||
257 | 266 | | |
258 | 267 | | |
259 | 268 | | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
260 | 277 | | |
261 | 278 | | |
262 | 279 | | |
| |||
352 | 369 | | |
353 | 370 | | |
354 | 371 | | |
| 372 | + | |
| 373 | + | |
| 374 | + | |
| 375 | + | |
| 376 | + | |
| 377 | + | |
| 378 | + | |
| 379 | + | |
| 380 | + | |
| 381 | + | |
355 | 382 | | |
356 | 383 | | |
357 | 384 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
53 | 63 | | |
54 | 64 | | |
55 | 65 | | |
0 commit comments