Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); docs(skills): land the app-repo working principles in the published catalog by claude[bot] · Pull Request #14052 · objectstack-ai/objectstack · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions .claude/skills/pm-dispatch/references/decision-analysis.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,9 +4,8 @@

## 适用面(边界)

- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;
内部工具卡(dev 派发用)的四维照旧,
不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只约束 **`needs-user-decision` 卡与决策箱讨论**的四维分析;内部工具卡(dev 派发用)的四维照
旧,不强加行业类比。四条轴本身不变,轴定义与绑定句在主文件「升级与决策」。
- 只适用新记录,存量分析 ⛔ 不回改(new-records-only,与四维中文化同款先例)。

## 常设决裁批流程(2026-08-26 裁;裁决原话与归席见主文件「升级与决策」)
Expand DownExpand Up@@ -34,13 +33,14 @@

## 四棱卡面块固定形状(落卡即带,⛔ 不留待维护者到场再补)

首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,
写后回读核验存活;提取按字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);
四棱各一行,用中文写(语言例外见主文件全体座位的不变量)——
① 项目长远合理性(缩小还是扩大特例/契约增生);② 实际业务拉动(今天谁撞上;
零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮拒绝优于静默容忍);
④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久义务);
一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。
四棱行同受六项约束:每行论据从业务立场写,机制名词只作括号补充。
首行**机器可寻固定标记** `<!-- os-decision-facets -->`(转义拼写写入,写后回读核验存活;提取按
字面文本 grep,不依赖注释形状 —— sanitizer 纪律见平台读数);四棱各一行,用中文写(语言例
外见主文件全体座位的不变量)—— ① 项目长远合理性(缩小还是扩大特例/契约增生);② 实
际业务拉动(今天谁撞上;零拉动默认 defer/remove);③ 防 AI 犯错(闭合枚举优于自由结构、响亮
拒绝优于静默容忍);④ 创业阶段不扩散(remove 优于 declare-and-maintain,每个已声明的键都是永久
义务);一行推荐 + 字母选项(A/B/…);**一行强制置信缺口(「本分析看不见什么」)**。四棱行同
受六项约束:每行论据从业务立场写,机制名词只作括号补充。
**四棱分歧推荐序**(2026-08-27「tong y 4」):②实测拉动⇒荐①长远终态;零拉动⇒荐④不扩散;
③破余下平局向响亮/结构;安全与难逆恒人工;⛔ 只排推荐,分歧照旧升级、代裁面不扩。
**应用仓推荐序特例**(2026-08-31 裁「既然是平台缺陷,就应该等待平台处理」):阻塞源是平台
缺陷 ⇒ 恒荐等待,⛔ 不荐绕行(形状容错/复刻平台规则)、不荐劈半落地。
103 changes: 97 additions & 6 deletions scripts/check-skills-token-ratchet.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.
Expand All@@ -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
Expand All@@ -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 ────────────────
Expand All@@ -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],
Expand DownExpand Up@@ -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
Expand Down
10 changes: 10 additions & 0 deletions skills/objectstack-automation/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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',
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
6 changes: 6 additions & 0 deletions skills/objectstack-data/rules/validation.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
22 changes: 22 additions & 0 deletions skills/objectstack-platform/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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:
Expand Down
22 changes: 20 additions & 2 deletions skills/objectstack-ui/SKILL.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -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;
Expand DownExpand Up@@ -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' }] },
},
});
```
Expand DownExpand Up@@ -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
Expand DownExpand Up@@ -1300,6 +1311,13 @@ src/docs/
The console rewrites `*.md` → `/docs/<target>` (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/<name>` route
Expand Down
Loading