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..2117a0a6f0 100644 --- a/scripts/check-skills-token-ratchet.mjs +++ b/scripts/check-skills-token-ratchet.mjs @@ -234,6 +234,44 @@ export const CEILING_BASIS = { bundleExtension: 'c026b0d2d', }; +// ── 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: +// +// 「元数据应用就应该是在平台的规范下,基于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. + /** * Measured counts, in the convention above. SHRINK-ONLY: lower freely, and see * the header for what the other direction costs. @@ -258,11 +296,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 +348,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 +391,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 +819,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