Skip to content

skills(objectstack-api): teach import applyOn, read FLOW_FAILED's innerRefusal, and the anonymous-form file refusal - #26392

Merged
os-zhuang merged 1 commit into
mainfrom
claude/project-thread-i1b4v5
Oct 27, 2026
Merged

os-zhuang merged 1 commit into
mainfrom
claude/project-thread-i1b4v5

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
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。
  • 为什么改:skills: audit what the skills teach against the protocol since 18.0 — (A) published skills/ vs every added, changed or retired authorable key; (B) where pm-dispatch requires the skills/docs follow-up #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

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)
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
`--ran` reconciliation
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).
Token ratchet before and after the raise
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).

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (no schema touched)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; eslint passes on the one script touched (pnpm lint is CI's run).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 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)
  • 6. packages/spec touched: its generated artifacts are regenerated and committed. (not touched)
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English (the 维护者速读 block and the quoted ruling are the repo's own Chinese conventions).
  • The Before Submitting list in CONTRIBUTING.md is done.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbaQ2Qeznx5DrAfybLP

…erRefusal`, and the anonymous-form file refusal

Three teachings the skills gap audit measured missing from the published
objectstack-api package, each in the section the card names:

- the import body's `mapping` entry form carries `applyOn` (`always` /
  `create`), so a re-run upsert leaves an admin's edit alone;
- a flow door's `400 FLOW_FAILED` details are `FlowFailureDetailsSchema`
  with `innerRefusal`, and the order a client renders them in;
- a form open to anonymous intake declares no file field: the gating lint
  rule, the publish gate, and the submit door's refusal for older forms.

One eval fixture for the import. The eval file's token ceiling rises by
the measured size of the fixture under the maintainer's ruling quoted in
the ratchet row.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JQwxbaQ2Qeznx5DrAfybLP
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 27, 2026
@github-actions github-actions Bot added size/s documentation Improvements or additions to documentation labels Oct 27, 2026
@os-zhuang
os-zhuang marked this pull request as ready for review October 27, 2026 01:52
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Reviewed at 2026-10-27T02:06Z · PR #26392 · card #26299 · independent contract review at the ceiling tier, read-only, adversarial to the dispatch order, the seat's claim and the dev's report alike.

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 3add67c79c287ff30ecc3e921c93959a9908624a
Local-runs: none
Scope: skills/objectstack-api/SKILL.md, skills/objectstack-api/evals/endpoints-auth-routes.json + scripts/check-skills-token-ratchet.mjs, packages/rest/src/import-prepare.ts, packages/core/src/utils/import-runner.ts, packages/spec/src/api/export.zod.ts, packages/spec/src/data/column-apply-on.zod.ts, packages/spec/src/api/automation-api.zod.ts, packages/spec/src/api/contract.zod.ts, packages/runtime/src/domains/automation.ts, packages/runtime/src/endpoint-executor.ts, packages/runtime/CHANGELOG.md, packages/lint/src/validate-public-form-file-fields.ts, packages/lint/src/authoring-rules.ts, packages/lint/src/runtime-gate.ts, packages/spec/src/data/field-value.zod.ts, packages/rest/src/form-routes.ts, packages/types/src/validation-failure.ts, .claude/agents/os-dev.md, .github/PULL_REQUEST_TEMPLATE.md, scripts/pm/clause2-line.mjs, .github/workflows/lint.yml

Landing state as read. A GOVERNED_APPROVERS account approved twice at 2026-10-27T01:52Z on this head and armed auto-merge (merge method merge); at 2026-10-27T02:03Z the PR was still open, merged: false, mergeable_state: blocked (waiting on checks). This record is owed on the head regardless and is rendered from git show 3add67c7:PATH against origin/main at abb90460, never from the shared checkout. The PR's recorded base abb90460 is not an ancestor of the head: the merge-base is 756220c4, and main moved from 756220c4 to abb90460 on none of the three files this PR touches (git diff --stat 756220c4 abb90460 -- scripts/check-skills-token-ratchet.mjs skills/objectstack-api/ is empty), so the three-file diff below is the whole PR and the dev's "read off main at 756220c" readings hold on the live tip.

Check-runs on 3add67c7 (read twice, last at 2026-10-27T02:03Z, not waited for): 51 runs, 34 success, 16 skipped (UI, docs, dogfood-regression and opt-in lanes the path filter turned off), 1 in_progress (TypeScript Type Check). Every gate this PR can move is green: Type Check · source gates (the lane that runs node scripts/check-skills-token-ratchet.mjs and its --self-test, check:ratchet-remedy-authority, check:skill-docs and check:skill-refs — read off lint.yml lines 5326, 5458, 5582, 5589–5590 under job typecheck-source-gates, not the "Lint · repo & tooling gates" lane the sibling brief assumed), Type Check · consumer gates (runs check:skill-examples, line 6463, job typecheck-consumers) — success at 2026-10-27T01:56Z and later, Lint · ESLint, Lint · repo & tooling gates, Lint · docs & code guards, Lint · PM dispatch-gates, Check Changeset, Governed Surface Queue Guard, Part-of PR must not also close its card, The card this PR closes must claim this branch, No other open PR may claim the same issue, Build Core and Test Core 1–8. Nothing red.

① Derived judgments

(a) Every new sentence is true of the spec and the implementation on origin/main. Three hunks, 27 added lines in SKILL.md, each read line by line:

  1. Public-form file field (SKILL.md lines 104–111, inside "Public (anonymous) Form Endpoints", which the card's SKILL.md:75-100 names). RIGHT on every clause. FILE_REFERENCE_TYPES is image, file, avatar, video, audio (field-value.zod.ts:306-308). The rule id is the exported constant PUBLIC_FORM_FILE_FIELD = 'public-form-file-field'; its registry row is tier: 'gating', commands: ALL where AUTHORING_COMMANDS = ['validate', 'build', 'lint'] (authoring-rules.ts:167, 462, 1584-1593) — so "at os validate / os build / os lint" is exact. "And at the view's publish": surfaces: CLI_AND_RUNTIME with runtimeTypes: ['view'], and runtime-gate.ts:584-592 selects rules whose surfaces include runtime-publish for the written type — the registry comment states the view's publish is refused at the key and an object write is deliberately not covered; the skill's sentence claims only the view's publish, which is what holds. "A visitor has no upload door to mint a sys_file id" is the rule's own header wording. The submit-door sentence: form-routes.ts:650-663 pushes { field, code: 'invalid_value', message, label } for every file-typed field carrying a non-empty value and throws validationFailure(...), whose constructor (packages/types/src/validation-failure.ts:76-82) is what both dispatcher exits map to 400 VALIDATION_FAILED + details.fields[]; the file's own comments at 585 and 616 call this "the flat 400 VALIDATION_FAILED with fields[]", so "flat" is the code's term, not the dev's. The qualifier "a form published before that rule" is the only population that can still carry a file field after the rule, and the refusal fires on any such submission. The prescription "collect the file on a signed-in form instead" is ruling C's own route as the rule header records it.

  2. Import applyOn (SKILL.md lines 269–275, directly after the derived-verbs table whose import row is the card's SKILL.md:253 neighbourhood). RIGHT. ImportMappingSchema is the union of the { Column: field } record and z.array(FieldMappingEntrySchema) (export.zod.ts:241-245); applyOn: ColumnApplyOnSchema.default('always') sits on the entry only (export.zod.ts:174; the enum is ['create', 'always'], column-apply-on.zod.ts). The door: prepareImportRequest reads applyOn only in the Array.isArray(body.mapping) branch, treats undefined as always, and returns { status: 400, code: 'INVALID_REQUEST' } for any value outside the pair (import-prepare.ts:610-627); both routes (data-action-routes.ts:369, :483) call it on the raw body with no prior Zod parse that could answer first, so "any other value is 400 INVALID_REQUEST" is exactly what the door answers. The record form silently ignores applyOn (it keeps only string values), so "only the entry form carries applyOn" is right. "A re-run upsert refreshes the always columns and leaves a value an admin set since": import-runner.ts:690-694 builds the update patch as if (!createOnly.has(k) || keySet.includes(k)) patch[k] = v. "A create-only column is still read for matching": true — findExisting reads data[f] for every key-set field regardless of applyOn — and the runner additionally still WRITES a create-only column on update when it belongs to the matched key set (the || keySet.includes(k) arm). The skill's sentence is true as written; it leaves that corner unsaid, which is a scope choice, not a falsehood (an author who marks a match key create-only gets the key written back unchanged).

  3. FLOW_FAILED details (SKILL.md lines 372–380, inside "Error Envelopes & the Code Ledger"). RIGHT. FlowFailureDetailsSchema is exactly errorMessage? ("the flow author's own failure text"), summary? (FlowRunSummarySchema, "which node failed") and innerRefusal? (automation-api.zod.ts:585-602); FlowInnerRefusalSchema is { code: ErrorCode, message: string } with code "in its WIRE spelling (e.g. UNIQUE_VIOLATION)" (lines 334–344). The three example codes the skill lists are the ones the runtime CHANGELOG names for the node catch arms. The doors: the trigger route is POST /api/v1/automation/:name/trigger (automation-api.zod.ts:43; the skill's {name} spelling matches the file's own /data/{object}/import convention), and the dispatch table at automation.ts:2041-2048 / endpoint-executor.ts:476-480 answers "ran and failed" with 400 FLOW_FAILED at the trigger, resume, /actions and declared-endpoint doors (the "409 at the trigger door" line in endpoint-executor.ts:487 is the history the table replaced, not the current answer). The render order userMessage → details.innerRefusal → message is NOT the dev's invention: it is the CHANGELOG's own "For consumers" sentence on the entry that landed innerRefusal ("render a refused run the way a record form renders a refused save: the top-level userMessage when present, then details.innerRefusal, then message"), and userMessage is ApiErrorSchema.userMessage (contract.zod.ts:93). "Absent keys mean the run produced none — never invent an empty" is the schema's own "absent means absent, never an invented empty".

(b) Cuts. None. The diff removes nothing from the package (0 deletions in SKILL.md and the eval file; references/_index.md byte-identical, 3200 bytes). The card names no stale lines and the base package has 0 hits for applyOn, innerRefusal, FLOW_FAILED and file field (git grep -c on 756220c4), so no duplicate existed to cut; ruling A's "削重复 first" is satisfied vacuously and the PR body says so.

(c) Ratchet row. The script's measure is Math.ceil(Buffer.byteLength(text, 'utf8') / 4) with one exclusion, the indented release-stamp line of a SKILL.md (check-skills-token-ratchet.mjs:49, 77-113). Recomputed by hand over the head blobs (a byte count, not a run): endpoints-auth-routes.json is 4936 bytes on the head → ceil(4936/4) = 1234, and 4156 bytes on the base → 1039; the new row is 1234 — pinned AT the measurement, zero headroom, no smuggled room. The row's arithmetic checks: +780 bytes = 4936 − 4156; +192 = 1234 − 1042; the 3 absorbed tokens = 1042 − 1039. SKILL.md is 20532 bytes on the head; minus the 20-byte stamp line → 20512/4 = 5128, under the untouched ceiling 6314 (base 18782 − 20 → 4691), so no second row moves and none should. The raise rule (header lines 128–130) requires "a PR whose body quotes a maintainer ruling authorizing it"; the script itself reads no PR body, so the evidence is the body and the row comment, and both quote the tap 「A:先削重复,余额按实测提高」 verbatim with the card's title, the date and the per-file before/after. The row comment follows the shape of the 2026-08-31 precedent block (ruling quoted untranslated, paid-down-first statement, arithmetic) and sits inside the CEILINGS map, not between the precedent block and the SHRINK-ONLY doc comment that check-ratchet-remedy-authority anchors on (placement warning at lines 294–301); that gate ran green in Type Check · source gates. No eslint max-len rule exists anywhere in the repo's config (0 hits), and Lint · ESLint is green, so the long comment lines are not a lint risk. The "0 duplicate bytes" claim, judged on the eval file itself: fixtures 1–5 are five distinct prompts (declarative endpoint, anonymous endpoint budget, webhook gap, auth providers, apiMethods derivation) and no sentence recurs between them; the only phrase shared with anything is "the flat public + permissions pair" in fixture 4, which the SKILL.md also carries at line 322 — a different file, and the ratchet prices per file. The claim holds.

(d) Eval. One fixture, id 6, ids 1–6 unique and sequential, the file's existing shape (prompt / expected_output / files: [] / assertions). Every must_contain token (applyOn, 'create', sourceField, targetField) appears in expected_output; must_not_contain: ["writeMode: 'insert'"] is consistent with the expected writeMode: 'upsert' and with the import door's semantics. The expected output's mechanics match the runner (written on create, left out of the patch on a match, others default always). The card-specific acceptance asks one eval for the import and that is the one present; the shared Acceptance's "every new teaching gets an eval prompt" is met for the import teaching only — the FLOW_FAILED and public-form-file teachings carry no eval. The card's own text resolves that conflict in the dev's favour (the card-specific line "one eval for the import" is the narrower, later instruction, the dispatch order says "the card asks one eval, for the import", and the dev's report declares the scope), so this is a boundary note, not a failure. The package has no evals README or index counting prompts (three files in the tree), so nothing else needs updating.

(e) Wanted lines met, nothing beyond. All three Wanted items land in the named sections (verified above). Line 1 of SKILL.md (version frontmatter, PR #25640's line) is unchanged — the first five lines of base and head are identical. No file outside the claim + amendment surface: the three paths are skills/objectstack-api/SKILL.md, skills/objectstack-api/evals/endpoints-auth-routes.json and the one CEILINGS row in scripts/check-skills-token-ratchet.mjs. Line readings claimed in the body are exact: SKILL.md 417 → 444, eval 55 → 65, _index.md 46 → 46, package 463 → 490.

(f) PR body shape. Line 1 is the harness attribution HTML comment, line 2 the harness's _Requested by_ line, line 3 Fixes #26299, lines 4–5 Clause-②: no and Metadata-protocol: none, each line-initial and each occurring exactly once. os-dev.md on main (line 323) exempts the harness-written attribution comment on line 1 ("原样留、只报") and wants Fixes "紧随其后"; the harness wrote two lines, and Fixes follows that block on line 3. clause2-line.mjs's reader is CLAUSE2_KEY_LINE = /^[ \t]*...Clause-②...:(.*)$/ (line-initial, any line of the body), so the declaration lines are read wherever they sit; the template puts them under ## Verification, os-dev.md after Fixes, and this body satisfies both readers with one pair. The CI checks that parse the closing keyword (The card this PR closes must claim this branch, Part-of PR must not also close its card, No other open PR may claim the same issue) are green on the head, which is the proof the attribution comment did not hide Fixes from any parser. ## 维护者速读 carries the six labels in order; 需要你做的 names one action (approve / Merge when ready — right for Tier H); 先看 names three hunks with line numbers; 两分钟验证 is one command with the expected sight, and the sight it promises (SKILL.md 5128 / 6314, endpoints-auth-routes.json 1234 / 1234 (+0)) is the dev's paraphrase of the script's report lines, whose real wording the body's own details block quotes (... is 5128 tokens (ceiling 6314; headroom 1186) / ... is 1234 tokens (ceiling 1234; headroom 0)) — a reader grepping the output finds the numbers, so the verification works as written. English sections What changes / Tests / Acceptance notes / Verification are present with readings folded into details blocks; the extra ## Why and ## Human Minimum come from .github/PULL_REQUEST_TEMPLATE.md (landed on main at 756220c4, after dispatch); os-dev.md's 英文节序 line names an order and forbids no extra section, and the dev named the conflict rather than picking silently. The commit's trailer pair is model-free. The body reports no model name.

(g) Report. os-dev-report 6431775093 carries issue, status: done, branch: claude/project-thread-i1b4v5 (= the claim's branch), pr, session, premise_still_valid, summary, tests, gates, dogfood_battery: n/a (no !, no narrowing arm — right), mcp_calls: 0, api_writes: 3 with endpoints, line_budget, deviations (7), open_questions: [], out_of_scope_findings (2). files_changed / rules_retired are absent, and os-dev.md's JSON template on main (lines 362–384) does not list them, so nothing is missing. One note: the report's summary and first deviation name the served models by id; the dispatch order's ban reads "no model name anywhere in the commit or PR", and both the commit message and the PR body are clean — the card comment is outside the ban's letter, so this is a process note, not a defect.

② Semver level

skip-changeset is right: skills/objectstack-api/ carries no package.json (tree: SKILL.md, evals/endpoints-auth-routes.json, references/_index.md), scripts/** publishes no npm package, and Check Changeset is green on the head. Clause-②: no and Metadata-protocol: none are correct — the skill teaches keys the protocol already carries (FieldMappingEntry.applyOn, FlowFailureDetails, the lint rule), no accept set moves. Both declaration lines are line-initial and the value is the first thing after the colon.

③ Boundary flags

  • Dev's out-of-scope note 1 — SeedColumn.applyOn (same ColumnApplyOnSchema, seed.zod.ts) is objectstack-data's surface and is untaught there: confirmed, git grep -c applyOn origin/main -- skills/objectstack-data/ is 0 hits. That belongs to the skills(objectstack-data): teach membership keys, ownerMayEdit, create-only seed columns, the position-to-set binding and the external role, and fix the reference: 'user' example #26297 (objectstack-data) card's dev or a follow-up card on that package; it is a 收班简报 note for the seat, not this PR's debt.
  • Dev's out-of-scope note 2 — the premise read looked for the anonymous-form file refusal in packages/spec/src/api while it lives in packages/lint + packages/rest/src/form-routes.ts: confirmed by reading; a process note for the seat's premise-read recipe, nothing to file.
  • Eval coverage — only the import teaching has an eval (card-specific acceptance), while the shared Acceptance's "every new teaching gets an eval" would want two more (FLOW_FAILED details, public-form file field). The card's own narrower line and the dispatch order settle this PR; the seat may want to carry "evals for API2/API3" as a note for a later ratchet-paying card. Not a defect of this head.
  • PR-template vs os-dev.md — the template's declaration-lines-under-Verification layout and os-dev.md's after-Fixes layout are both satisfied by one pair here, and the template's ## Why / ## Human Minimum are extra sections os-dev.md does not forbid. The two documents should converge in os-dev.md, and the harness's two-line attribution block (comment + _Requested by_) should be named in os-dev.md line 323, which today reads as one line. 收班简报 note; nothing for this PR.
  • Lane naming — the ratchet, check:ratchet-remedy-authority, check:skill-docs and check:skill-refs run in Type Check · source gates, and check:skill-examples in Type Check · consumer gates, not in the "Lint · …" lanes the reviewer briefs assumed. A note for the brief template, not for this PR.
  • Base reconciliation — base abb90460 vs merge-base 756220c4: main moved on none of the three touched files, the ratchet row on main still reads 1042, so the merge carries exactly this diff.

Implemented-by: claude/project-thread-i1b4v5
Reviewed-by: session_01LqKsGyMNFwwnbF5FTAqyPn

VERDICT: PASS

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

维护者速读

改了什么:skills/objectstack-api 补三段教学,各落在卡点名的章节:公开表单段加「匿名表单不声明文件字段」(lint 规则 public-form-file-field、发布门、旧表单提交时的 400 VALIDATION_FAILED);派生操作表后加导入 mapping 条目的 applyOn: 'always' | 'create'(仅条目数组形态;其它值 400 INVALID_REQUEST);错误信封段加流程门 400 FLOW_FAILED 的 details(errorMessage / summary / innerRefusal)与渲染顺序。eval 加一题导入;该 eval 文件天花板按你的裁决 A 实测抬 1042 → 1234(五条夹具无一句重复可削,正文已说明)。什么都没删。
为什么改:审计 § objectstack-api 三行均「未教、无 eval」:写导入作业的 AI 作者每次重跑 upsert 都会覆盖管理员手改的列;客户端只能显示「flow failed」;公开表单作者会声明访客永远填不了的文件字段。
风险与回滚:只改发布技能文本与一条棘轮行,无运行时代码;每句都对着 main 的 spec/实现核过;回滚 = revert 本 PR。
先看:① SKILL.md 三个 hunk(行 104–111、269–275、372–380);② eval 第 6 题;③ 棘轮 endpoints-auth-routes.json 行。
两分钟验证:node scripts/check-skills-token-ratchet.mjs | grep objectstack-api 预期 SKILL.md 5128 / 6314、endpoints-auth-routes.json 1234 / 1234。
需要你做的:无——os-zhuang 已 approve + ready + 入队(01:52Z),合入即落地。
席位意见:同意落地。两处留作后续(不挡本 PR):导入里「仅建时写」的列若同时在匹配键集里,更新时仍会写入(runner || keySet.includes(k)),技能只说「仍参与匹配」;FLOW_FAILED 与公开表单两段还没有 eval(卡只要求导入那一条)。

席位核对

ACCEPT · domain:skills#2 · session_01LqKsGyMNFwwnbF5FTAqyPn · 2026-10-27T02:10Z — seat review of record for #26299 on head 3add67c7, against GitHub, not the report's own account. Published surface skills/** ⇒ Tier H: the record of this tier is the independent ## Contract review at comment 6431952702 (Served-tier: CONTRACT_REVIEW_TIER, head 3add67c7, VERDICT: PASS, Local-runs: none; drafted by a reviewer other than the implementer, the card thread's dev). This seat adds the landing read. Order of events, reported: the governed approver's hand ran ahead of this record (see 6).

  1. Shape. Base main, mergeable_state: clean; body lines 1–2 the harness attribution block, line 3 Fixes #26299 (os-dev.md 323's exemption; the closing-keyword guards green), Clause-②: no, Metadata-protocol: none line-initial (kind none ⇒ no Skills-docs: line owed under the rule landed in b33dd25b); ## 维护者速读 six labels in order, then What changes / Tests / Acceptance notes / Verification with folds. Files: SKILL.md (+27), evals/endpoints-auth-routes.json (+10), scripts/check-skills-token-ratchet.mjs (+18/−1, one row) — the claim's surface (6430933607) as amended at 2026-10-27T01:26Z (+ the package's CEILINGS rows under ruling A); line 1 of SKILL.md untouched. Labels documentation, size/s, skip-changeset — right, no package.json under skills/objectstack-api. Branch claude/project-thread-i1b4v5, named by the claim. Model: the thread switched to the ceiling tier on the maintainer's own words before its first edit (reported in the os-dev-report).
  2. Record, measured. All three teachings true of the spec/implementation on origin/main: applyOn (export.zod.ts:174, column-apply-on.zod.ts, import-prepare.ts:610–627 → 400 INVALID_REQUEST; runner import-runner.ts:690–694); FLOW_FAILED details (automation-api.zod.ts:585–602, inner refusal 334–344; the render order is the runtime CHANGELOG's own sentence; doors per automation.ts:2041–2048 / endpoint-executor.ts:476–480); public-form file field (lint rule public-form-file-field, publish gate; form-routes.ts:650–663 invalid_value → 400 VALIDATION_FAILED, 「flat」 the code's own word). Ratchet: ceil(4936/4) = 1234 = the row, headroom 0; SKILL.md 5128 of 6314 untouched; 0 duplicate bytes holds. Eval 6 well-formed, ids 1–6 sequential. Line counts 417→444, 55→65, 46→46, package 463→490. Base reconciled: merge-base 756220c4, main moved on none of the three files. The seat re-read the diff and reads the same.
  3. Gates, pinned to head 3add67c7. 51 check-runs on the head: 34 success, 16 skipped, 1 in progress (TypeScript Type Check) at 2026-10-27T02:03Z; Type Check · source gates (ratchet, skill-docs, skill-refs) and Type Check · consumer gates (skill-examples) success. Lane map read off today's lint.yml: the token ratchet, check:skill-docs and check:skill-refs run in Type Check · source gates; check:skill-examples in Type Check · consumer gates.
  4. Accepted deviations. (a) Surface widened to the CEILINGS rows — the seat's own amendment under ruling A. (b) The card's cited commits unreachable in the shallow clone; spec lines cited instead. (c) check:dual-build-cjs-loads NOT MEASURED locally (whole-tree build prerequisite), CI's run green. (d) Fixes on line 3 under the harness block.
  5. Carrier. Nothing to file. 收班简报: SeedColumn.applyOn untaught in skills/objectstack-data (route to skills(objectstack-data): teach membership keys, ownerMayEdit, create-only seed columns, the position-to-set binding and the external role, and fix the reference: 'user' example #26297); API2/API3 have no eval; os-dev.md 323 names a one-line attribution exemption while the harness writes two lines; the os-dev-report named served models by id (the ban covers commit and PR; extend to reports?).
  6. Landing. os-zhuang approved twice, readied and armed at 2026-10-27T01:52Z; the PR sits in the merge queue — ⛔ nothing for the seat to ready, arm or enqueue. On MERGED the seat closes the card with the landing record.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/s skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

skills(objectstack-api): teach import applyOn, read FLOW_FAILED's innerRefusal, and the anonymous-form file refusal

2 participants