Skip to content

Commit bec4fd6

Browse files
committed
docs(agents): state the app-vs-platform boundary once, so it stops being re-derived
The boundary was decided ad hoc three times in one day, by three seats, each from scratch, and the three derivations differed. Nothing stated the rule. Written to fit the ratchet rather than raise it. AGENTS.md had exactly one line of headroom (1161 against a 1162 ceiling), and both funding routes an author may take alone are closed here: re-wrap funding is banned by the 2026-08-17 ruling, and a declared cross-file move cannot fund new content because the source decrease cancels against the destination raise. So the split follows the ratchet's own division of labour — principles in the instruction file, on-demand detail in references/: - AGENTS.md gains ONE line, a Context Routing row carrying the deciding question and pointing at the rule. 1161 -> 1162, exactly the ceiling, which is unchanged. - The rule itself lands in a new reference file: the deciding question, the publication test, and the two anti-patterns with the measurement behind each. - The new file is entered in both ratchet maps at its landed count, so it arrives metered rather than as an un-ceilinged file in a ratcheted directory. Every lesson is carried self-contained (failure mode, discipline, boundary) with no issue-ID citation, per the 2026-08-12 ruling that check:pm-skill-id-lint enforces. No deletions: nothing was removed to make room. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UHvF5hyiZjnCyExFnfQB8m
1 parent 52d5a52 commit bec4fd6

3 files changed

Lines changed: 76 additions & 0 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# App or platform — which work belongs where
2+
3+
Routed here from `AGENTS.md`. This boundary was decided ad hoc three times in one day, by
4+
three seats, each from scratch, and the three derivations differed. It is one rule, not new
5+
policy: the rule those decisions already followed, written down so it stops being re-derived.
6+
7+
Lessons here are self-contained by the 2026-08-12 ruling — failure mode, discipline and
8+
boundary in the text, no issue number to dereference.
9+
10+
## The deciding question
11+
12+
**Could this be written by something that has only the metadata, and no knowledge of this
13+
company?**
14+
15+
| Answer | Belongs in |
16+
|:--|:--|
17+
| **No** — it encodes the company's own judgement | the **metadata app**: a discount ceiling, who a case is assigned to, how won/lost is booked; its own objects, views and flows |
18+
| **Yes** — it only asks whether the metadata is self-consistent | the **platform**: reference integrity, translation coverage, view rosters, sharing-rule coverage, CRUD round-trips, RLS probes per declared position |
19+
| the subject is platform behaviour, the cost lands on the app | the **platform**, and it is a **gap** until it does — asserting what a hook does inside the platform's own sandbox is one |
20+
21+
The second row is the one that pays. An app that hand-writes it is hand-writing a
22+
consistency proof the platform can derive once, for every app, and check on every build.
23+
24+
## The second question, for a capability an app wants published
25+
26+
**If a second app needed this, would it copy the implementation?** Yes ⇒ platform.
27+
28+
One consumer is a **use**; two is a **contract**. A resolution wanted by four call sites
29+
inside a single package stays package-private on exactly this test — the home question gets
30+
decided when a second consumer is actually in hand, and a symbol that was never published
31+
can move without a major. ⛔ The dependency graph is not the test: check the real edges
32+
before claiming two packages cannot import each other, because three of six directed edges
33+
usually already exist.
34+
35+
⛔ This question decides what a package **in this monorepo** exports. It is contributor
36+
guidance and must not ship to customers, who cannot act on it.
37+
38+
## ⛔ Anti-pattern 1 — an app hand-copies a platform rule
39+
40+
The copy diverges from the rule the build actually applies, so the app stays green while
41+
the platform refuses. The divergence is invisible until the two disagree, and then the
42+
app's own test suite is the thing arguing for the wrong answer.
43+
44+
Measured twice. A card wanting the platform's hook-body lowering pass turned out to want
45+
half a command that already ships (`os build --strict-body`, which turns "bundled instead
46+
of lowered" into exit 1 — `packages/cli/src/commands/compile.ts`, of which `build` is an
47+
alias) and half an `os lint` rule that exists nowhere in the tree. One level up, the same
48+
shape ships a rule claiming "0 findings over the corpus" against a corpus that is not the
49+
app it names.
50+
51+
⇒ Before hand-writing against platform behaviour, grep the CLI for the command that already
52+
does it, and confirm the rule you are citing exists.
53+
54+
## ⛔ Anti-pattern 2 — a capability that under-delivers silently
55+
56+
Worse than none. `os verify` once derived **zero** cases on a multi-package app and reported
57+
success. `packages/cli/src/commands/verify.ts` states the shape in its own header: *a
58+
verifier that under-verifies reports success it never established*.
59+
60+
An app that meets one of these writes its own harness and never comes back — so the loss is
61+
permanent, and it is invisible, because the capability is still reporting green.
62+
63+
**The order is therefore fixed: make the derived half trustworthy first, then take the
64+
hand-written half back.** Reversing it just deletes protection. The fix for the `os verify`
65+
case landed as an ordered pair — ledger the four losses first so the fix has something to be
66+
checked against, then close them — which is this rule executed.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ their output is current; the wrapper reports that each run rather than staying s
833833
| `apps/docs/**` | Docs Engineer | Fumadocs + Next.js, MDX |
834834
| `examples/**` | Example Author | Minimal, runnable, uses `defineStack` |
835835
| `content/docs/**` | Technical Writer | Respect auto-gen boundaries |
836+
| `examples/**` app metadata vs `packages/**` capability | Scope Triage | Could this be written from the metadata alone, with no knowledge of this company? No ⇒ the app; yes ⇒ the platform, and a **gap** until it does. Full rule, publication test and both anti-patterns: `.claude/skills/pm-dispatch/references/app-platform-boundary.md` |
836837
| `../objectui/**` (sibling repo) | Studio UI Engineer | React + Shadcn + Tailwind, dark mode default |
837838

