From 5b868af7a5c2d51aac66dde3a7a1fa7a0baae17a Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 01:02:38 +0000 Subject: [PATCH 1/2] docs(skills): land the app-repo working principles in the published catalog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Encode the maintainer's 2026-08-31 metadata-app principles on the surface the ruling names — the published `skills/` catalog — plus the PM decision-analysis recommendation-order clause they imply. - platform: a new "App / Platform Boundary" section — what an app IS, where a capability gap gets fixed, and what a platform defect obliges (wait for the fix; no workaround, no half-landing; verify the pin and re-run the repro before resuming). - ui: the section escape-hatch ladder (derive -> group reference -> hand enumeration, last) in Record Presentation, and the docs rule that a doc explains business concepts rather than hand-copying a machine inventory. The file's two worked `sections` examples now reference a declared group instead of enumerating members, so the page stops teaching the rung it demotes. - data: the invariant-vs-transition-gate choice beside `requiredWhen`, and blocking-rests-on-a-human-judgement beside the severity levels. - automation: keep a screen flow at `runAs: 'user'` and move an elevated write into a `subflow`; pin an organization predicate on a `runAs: 'system'` sweep. - pm-dispatch decision-analysis: the app-repo exception to the recommendation order, funded entirely by reflow within the file's 46-line ceiling. Token ceilings rise by 900 across five rows under the ruling quoted verbatim in the CEILINGS block; the SKILL.md subtotal pin shifts by the same amount so the id-strip's lowering claim keeps its original slack. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2 --- .../references/decision-analysis.md | 22 ++--- scripts/check-skills-token-ratchet.mjs | 93 +++++++++++++++++-- skills/objectstack-automation/SKILL.md | 10 ++ skills/objectstack-data/SKILL.md | 6 ++ skills/objectstack-data/rules/validation.md | 6 ++ skills/objectstack-platform/SKILL.md | 22 +++++ skills/objectstack-ui/SKILL.md | 22 ++++- 7 files changed, 162 insertions(+), 19 deletions(-) diff --git a/.claude/skills/pm-dispatch/references/decision-analysis.md b/.claude/skills/pm-dispatch/references/decision-analysis.md index e8dcd1b3f4..ecfb67c3a7 100644 --- a/.claude/skills/pm-dispatch/references/decision-analysis.md +++ b/.claude/skills/pm-dispatch/references/decision-analysis.md @@ -4,9 +4,8 @@ ## 适用面(边界) -- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析; - 内部工具卡(dev 派发用)的四维照旧, - 不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。 +- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照 + 旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。 - 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。 ## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」) @@ -34,13 +33,14 @@ ## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补) -首行**机器可寻固定标记** ``(转义拼写写入, -写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数); -四棱各一行,用中文写(语言例外见主文件全体座位的不变量)—— -① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上; -零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍); -④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务); -一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。 -四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。 +首行**机器可寻固定标记** ``(转义拼写写入,写后回读核验存活;提取按 +字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例 +外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实 +际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮 +拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久 +义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同 +受六项约束:每行论据从业务立场写,机制名词只作括号补充。 **四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散; ③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。 +**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台 +缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。 diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index eace267b2e..224f8b84a5 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -242,6 +242,34 @@ export const CEILING_BASIS = { * touches (`objectstack-data`, `objectstack-platform`), measured from that PR's * branch head `7228d6c25` — see {@link CEILING_BASIS}. */ +// ── The 2026-08-31 app-repo-principles raise, shared by five rows below ───── +// +// The maintainer ruled that day that the way a metadata app works is itself +// published-skill material. Verbatim and untranslated: +// +// 「元数据应用就应该是在平台的规范下,基于skills 写元数据,并且使用平台提供的 +// os命令校验元数据的合法性,而不是重新造轮子。这个也应该进入 hotcrm 的规范, +// 甚至是 objectstack 的skills」 +// +// and, on the layout escape hatch (same day): +// +// 「或者说 skills 应该说明,逃生仓是极端场景按照客户需求自定义的场景下才需要, +// 应该尽量避免。」 +// +// That is the authorization this raise runs on, and it names THIS surface — +// `skills/`. The app-side half of the same ruling landed first in the app repo's +// own AGENTS.md; these rows are the platform half, carrying only the items that +// are universal to any ObjectStack app (the app/platform boundary, the docs +// discipline, the invariant-vs-transition-gate choice, blocking on a human +// judgement, flow privilege, the org predicate, and the section ladder). +// +// PAID DOWN FIRST, then raised for the remainder. The only genuine deletion +// available was in the ui row and it is recorded there; every other file's new +// text is a live fact with no existing text it makes redundant, so deleting to +// fund would have removed a fact with no other home. 27 tokens of existing +// headroom across three rows absorb part of the growth, so the ceilings move by +// 900 while the bundle grows by 927. The raising PR's body carries the same +// arithmetic, per-file and summed. export const CEILINGS = new Map([ // Every row re-measured after the internal issue-id strip — see // CEILING_BASIS.strippedInternalIds. `(was N)` is the ceiling this replaced. @@ -258,11 +286,35 @@ export const CEILINGS = new Map([ // published teaching is the only guard. +132 tokens, compressed to minimum. // Maintainer ruling 2026-08-25 (option B1, raise the ceiling), verbatim and // untranslated: 「我看到了,你分析过了,接受你的建议」. - ['skills/objectstack-automation/SKILL.md', 12643], - ['skills/objectstack-data/SKILL.md', 13783], // -34 (was 13817) + // 12643 -> 12768 (2026-08-31 app-repo-principles raise, see the block above). + // Two flow rules an app author gets wrong in the direction that leaks data. + // The `readonly` blockquote above them says "a flow that maintains a readonly + // field must run runAs:'system'" — true of the SCHEDULED flow it was written + // for, and read by an agent writing a SCREEN flow it says "elevate the whole + // screen flow", which elevates every other write in it. The second rule has no + // prior statement anywhere in the bundle: a `runAs:'system'` sweep has no + // trigger user to narrow it, so one with no organization predicate reads and + // writes across every tenant. +150 tokens, of which the row's 25 tokens of + // headroom absorb 25; the CEILING moves 125. + ['skills/objectstack-automation/SKILL.md', 12768], + // 13783 -> 13892 (2026-08-31 app-repo-principles raise, see the block above). + // The invariant-vs-transition-gate choice, placed in the field-conditional-rule + // bullet list where the tool is actually picked. Both wrong picks are silent: + // a transition gate written as a `validations[]` invariant bricks rows that + // were legal when stored, and an invariant written as `requiredWhen` never + // enforces itself at all. +110 tokens, 1 absorbed by headroom, ceiling +109. + ['skills/objectstack-data/SKILL.md', 13892], ['skills/objectstack-formula/SKILL.md', 6002], // -53 (was 6055) ['skills/objectstack-i18n/SKILL.md', 6338], // -11 (was 6349) - ['skills/objectstack-platform/SKILL.md', 12705], // -11 (was 12716) + // 12705 -> 12984 (2026-08-31 app-repo-principles raise, see the block above). + // The largest of the five and the frame the other four are read under: what an + // app IS (metadata under the platform's spec, authored from these skills, + // checked with `os`), where a capability gap gets fixed (upstream, never + // compensated for locally), and what a platform defect obliges — wait for the + // fix; no defensive coding, no shape tolerance, no hand-written predicate + // re-implementing a platform rule, and no landing of the half that fits. + // Nothing in this file said any of it. +280 tokens, 1 absorbed, ceiling +279. + ['skills/objectstack-platform/SKILL.md', 12984], // 14239 -> 14391: the pull-directed split-resolution order joined the decision // frame (maintainer ruling 2026-08-27, verbatim and untranslated: 「tong y 4」 — // accepting the four-rule set), and this file carries TWO enforced frame copies @@ -286,7 +338,23 @@ export const CEILINGS = new Map([ // that restated report binding a second time inside its own paragraph — +120 // net, 25113 -> 25143 tokens. The row's 12 tokens of headroom absorb part of // it, so the CEILING moves 18. The raising PR's body carries the same numbers. - ['skills/objectstack-ui/SKILL.md', 25143], + // 25143 -> 25445 (2026-08-31 app-repo-principles raise, see the block above). + // Two additions, one of them the escape-hatch ruling quoted there. The section + // ladder joins Record Presentation, which already tells an author to declare + // `fieldGroups` + `Field.group` and let the platform lay it out but never said + // what `sections` are FOR: derive (author none) -> reference a declared group + // (`{ group: '…' }`) -> enumerate members by hand, last, for a named-customer + // shape a reference cannot express. Corpus gravity is the whole reason the + // teaching has to move: every existing tutorial enumerates, so an agent copies + // enumeration unless the page says otherwise. The docs rule is the second + // addition — a doc explains business concepts, and a hand-copied inventory of + // objects/fields/components has no producer and goes stale. + // THE ONE GENUINE DELETION in this raise is here too: the file's two worked + // `sections` examples enumerated their members, i.e. taught rung 3 as the + // default in the very PR that demotes it. Both now reference a declared group, + // which is shorter — -13 tokens, real, and the fix the ruling asks for rather + // than a payment invented to fund it. +315 gross, -13, ceiling +302. + ['skills/objectstack-ui/SKILL.md', 25445], ['skills/objectstack-upgrade/SKILL.md', 8333], // -2 (was 8335) // ── the #12392 extension: the rest of the AUTHORED bundle ──────────────── @@ -313,7 +381,13 @@ export const CEILINGS = new Map([ ['skills/objectstack-data/rules/lifecycle.md', 1590], ['skills/objectstack-data/rules/naming.md', 773], ['skills/objectstack-data/rules/relationships.md', 3778], - ['skills/objectstack-data/rules/validation.md', 3024], + // 3024 -> 3109 (2026-08-31 app-repo-principles raise, see the block above). + // Severity Levels listed the three values and left the CHOICE unstated: a + // block rests on a judgement a person made, so a machine-inferred signal — a + // score, a duplicate guess — warns and never errors, and an override flag + // added to soften a block is evidence the block should have been a warning. + // +85 tokens, no headroom to absorb any of it, ceiling +85. + ['skills/objectstack-data/rules/validation.md', 3109], // objectstack-platform ['skills/objectstack-platform/evals/README.md', 514], @@ -735,8 +809,15 @@ function selfTest() { // 117943 -> 118095: shifted by exactly the +152 ruling-authorized raise on // the pm-dispatch row (2026-08-27 「tong y 4」, see that row), so the strip's // lowering claim keeps its original slack instead of being silently eaten. + // 118095 -> 118910: same operation for the 2026-08-31 app-repo-principles + // raise (see the block above the map), which lands on FOUR SKILL.md rows — + // platform +279, ui +302, data +109, automation +125 = +815. The fifth row + // of that raise, `objectstack-data/rules/validation.md`, is not a SKILL.md + // and correctly does not shift this pin. Subtotal after: 118830, i.e. the + // same 80 tokens of slack the strip's claim had before either raise — which + // is the point of shifting rather than widening. ['the re-measure lowered the SKILL.md subtotal', - skillMdCeilings.reduce((a, [, n]) => a + n, 0) < 118095, true], + skillMdCeilings.reduce((a, [, n]) => a + n, 0) < 118910, true], // ── the #12392 extension basis ─────────────────────────────────────── // Same reasoning as the pins above: the gate never reads this sha, so it diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 739f0c3123..bb478191f8 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -175,6 +175,16 @@ variables: [ > flagged as a warning.) Do **not** work around this by removing `readonly`; > that loses the field's edit protection. +> **Elevate the write, not the flow.** A `screen` flow stays `runAs: 'user'`. +> When one step in it must write a `readonly` field, move that step into a +> dedicated `runAs: 'system'` flow and call it from a `subflow` node — raising +> the whole flow silently elevates every other write in it. +> +> **A `runAs: 'system'` sweep must pin its organization.** System context has no +> trigger user, so nothing narrows the query: a scan or rollup with no +> organization predicate reads and writes across every tenant. The tenant column +> is platform-injected — filter on it, never re-declare it per object. + ```typescript { name: 'escalate_overdue_cases', diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index 0b25befac6..46e2753888 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -291,6 +291,12 @@ export const Invoice = ObjectSchema.create({ - Use `requiredWhen` for conditional requiredness; the ObjectQL validator enforces it on submit. The `conditionalRequired` alias was REMOVED in protocol 17 — emitting it is a parse error. +- **Choose by intent — invariant or transition gate.** A fact that must hold for + *every stored record* is an invariant: express it in `validations[]`. A + condition on a *transition* ("required once the record reaches `paid`") is + `requiredWhen` / field bounds, which let already-stored rows through. A + transition gate written as an invariant bricks existing data; an invariant + written as a transition gate never enforces itself. - For inline `master_detail` grids, predicates are evaluated row-by-row against the child row's `record`, so line-item rules should live on child fields. - For complex predicates, load **objectstack-formula** and emit CEL via diff --git a/skills/objectstack-data/rules/validation.md b/skills/objectstack-data/rules/validation.md index 83fbccb4cc..dc8de38238 100644 --- a/skills/objectstack-data/rules/validation.md +++ b/skills/objectstack-data/rules/validation.md @@ -260,6 +260,12 @@ severity: 'warning' // Allows save, shows warning severity: 'info' // Informational only ``` +**Blocking rests on a human judgement.** A machine-inferred signal — a score, a +duplicate guess, anything the system decided — may `warning`, never `error`. +Only a value a person wrote may block a save. Do not add an override flag to +soften a block either: an escape hatch around a rule is evidence the rule should +have been a warning. + ### Events ```typescript diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index d7708a5b13..37287f4a6e 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -49,6 +49,28 @@ they live in one skill. --- +## The App / Platform Boundary + +An ObjectStack app is a **simplified implementation of business features**: +author metadata under the platform's spec, guided by these skills, and check it +with the `os` commands ([Verify your work](#verify-your-work)). Never rebuild +what the platform owns. + +- **Business features belong in the app; capability belongs in the platform.** + A missing default, a wrong diagnostic, a shape the spec refuses — the fix is + upstream. Raise it there; do not compensate for it here. +- **A platform defect means waiting for the platform fix.** No defensive coding, + no shape tolerance, no hand-written predicate re-implementing a platform rule, + and never "land the half we can" — that spends the contract-first option and + leaves a decision half-executed. Record the block against the platform issue + so it is machine-visible; before resuming, confirm the version you **pin** + carries the fix (merged upstream ≠ present on your pin) and re-run the + defect's own reproduction. +- **A bad platform default is a default to fix**, not something to work around + at every call site. + +--- + ## The Template `blank` is the only template `create-objectstack` offers, and it is the default: diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index 960d88598f..2a0eb7a69e 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -95,7 +95,7 @@ custom page or form config. Prefer, in order: formViews: { default: { type: 'simple', - sections: [{ label: 'Invoice', fields: ['number', 'account'] }], + sections: [{ group: 'invoice_header' }], // a declared fieldGroup subforms: [ { childObject: 'invoice_line', // relationshipField + columns are title: 'Line Items', // derived from the child object; @@ -208,7 +208,7 @@ export const CaseViews = defineView({ filter: [{ field: 'status', operator: 'equals', value: 'open' }] }, }, formViews: { - edit: { type: 'simple', data, sections: [{ label: 'Case', fields: ['subject', 'status'] }] }, + edit: { type: 'simple', data, sections: [{ group: 'case_detail' }] }, }, }); ``` @@ -945,6 +945,17 @@ at runtime from how heavy the record is + the client viewport, because an author via container queries — the same form is 1 column in a narrow drawer and up to 4 on a wide page. Author *grouping* with `fieldGroups` + `Field.group`; the columns adapt themselves. +- **`sections` are the escape hatch — reach for them last.** The ladder, in + order: (1) **derive** — declare `fieldGroups` + `Field.group` and author no + `sections` at all; (2) **reference** — when one surface needs a local + arrangement, a section may name a declared group, `{ group: 'contact_info' }`, + and inherits its members, label and presentation (restating a key the group + declares is refused at parse); (3) **enumerate** — `{ label, fields: [...] }` + only for a named-customer requirement a group reference genuinely cannot + express (a cross-group entry combination, a wizard/pane structure), with that + reason in a comment beside it. A hand-enumerated section re-copies membership + the object already owns and goes stale on the next field added, so rung 3 is + an exception, never a default. > **Rule of thumb: presentation (surface / width / columns) is not metadata.** > Write fields + semantic roles; the renderer decides the pixels. Reach for @@ -1300,6 +1311,13 @@ src/docs/ The console rewrites `*.md` → `/docs/` (anchors preserved); broken same-package links fail the build. +**Write business concepts, not machine inventories.** A hand-copied table of +objects, fields or components has no producer and drifts; the self-describing +metadata is the one source. A doc answers *what is this, what business problem +does it solve, how do I use it*. Boundary: a fact the reader sees on screen +(the view list in an app's navigation) is documentable; the semantic layer +behind the screen is not. + ### Routing model — platform-level viewer, opt-in entry The viewer is **platform-level**: one global `/docs/` route From fa2a2ef706d453815df82e83401575df35e069ed Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 1 Sep 2026 01:11:49 +0000 Subject: [PATCH 2/2] fix(scripts): keep the ratchet's SHRINK testimony adjacent to its declaration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The authority block added by the previous commit sat between the `SHRINK-ONLY` doc comment and `export const CEILINGS`, pushing that testimony outside the 400-char anchor window check-ratchet-remedy-authority.mjs searches around every `CEILINGS` mention. The sweep then classified this gate `excluded` instead of `marked` — a MISCLASSIFIED failure, the farm reporting that a gate had silently left the maintainer-only convention. Measured green on origin/main and red on the branch, so the regression was this PR's. Move the block above the doc comment, restoring adjacency, and record the constraint where the next author will hit it. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01Msg17tAHJ3jVTYFgHydCm2 --- scripts/check-skills-token-ratchet.mjs | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/scripts/check-skills-token-ratchet.mjs b/scripts/check-skills-token-ratchet.mjs index 224f8b84a5..2117a0a6f0 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -234,16 +234,17 @@ export const CEILING_BASIS = { bundleExtension: 'c026b0d2d', }; -/** - * Measured counts, in the convention above. SHRINK-ONLY: lower freely, and see - * the header for what the other direction costs. - * - * Basis: `main` = 465bfce90 for every file, EXCEPT the two files PR #10402 - * touches (`objectstack-data`, `objectstack-platform`), measured from that PR's - * branch head `7228d6c25` — see {@link CEILING_BASIS}. - */ // ── The 2026-08-31 app-repo-principles raise, shared by five rows below ───── // +// ⚠️ PLACEMENT IS LOAD-BEARING: this block sits ABOVE the doc comment below, not +// between it and the declaration. check-ratchet-remedy-authority.mjs anchors this +// gate's expanding-remedy offer by finding SHRINK testimony within 400 chars of a +// `CEILINGS` mention in source, and that testimony is the "SHRINK-ONLY" line in +// that doc comment. Inserting prose between the two pushed it out of the window +// and flipped this gate's classification from `marked` to `excluded` — a MISCLASSIFIED +// failure, i.e. the farm reporting that a gate silently left the convention. +// Measured both ways while landing this raise. Keep new prose on this side. +// // The maintainer ruled that day that the way a metadata app works is itself // published-skill material. Verbatim and untranslated: // @@ -270,6 +271,15 @@ export const CEILING_BASIS = { // headroom across three rows absorb part of the growth, so the ceilings move by // 900 while the bundle grows by 927. The raising PR's body carries the same // arithmetic, per-file and summed. + +/** + * Measured counts, in the convention above. SHRINK-ONLY: lower freely, and see + * the header for what the other direction costs. + * + * Basis: `main` = 465bfce90 for every file, EXCEPT the two files PR #10402 + * touches (`objectstack-data`, `objectstack-platform`), measured from that PR's + * branch head `7228d6c25` — see {@link CEILING_BASIS}. + */ export const CEILINGS = new Map([ // Every row re-measured after the internal issue-id strip — see // CEILING_BASIS.strippedInternalIds. `(was N)` is the ceiling this replaced.