838839
---

scripts/pm/check-skill-line-ratchet.mjs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -599,6 +599,14 @@ export const CEILINGS = new Map([
599599
// over-120B lines are all `table` rows, structurally exempt and NOT
600600
// re-wrappable; they are metered instead by MAX_TABLE_ROW_BYTES below.
601601
['.claude/skills/pm-dispatch/references/compile-surfaces.md', 26],
602+
// The app-vs-platform boundary — the deciding question (could this be written
603+
// from the metadata alone, with no knowledge of this company?), the publication
604+
// test (one consumer is a use, two is a contract) and the two anti-patterns.
605+
// It is on-demand DETAIL by this header's own division of labour: AGENTS.md
606+
// carries the one routing row, this file carries the rule and the measurement
607+
// behind each clause. A NEW file, so this is not a raise and funds nothing —
608+
// set at the landed line count, headroom 0, same convention as above.
609+
['.claude/skills/pm-dispatch/references/app-platform-boundary.md', 66],
602610
// Lane job descriptions (maintainer ruling 2026-08-19: per-lane PM job
603611
// descriptions move from seat-post prose into versioned skill references).
604612
// Set at landed line counts (headroom 0, same convention as above).
@@ -916,6 +924,7 @@ export const MAX_TABLE_ROW_BYTES = new Map([
916924
['.claude/skills/pm-dispatch/references/seat-post-protocol.md', 0],
917925
['.claude/skills/pm-dispatch/references/true-green.md', 0],
918926
['.claude/skills/pm-dispatch/references/compile-surfaces.md', 352],
927+
['.claude/skills/pm-dispatch/references/app-platform-boundary.md', 219],
919928
['.claude/skills/pm-dispatch/references/lanes/engine.md', 0],
920929
['.claude/skills/pm-dispatch/references/lanes/services.md', 0],
921930
['.claude/skills/pm-dispatch/references/lanes/cli.md', 0],

0 commit comments

Comments
 (0)