From 9da6bbcf5a396b6736274fb388339f5f9d811ffa Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 14:20:14 +0000 Subject: [PATCH 1/2] docs(agents): keep only what is necessary, and add a CLAUDE.md pointer Rewrite AGENTS.md top-down from the necessity test: the metadata-application principle and the four Scope rules open the file; every kept rule is one to three lines with its failure mode; measurement transcripts, superseded-claim histories, origin anecdotes and precedent rosters leave; hand-copied inventories (the 12-suffix File Suffix Protocol, rule 5's own drift tally, the chart-type list) are replaced by the neighbour-file rule and pointers at src/{type}/. Rule 6 now states its real width (list view names take the pack wording; other pack-carried nouns keep English, the tile guard's ablation is the discriminator). Platform Upgrades gains step 5 (re-scope, never renumber; version-agnostic, cross-line, identifiers and labels; hits are candidates; honest about what a grep cannot see). 651 lines -> 248. CLAUDE.md is a pointer to AGENTS.md and nothing else, modelled on .github/copilot-instructions.md. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MoTv7pn338AZ71owsp19gQ --- AGENTS.md | 735 ++++++++++++------------------------------------------ CLAUDE.md | 6 + 2 files changed, 172 insertions(+), 569 deletions(-) create mode 100644 CLAUDE.md diff --git a/AGENTS.md b/AGENTS.md index 3218e0ef6..c8f5db311 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,651 +1,248 @@ # AGENTS.md โ€” HotCRM -Single source of truth for AI coding agents working on **HotCRM**, the AI-Native -Enterprise CRM built on the **@objectstack/runtime** engine. Tool-specific files -(e.g. `.github/copilot-instructions.md`) point here. +Single source of truth for every AI coding agent working on **HotCRM**. Tool-specific files (`CLAUDE.md`, `.github/copilot-instructions.md`) only point here and restate nothing. -You are an expert developer working on HotCRM, delivering business capabilities -as metadata on top of the ObjectStack platform. HotCRM is a **pure metadata -application**: business features are built here, platform capability is built on the -platform. Read **๐Ÿšซ Scope โ€” a pure metadata application** before your first edit; it -governs every other chapter in this file. +## ๐Ÿšซ Scope โ€” a pure metadata application -## ๐Ÿ—ฃ๏ธ ๆฒŸ้€š่ฏญ่จ€ / Communication Language +Maintainer rulings, verbatim and kept untranslated (2026-09-09, then 2026-08-31): -**ๅง‹็ปˆไฝฟ็”จไธญๆ–‡ไธŽ็”จๆˆทๆฒŸ้€šใ€‚** ๆ‰€ๆœ‰้ขๅ‘็”จๆˆท็š„ๅ›žๅคใ€่งฃ้‡Šใ€ๆ€ป็ป“ใ€ๆ้—ฎ้ƒฝ็”จไธญๆ–‡ใ€‚ -ไปฃ็ ใ€ๆ ‡่ฏ†็ฌฆใ€ๆไบคไฟกๆฏ๏ผˆcommit message๏ผ‰ใ€PR ๆ ‡้ข˜/ๆญฃๆ–‡ใ€ไปฃ็ ๆณจ้‡ŠไฟๆŒ่‹ฑๆ–‡ไธๅ˜ใ€‚ +> ใ€Œ้ฆ–ๅ…ˆ่ฆๆ˜Ž็กฎ hotcrm ๆ˜ฏๅ…ƒๆ•ฐๆฎๅบ”็”จ๏ผŒๅนณๅฐๅŠŸ่ƒฝๅบ”่ฏฅๅœจ objectstackไธญๅผ€ๅ‘ใ€‚ใ€ -**Always communicate with the user in Chinese (ไธญๆ–‡).** All user-facing replies, -explanations, summaries, and questions must be in Chinese. Keep code, identifiers, -commit messages, PR titles/bodies, and code comments in English. +> ใ€ŒๅŸบไบŽไปฅไธŠๅ†ณ่ฃ๏ผŒๅฏนไบŽhotcrm ๅ…ƒๆ•ฐๆฎ้กน็›ฎ๏ผŒ้œ€่ฆ่กฅๅ……ๅ“ชไบ› agents.md ๏ผŒๆฏ”ๅฆ‚็บฏๅ…ƒๆ•ฐๆฎๅบ”็”จๅบ”่ฏฅ็ฎ€ๅŒ–๏ผŒๅชๅผ€ๅ‘ไธšๅŠกๅŠŸ่ƒฝ๏ผŒๅนณๅฐ่ƒฝๅŠ›ๅ…จ้ƒจๅœจๅนณๅฐๅผ€ๅ‘ใ€‚ใ€ -## ๐Ÿ—๏ธ Project Architecture +**HotCRM is a metadata application. Platform capability is developed in objectstack.** This chapter governs every other chapter of this file; the ruling corpus behind the four rules is objectstack#13848. + +### 1. What HotCRM is -HotCRM is a **single ObjectStack marketplace app**, not a multi-package monorepo. -The source of truth is `objectstack.config.ts`, which registers all metadata from a -flat `src/` tree organised **by metadata type** (not by business package). +Business capability authored as metadata under the platform spec, guided by the platform's published skills, checked for legality by +the `os` commands (`pnpm validate`, `pnpm lint`). **Build business features here; build platform capability on the platform.** A gap +you find in the platform โ€” infrastructure, a service, dev tooling, a builder โ€” is filed upstream: โ›” never compensated for or re-invented here. + +### 2. A platform defect means you WAIT for the platform fix + +โ›” Do not route around it โ€” no defensive coding, no shape tolerance, no hand-written predicate re-implementing a platform rule, no +landing "the half we can land now". File the blocked card with a `Blocked-by:` line to the platform card; when the fix lands, resume the +**original** ruling after confirming the pinned `@objectstack/*` version carries it (**merged is not available in the pin**) and +re-running the defect card's own fixture. Red means stop. (#549) + +### 3. โ›” Do not build platform-level tooling here + +Lint, validation, gates and diagnostics belong to the platform, uniformly; a drift-class or validation-class gap goes upstream. Tests +here pin **this repo's own business facts** only โ€” a seed-row pin, a "doc wording equals the language-pack label" guard. โ›” No gate farm. (#806) + +### 4. A bad platform default is fixed at the default + +Writing an `emptyState` on every tile, or hand-tuning tile placement to dodge a platform render defect, pays the same tax on every tile. Change the default upstream. (#1212 โ†’ objectui#7063) + +## ๐Ÿ—ฃ๏ธ ๆฒŸ้€š่ฏญ่จ€ / Communication Language + +**ๅง‹็ปˆไฝฟ็”จไธญๆ–‡ไธŽ็”จๆˆทๆฒŸ้€šใ€‚** ๆ‰€ๆœ‰้ขๅ‘็”จๆˆท็š„ๅ›žๅคใ€่งฃ้‡Šใ€ๆ€ป็ป“ใ€ๆ้—ฎ้ƒฝ็”จไธญๆ–‡๏ผ›ไปฃ็ ใ€ๆ ‡่ฏ†็ฌฆใ€ๆไบคไฟกๆฏใ€PR ๆ ‡้ข˜/ๆญฃๆ–‡ใ€ไปฃ็ ๆณจ้‡ŠไฟๆŒ่‹ฑๆ–‡ไธๅ˜ใ€‚ +**Always communicate with the user in Chinese (ไธญๆ–‡)**; code, identifiers, commit messages, PR titles/bodies and code comments stay in English. + +## ๐Ÿ—๏ธ Project Architecture -- **Engine**: We DO NOT build the core engine. We use `@objectstack/runtime` as the platform dependency. -- **Metadata** (`src/{type}/`): All business capability lives here as typed metadata files. -- **Product docs** (`content/docs/`): User/admin/marketplace documentation (Fumadocs). -- **Internal docs** (`docs/`): Architecture, status, release, and maintenance notes. +HotCRM is a **single ObjectStack marketplace app**, not a multi-package monorepo: `objectstack.config.ts` registers every metadata +collection from a flat `src/` tree organised **by metadata type**, on the `@objectstack/runtime` dependency โ€” โ›” never built, patched or worked around here. -**Directory Structure** (see `docs/README.md` for the full map): ``` hotcrm/ -โ”œโ”€โ”€ objectstack.config.ts # App manifest โ€” registers metadata from src/ +โ”œโ”€โ”€ objectstack.config.ts # App manifest โ€” feeds each src/{type}/index.ts barrel to defineStack() โ”œโ”€โ”€ src/ โ”‚ โ”œโ”€โ”€ objects/ # *.object.ts schemas + *.hook.ts lifecycle hooks -โ”‚ โ”œโ”€โ”€ views/ pages/ # App UI metadata (*.view.ts / *.page.ts) +โ”‚ โ”œโ”€โ”€ views/ pages/ # List views (*.view.ts) and page layouts (*.page.ts) โ”‚ โ”œโ”€โ”€ flows/ # Automation (*.flow.ts) โ”‚ โ”œโ”€โ”€ actions/ # UI actions + AI-callable tools (*.actions.ts) -โ”‚ โ”œโ”€โ”€ dashboards/ reports/ datasets/ # Analytics metadata -โ”‚ โ”œโ”€โ”€ skills/ # AI skill metadata (*.skill.ts) โ€” skills-only surface -โ”‚ โ”œโ”€โ”€ profiles/ sharing/ # Permission sets, positions, sharing rules -โ”‚ โ”œโ”€โ”€ translations/ # Locale bundles (en / zh-CN / es-ES / ja-JP) +โ”‚ โ”œโ”€โ”€ dashboards/ reports/ datasets/ # Analytics metadata +โ”‚ โ”œโ”€โ”€ skills/ # AI skills (*.skill.ts) โ€” the app's only AI surface +โ”‚ โ”œโ”€โ”€ profiles/ sharing/ # Permission sets (*.profile.ts) and sharing rules (*.sharing.ts) +โ”‚ โ”œโ”€โ”€ translations/ # Locale packs: en ยท zh-CN ยท es-ES ยท ja-JP โ”‚ โ””โ”€โ”€ data/ # Seed data (defineDataset) -โ”œโ”€โ”€ content/docs/ # Product documentation site content -โ””โ”€โ”€ docs/ # Internal maintainer documentation +โ”œโ”€โ”€ content/docs/ # Product documentation site (Fumadocs): en, zh-Hans, zh-Hant +โ””โ”€โ”€ docs/ # Internal maintainer documentation (docs/README.md is the full map) ``` -> The retired multi-package (`packages/*`) direction is archived under -> `docs/archive/`. Do NOT create `packages//src/` paths โ€” everything lives in the -> flat `src/{type}/` tree above. +`src/` itself is the roster of what this app authors โ€” open it rather than trust a list. โ›” Do NOT create `packages//src/` paths: the multi-package layout is retired and archived under `docs/archive/`. ## ๐Ÿ’ป Tech Stack & Protocol -1. **Metadata-First (`*object.ts`)**: - - All business objects are defined in **TypeScript** using the `ServiceObject` interface. - - strictly typed using `@objectstack/spec`. - - **NEVER** use YAML or JSON for metadata. - - Object names must be `snake_case`. - - **All HotCRM business object names MUST use the `crm_` prefix and the prefix MUST be written explicitly in source** (e.g., `crm_account`, `crm_opportunity`, `crm_knowledge_article`). **No automatic prefix injection by the runtime.** Open architectural decision: AI-authored metadata is fragile around "context-aware" naming, so we trade verbosity for grep-ability. Lookup and master-detail targets (`Field.lookup(...)` / `Field.masterDetail(...)`) MUST use the prefixed name. Cube `sql:` fields, view `data.object`, hook `object:`, action `objectName:`, navigation `objectName:`, dashboard `object:`, translation `objects.{key}`, REST URLs, and DB table names ALL use the same prefixed name. The name in source = the name at runtime = the name in DB = the name in URL = the name in docs. No translation layer. - -2. **ObjectQL (No-SQL)**: - - Data access MUST use **ObjectQL**, reached through the `ctx.api` surface. There is - **no `broker`** in this repo โ€” the identifier has zero occurrences in `src/`. - - **NEVER** write raw SQL. - - Format: `ctx.api.object('crm_opportunity').find({ where: { amount: { $gt: 50000 } } })`. - Object names carry the `crm_` prefix, same as rule 1. In a `*.hook.ts` the surface is - cast once โ€” `const api = ctx.api as HookApi | undefined`, from `src/objects/_hook-api.ts` - โ€” and then called as `api.object(...)`; action script bodies call `ctx.api.object(...)` - directly. - - On `ctx.api`, the predicate key is **`where`**, and only `where` โ€” but read the - **reason** rather than recalling it, because the reason this file used to give has - been measured false. **Measured per method against the pinned `@objectstack` - packages (17.2.0)**, on the object the kernel injects as `ctx.api`, and pinned - assertion-by-assertion in `test/hook-query-predicate.test.ts`: `filter` is a **live - alias of `where`** โ€” on `find`, on `findOne`, on `count`, and on the `update` / - `delete` options bag โ€” folded to the canonical key before the query executes. It is - **not** dropped and it does **not** widen the row set. Nothing degrades quietly - either: the engine **throws** on any option it does not recognise, so `filters` - (plural โ€” still not an alias) and every misspelling fail loudly, and `findOne` with - no predicate at all throws rather than returning an arbitrary row. - โš ๏ธ **Superseded**, and quoted widely enough to be worth naming: the claim that - passing `filter` in process "fails **silently**", that `findOne` returns the object's - **first row** and `count` counts the **whole object**. That was true of an older - kernel โ€” the repo really did pay for it once, seventeen hook calls whose predicate - vanished (`CHANGELOG.md`, the `hook-query-where-not-filter` entry) โ€” and it is the - error that is **unsafe in the wrong direction**: it invites an author to imagine an - unscoped read where this engine either applies the predicate or throws. - So the reason to write `where` is **one idiom**, not silent data loss โ€” and it is - still a real reason, because *mixing* the spellings is what breaks. A query assembled - in two places that ends up carrying **both** keys with different values throws - `Conflicting options โ€ฆ 'where', 'filter' are spellings of the same parameter`, and an - empty `where: {}` counts as a different value, so a base predicate plus a `filter:` - override is a runtime throw and not a merge. `HookQuery` in - `src/objects/_hook-api.ts` therefore still omits the alias, which keeps the mistake a - **compile** error, and the repo-wide guard in `test/hook-query-predicate.test.ts` - keeps `src/objects/` to the one spelling. - Other surfaces are **not** governed by this rule and have their own spelling โ€” but a - different surface never licenses a shape that surface's own schema rejects. A - `*.flow.ts` node `config` takes `filter:`, and `where:` appears in no flow file at - all โ€” grep `src/flows/` for the current spread rather than trusting a number in - prose (*the hand-copied occurrence counts that stood here had already drifted; - superseded by the 2026-08-31 ruling, item 5*). Do not "fix" that into a hook's - `where:`, nor a hook's `where:` into `filter:`. A **page component** also spells - the key `filter:`, and its shape is whatever that component's entry in `ComponentPropsMap` - (`@objectstack/spec/ui`) declares โ€” for `record:related_list`, an array of rule - **objects**: `filter: [{ field, operator, value }]`, `operator` from a closed - vocabulary (`equals`, `not_equals`, `in`, โ€ฆ), no other key accepted. The AST array - (`[['status', '!=', 'completed']]`) and the `op:` key are not second spellings: - `objectstack build` rejects both and the list renders unfiltered. No in-repo example - is cited here on purpose โ€” every `record:related_list` filter currently under - `src/pages/` is one of those rejected forms (#1248), so read the shape off the - schema instead. - -3. **AI-Native**: - - Every feature should consider AI augmentation (Co-Pilot, Agents). - - Use `*.actions.ts` to define tools callable by AI agents. - -## ๐Ÿง  Autonomous Iteration Protocol - -When asked to implement a feature, you MUST follow this **Thinking Process**: - -### Phase 1: Architecture & Planning -1. **Analyze**: Identify the domain area (e.g., sales, service, marketing) and the metadata it touches. -2. **Schema Design**: List all necessary Objects, Fields, and Relationships. -3. **File Inventory**: List exact file paths to be created. - * `src/objects/candidate.object.ts` (Data) - * `src/flows/candidate.flow.ts` (Automation) - * `src/pages/candidate.page.ts` (UI) - -### Phase 2: Implementation (Iterative) -1. **Metadata First**: Create `*.object.ts` files first. They are the source of truth. -2. **Logic Second**: Create `*.hook.ts` and `*.actions.ts` utilizing the defined objects. -3. **UI Last**: Create `*.page.ts` and Actions to expose functionality to users. - -### Phase 3: Self-Correction -After generating code, ask yourself: -* [ ] Did I respect the strictly typed `ServiceObject` interface? -* [ ] Do all lookup / master-detail targets name real objects? -* [ ] Did I use ObjectQL instead of SQL? -* [ ] are file names strictly `snake_case`? - -## ๐Ÿ“ Coding Standards (The "File Suffix Protocol") - -We enforce strict file naming to separate concerns. Files live under `src/{type}/`, grouped by metadata type (e.g. `src/objects/`, `src/flows/`, `src/views/`). - -The authoring form per type, the one place validation is enforced, and the traps in -between are in **๐Ÿ”’ Schema Validation Requirements** below. - -### Core File Types -- `*.object.ts`: Data Model (Schema) โ€” built with `ObjectSchema.create()` from `@objectstack/spec/data`, which validates as it constructs -- `*.hook.ts`: Server-side Business Logic (Triggers) -- `*.actions.ts`: API Endpoints & AI Tools โ€” the one PLURAL suffix in this protocol; one file bundles an entity's actions (e.g. `src/actions/lead.actions.ts`) -- `*.flow.ts`: Automation Flows โ€” typed as `Automation.Flow` from `@objectstack/spec/automation` -- `*.page.ts`: UI Page Layouts โ€” a literal annotated with the `Page` type from `@objectstack/spec/ui` -- `*.view.ts`: List View Configurations โ€” built with `defineView()` from `@objectstack/spec/ui` - -### Extended File Types (Phase 6+) -- `*.dashboard.ts`: Dashboard Definitions โ€” a literal annotated with the `Dashboard` type from `@objectstack/spec/ui` -- `*.form.ts`: Form View Definitions โ€” validated with `FormViewSchema` from `@objectstack/spec/ui` -- `*.statemachine.ts`: State Machine Definitions โ€” validated with `StateMachineSchema` from `@objectstack/spec/automation` -- `*.profile.ts`: Permission Set Definitions โ€” a plain literal in `src/profiles/`, registered as `defineStack({ permissions })` and validated against `PermissionSetSchema` (`@objectstack/spec/security`) there. โ›” Not `*.permission.ts`, which this app authors nowhere -- `*.capabilities.ts`: Plugin Capability Manifests โ€” validated with `PluginCapabilityManifestSchema` from `@objectstack/spec/kernel` -- `*.events.ts`: Domain Event Definitions โ€” validated with `EventSchema` from `@objectstack/spec/kernel` +1. **Metadata-first, TypeScript only.** Every business object is a `*.object.ts` built with `ObjectSchema.create()` from + `@objectstack/spec/data`. โ›” Never YAML or JSON metadata; โ›” never raw SQL. +2. **The `crm_` prefix is written out, everywhere.** Business object names are `snake_case` with an explicit `crm_` prefix + (`crm_account`); the runtime injects nothing, so the name in source = at runtime = in the DB = in the REST URL = in the docs, and + every reference โ€” lookup / master-detail targets, cube `sql`, view, hook, action, navigation, dashboard and translation keys โ€” uses + it. Platform objects keep `sys_*`; the file stays unprefixed (`src/objects/contract.object.ts` declares `name: 'crm_contract'`); the + roster is `src/objects/*.object.ts`, โ›” never restated in prose. The `os lint` `naming/namespace-prefix` warning on non-object items + is advisory (ADR-0048): โ›” do not mass-rename to chase it. +3. **Data access is ObjectQL through `ctx.api`** โ€” there is no `broker`. Shape: + `ctx.api.object('crm_opportunity').find({ where: { amount: { $gt: 50000 } } })`; a `*.hook.ts` casts once + (`const api = ctx.api as HookApi | undefined`, from `src/objects/_hook-api.ts`), an action body calls it directly. + - **The predicate key is `where`.** `filter` is a live alias the engine folds to `where`, so the hazard is not silent loss but + **mixing**: a query carrying both keys throws `Conflicting options โ€ฆ 'where', 'filter'`, and an empty `where: {}` is a different + value. `HookQuery` omits `filter` so the mix is a compile error; `test/hook-query-predicate.test.ts` pins the engine per method. + - **Other surfaces spell their own key, and their own schema decides.** A `*.flow.ts` node `config` takes `filter:`; a page component's + `filter` is its `ComponentPropsMap` entry (`@objectstack/spec/ui`) โ€” `record:related_list` takes rule **objects** `[{ field, operator, value }]`; the AST array and `op:` are rejected by `objectstack build` and the list renders unfiltered (#1248). +4. **AI-native.** AI-callable tools are `*.actions.ts`. The AI surface is skills-only โ€” `src/skills/*.skill.ts` via `defineSkill()`, + attached to a platform agent by `surface`. โ›” Never author a `*.agent.ts` (#512, ADR-0063 ยง2). ## ๐Ÿ”’ Schema Validation Requirements -Every metadata file under `src/` is validated โ€” but โ›” **not by a `parse()` call you write in -the file.** No file under `src/` makes one. Authoring form and enforcement point are two -separate questions, and only the first is yours to get right. - -### Author the file the way its neighbours are authored - -The form is **not uniform across metadata types**, so the reliable move is to open the file -next to the one you are creating and copy its shape. The directory is the source of truth; -the map below says which shape to expect, not what to transcribe. - -1. **A validating constructor, called in the file.** `src/objects/*.object.ts` uses - `ObjectSchema.create({ โ€ฆ })` (`@objectstack/spec/data`), `src/views/*.view.ts` uses - `defineView({ โ€ฆ })` and `src/skills/*.skill.ts` uses `defineSkill({ โ€ฆ })`. These reject - at author time โ€” `ObjectSchema.create()` names an unknown key back to you - (`unknown key(s) โ€” workflows`) rather than stripping it in silence. -2. **A typed object literal, with no runtime call in the file.** `src/pages/*.page.ts` - annotate with `Page`, `src/dashboards/*.dashboard.ts` with `Dashboard` (both - `@objectstack/spec/ui`), and `src/flows/*.flow.ts` with `Automation.Flow` - (`import type * as Automation from '@objectstack/spec/automation'`). The type is what - your editor and `pnpm typecheck` read. -3. **A plain object literal with no schema import at all.** `src/profiles/*.profile.ts` โ€” - where this app's **permission sets** live โ€” and `src/sharing/*.sharing.ts`. - -โš ๏ธ The form does not follow the metadata type in any tidy way: pages and views come from the -same `@objectstack/spec/ui` module and are authored differently. โ›” Do not generalise from -one directory to its neighbour. The `define*` names are the trap here โ€” `defineView()` and -`defineSkill()` **are** the form for their directories, while `defineFlow()` (which is -exactly `FlowSchema.parse(config)`) is **not** the form for `src/flows/`. - -### Where the validation actually happens - -All three forms converge on one enforcement point: `objectstack.config.ts` hands every -collection to `defineStack()`, which validates each against its `@objectstack/spec` schema. -`pnpm validate` and `pnpm build` run it, and the platform parses again at registration on -boot (for flows, `AutomationEngine.registerFlow`). - -Measured rather than asserted โ€” an unknown key added on disk to a page (form 2), and to a -permission set (form 3, whose file imports nothing from `@objectstack/spec`): +Every metadata file under `src/` is validated โ€” โ›” **not by a `parse()` call you write in the file**; no file under `src/` makes one. +The authoring form is yours to get right; the enforcement point is fixed. -``` -โœ— pages.7: Unrecognized key(s) on this page: `bogusUnknownKey`. โ€ฆ -โœ— permissions.1: Unrecognized key(s) on this permission set: `bogusUnknownKey`. โ€ฆ -``` - -Both fail `pnpm validate` with exit 1. Nothing in either metadata file performed that check. - -### โš ๏ธ A file missing from its barrel is validated by nothing - -Registration is explicit, file by file: each `src/{type}/index.ts` re-exports its files and -`objectstack.config.ts` feeds those barrels to `defineStack()`. A new file that compiles and -satisfies its schema but never reaches the barrel is **silently ignored** โ€” measured: a valid -new `src/objects/*.object.ts` left out of `src/objects/index.ts` leaves `pnpm validate` at -exit 0, still reporting `Data: 18 Objects`, naming the new file nowhere in its output. -Exporting it is part of authoring it, not a follow-up. - -### `XSchema.parse()` is a real API โ€” for tests, not for `src/` - -Those schemas are genuine exports and do carry `.parse()`: `ObjectSchema` (`/data`), -`PageSchema` / `ViewSchema` / `DashboardSchema` / `FormViewSchema` (`/ui`), `FlowSchema` / -`StateMachineSchema` (`/automation`), `PluginSchema` (`/kernel`), `PermissionSetSchema` -(`/security`), `AgentSchema` (`/ai`). Calling one is right in a **test**, or when building -metadata programmatically โ€” `content/docs/customization/testing-and-ci.mdx` shows that shape, -and `scripts/analytics-reconcile/run.ts` calls `DatasetSchema.parse()`. It is โ›” not the -authoring form for a file under `src/`. - -โš ๏ธ Several of those types have **no instance in this app**: no `*.form.ts`, -`*.statemachine.ts`, `*.capabilities.ts` or `*.agent.ts` exists under `src/`, and -`pnpm validate` reports `Runtime: 0 plugins`. App-authored agents were **retired** (#512) and -the AI surface is skills-only (ADR-0063 ยง2) โ€” `src/skills/*.skill.ts` via `defineSkill()`, -attached to a platform agent by `surface`. โ›” Do not author a `*.agent.ts`. - -> **There is no `workflow` metadata type** (ADR-0019/0020): `WorkflowRuleSchema` is not -> exported by any installed `@objectstack/*` package, and `ObjectSchema` rejects -> `workflows:` / `workflow:` by name. Field updates belong in `*.hook.ts`; status flips and -> notifications in a `record_change` / `schedule` flow; approvals in an `approval` node -> inside a flow. A record **lifecycle** constraint is not a `StateMachineSchema` either โ€” it -> is a `validations[]` entry with `type: 'state_machine'` on the object, validated with the -> rest of the object by `ObjectSchema.create()`; `StateMachineSchema` is a different shape -> (`initial` / `states` / `on`) and does not validate that entry. Whether a given constraint -> wants an invariant or a transition gate at all is decided by **Metadata semantics rule 7** -> below. - -## ๐Ÿท๏ธ Field Type Guidance +**Author the file the way its neighbours are authored.** The form is **not uniform across metadata types**: open the file next to the +one you are creating and copy its shape โ€” the directory is the template. โ›” Do not generalise from one directory to its neighbour; the +`define*` names are the trap (`defineView()` / `defineSkill()` are the form for their directories; `defineFlow()`, exactly +`FlowSchema.parse()`, is โ›” not the form for `src/flows/`). Three forms exist: -Use the most specific `Field` type available from `@objectstack/spec/data`: +1. **A validating constructor, called in the file.** `src/objects/*.object.ts` uses `ObjectSchema.create({ โ€ฆ })` (`@objectstack/spec/data`), + `src/views/*.view.ts` uses `defineView({ โ€ฆ })`, `src/skills/*.skill.ts` uses `defineSkill({ โ€ฆ })`. These reject at author time and + name the unknown key back (`unknown key(s) โ€” workflows`). +2. **A typed object literal, no runtime call in the file.** `src/pages/*.page.ts` annotate with `Page`, `src/dashboards/*.dashboard.ts` + with `Dashboard` (both `@objectstack/spec/ui`), `src/flows/*.flow.ts` with `Automation.Flow` (`import type * as Automation from '@objectstack/spec/automation'`). +3. **A plain object literal with no schema import.** `src/profiles/*.profile.ts` โ€” this app's **permission sets**; โ›” not + `*.permission.ts`, authored nowhere โ€” and `src/sharing/*.sharing.ts`. -| Relationship | Field Type | When to Use | -|---|---|---| -| Parent reference | `Field.lookup()` | Optional association to another object | -| Child of parent | `Field.masterDetail()` | Required parent-child with cascade delete | -| Rollup value | `Field.summary()` | Aggregate child records (sum, count, min, max) | +**Where the validation actually happens.** `objectstack.config.ts` hands every collection to `defineStack()`, which validates each against +its `@objectstack/spec` schema; `pnpm validate` and `pnpm build` run it, and the platform parses again on boot. An unknown key on a page or a permission set fails `pnpm validate` with exit 1 though the file imports nothing. -| Data Type | Field Type | When to Use | -|---|---|---| -| Multiple choices | `Field.select({ multiple: true })` | Multi-select picklist | -| File upload | `Field.file()` | Document/attachment fields | -| Image upload | `Field.image()` | Photo/avatar fields | -| GPS coordinates | `Field.location()` | Geographic location data | -| Mailing address | `Field.address()` | Structured postal address | +**โš ๏ธ A file missing from its barrel is validated by nothing.** Registration is explicit, file by file: each `src/{type}/index.ts` +re-exports its files by name and `objectstack.config.ts` feeds those barrels to `defineStack()` โ€” no glob discovery. A valid file that +never reaches the barrel is **silently ignored**: `pnpm validate` stays at exit 0 and names it nowhere. Exporting it is part of authoring it. -## ๐Ÿงฉ Metadata semantics โ€” say what you mean (2026-08-31 ruling) +**`XSchema.parse()` is a real API โ€” for tests, not for `src/`.** `ObjectSchema`, `PageSchema`, `ViewSchema`, `FlowSchema`, +`PermissionSetSchema` and their siblings carry `.parse()`; call it in a **test** or when building metadata programmatically, โ›” never as the authoring form of a file under `src/`. -Five rules on which construct carries which intent, plus the escape-hatch clause that -closes the chapter โ€” distilled from the 2026-08-31 rulings (verbatim source: -**objectstack#13848**). Prose in a `description` is not one of those constructs. +> **There is no `workflow` metadata type** (ADR-0019/0020): `WorkflowRuleSchema` is exported by no installed `@objectstack/*` package, +> and `ObjectSchema` rejects `workflows:` / `workflow:` by name. Field updates belong in `*.hook.ts`; status flips and notifications in +> a `record_change` / `schedule` flow; approvals in an `approval` node inside a flow. A record **lifecycle** constraint is a +> `validations[]` entry with `type: 'state_machine'` on the object, โ›” not a `StateMachineSchema` file; whether it wants an invariant or +> a transition gate at all is **Metadata semantics rule 7**. -**7. Invariant, or transition gate โ€” pick the tool by the intent.** -An **invariant** ("X may never exceed Y") is a `validations[]` script: existing -violations are frozen rather than bricked. A **transition gate** ("by the time the record -reaches state S, X must be filled") is `requiredWhen`, or a bound on the field: records -that predate the rule are let through. โ›” Do not let prose describe a transition gate as -an invariant. Which construct a lifecycle constraint takes is the same question the -Schema Validation note answers for state machines. +## ๐Ÿท๏ธ Field Type Guidance -> Precedent: #1069 (its platform-teaching half is objectstack#13879). +Use the most specific `Field` type `@objectstack/spec/data` offers: -**8. Interception stands on a person's judgement.** -A machine signal (`suspected`) warns and lets the write through. Only a value a person -wrote down (`confirmed`) may block one. โ›” Do not build an override escape hatch. +| Need | Field type | +|---|---| +| Optional association to another object | `Field.lookup('crm_x', โ€ฆ)` | +| Required parent-child with cascade delete | `Field.masterDetail('crm_x', โ€ฆ)` | +| Aggregate over child records (sum, count, min, max) | `Field.summary()` | +| Multi-select picklist | `Field.select({ multiple: true })` | +| File ยท image ยท GPS ยท postal address | `Field.file()` ยท `Field.image()` ยท `Field.location()` ยท `Field.address()` | -> Precedent: #1288. +## ๐Ÿงฉ Metadata semantics โ€” say what you mean (2026-08-31 ruling) -**9. Elevate as little as possible.** -A screen flow stays `runAs: 'user'`. A write that genuinely needs elevation is split into -a dedicated `system` sub-flow and called through a `subflow` node. โ›” Do not elevate a -whole flow to make `readonly` take effect. `close_case` is a historical precedent, not a -policy. +Which construct carries which intent (verbatim source objectstack#13848). Prose in a `description` is not a construct. -> Precedent: #1434. +**7. Invariant, or transition gate โ€” pick the construct by the intent.** An **invariant** ("X may never exceed Y") is a `validations[]` +script; existing violations are frozen, not bricked. A **transition gate** ("by the time the record reaches state S, X must be filled") +is `requiredWhen`, or a bound on the field; records that predate the rule pass. โ›” Never let prose describe a gate as an invariant. (#1069) -**10. The organization dimension.** -Any `runAs: 'system'` scan or rollup MUST pin an organization predicate โ€” the #1363 guard -is the acceptance criterion. The tenant column (`organization_id`) is a uniform platform -capability, injected by default: โ›” do not declare it object by object, and โ›” do not "add -a tenant dimension" object by object. +**8. Interception stands on a person's judgement.** A machine signal (`suspected`) warns and lets the write through; only a value a person wrote down (`confirmed`) may block one. โ›” Do not build an override escape hatch. (#1288) -> Precedents: #1372 ยท #1177 (the mechanism reading). +**9. Elevate as little as possible.** A screen flow stays `runAs: 'user'`; a write that genuinely needs elevation is a dedicated `system` sub-flow called through a `subflow` node. โ›” Never elevate a whole flow to make `readonly` take effect. (#1434) -**11. A deliberate deviation must be written down.** -A choice that departs from a repo-wide convention โ€” a demo position left empty, a board -that keeps `resolved`, a standing grant for the review window, two priority vocabularies -โ€” carries a comment beside the code **and** an entry in the roster of the guard that -would otherwise flag it. Without both, the next agent tidies it away. +**10. The organization dimension is the platform's.** Every `runAs: 'system'` scan or rollup MUST pin an organization predicate โ€” the +#1363 guard is the acceptance criterion. `organization_id` is injected by the platform: โ›” never declare it, or "add a tenant dimension", object by object. (#1372) -> Precedents: #1102 (the fence was strengthened) ยท #1328 (the boundary roster) ยท #1342 -> (the vocabulary comment). +**11. A deliberate deviation is written down twice.** A choice that departs from a repo-wide convention carries a comment beside the +code **and** an entry in the roster of the guard that would otherwise flag it; without both, the next agent tidies it away. (#1328) -**Escape hatches are for extreme cases โ€” layout is derived by default.** -Maintainer ruling, 2026-08-31 (verbatim, kept untranslated): +**Escape hatches are for extreme cases โ€” layout is derived by default.** Maintainer ruling, 2026-08-31 (verbatim, untranslated): > ใ€Œๆˆ–่€…่ฏด skills ๅบ”่ฏฅ่ฏดๆ˜Ž๏ผŒ้€ƒ็”Ÿไป“ๆ˜ฏๆž็ซฏๅœบๆ™ฏๆŒ‰็…งๅฎขๆˆท้œ€ๆฑ‚่‡ชๅฎšไน‰็š„ๅœบๆ™ฏไธ‹ๆ‰้œ€่ฆ๏ผŒๅบ”่ฏฅๅฐฝ้‡้ฟๅ…ใ€‚ใ€ -A layout escape hatch โ€” authoring `record:details` sections on a custom record page, or -enumerating fields in a view's `form.sections` โ€” is reached for **only** in the extreme -case, a named customer-demanded customization. โ›” Avoid it everywhere else. The ladder, -in order: - -1. **`fieldGroups` on the object, each field opting in with `group: ''`** โ€” the - norm, and it authors no sections at all: the layout is *derived*. -2. **The group-reference form** (`{ group: '' }`, objectstack#13897) when partial - arrangement is genuinely needed. No in-repo example is cited on purpose โ€” nothing here - uses it yet, so read the shape off the spec rather than off a neighbour. -3. **Per-field enumeration** only in the extreme case, and then with a comment beside the - code naming the customer need and why a group reference cannot express it (composing a - capture across groups; a wizard or pane structure). That comment is rule 11 applying - itself, not an extra ask. - -> Mechanism: objectstack#13855 ยท objectstack#13897. - -## ๐Ÿš€ Development Workflow - -1. **Define Object**: Create `src/objects/{entity}.object.ts`. -2. **Add Logic**: Create `src/objects/{entity}.hook.ts`. -3. **Expose Action**: Create `src/actions/{entity}.actions.ts` if external API/AI needed. -4. **Config UI**: Create `src/views/{entity}.view.ts` and `src/pages/{entity}.page.ts`. +Authoring `record:details` sections on a custom record page, or enumerating fields in a view's `form.sections`, is for a named, +customer-demanded customization only. The ladder: (1) `fieldGroups` on the object, each field opting in with `group: ''` โ€” the +norm, the layout is *derived*; (2) the group-reference form `{ group: '' }` (objectstack#13897) when partial arrangement is genuinely +needed; (3) per-field enumeration only in the extreme case, with a comment naming the customer need and why a group reference cannot express it โ€” rule 11 applying itself. ## โš ๏ธ Constraint Checklist -- **Object Naming**: All HotCRM business objects MUST be prefixed with `crm_` (e.g. `crm_account`, `crm_opportunity`). The roster is deliberately **not** restated here โ€” `src/objects/*.object.ts` is its source of truth. *Supersedes the hand-maintained fifteen-name list that stood in this bullet, which had already drifted three objects behind the tree โ€” 2026-08-31 ruling, item 5.* All references โ€” `Field.lookup(...)` / `Field.masterDetail(...)` targets, cube `sql`, view `data.object`, hook `object`, navigation `objectName`, action `objectName`, dashboard `object` โ€” MUST use the prefixed form. Platform objects keep their existing `sys_*` prefix. -- **i18n**: Every new object must have entries in all 4 locale files (`src/translations/{en,zh-CN,es-ES,ja-JP}.ts`) โ€” label, pluralLabel, all field labels + option labels, view labels, navigation labels. No new feature ships without all 4 locales. -- **Docs**: Every new object/feature requires user-facing documentation under `content/docs/` (e.g. `getting-started/`, `guides/`, `marketing/`, `analytics/`, `administration/`) written for business users + admins (not developers) โ€” business concepts, never a hand-copied machine roster (see **Documentation discipline** below). -- **Documentation**: written in English, then translated โ€” `content/docs` ships `.zh-Hans.mdx` and `.zh-Hant.mdx` pages beside the English ones, and that Chinese surface follows the three rules under **Documentation discipline** below. *Supersedes "All documentation MUST be in English", a blanket the shipped tree already contradicted โ€” 2026-08-31 ruling, item 6.* -- **Validation predicates must be TOTAL**: every `record.x` read in an authored - CEL predicate โ€” `validations[].condition`, `requiredWhen`, `readonlyWhen`, - `visibleWhen` โ€” carries a `has(record.x)` guard. See below. -- **No Engine Code**: Do not try to modify the core runtime code. Focus on the *usage* of the runtime. -- **Dependencies**: HotCRM depends on the published `@objectstack/*` packages (runtime, spec, drivers, services) declared in `package.json`. Keep `specVersion` in `objectstack.manifest.json` aligned with the installed `@objectstack/spec`. -- **Tone**: Act as a Senior 10x Engineer. Be concise, professional, and technically accurate. - -> **Naming note (ADR-0048):** the `crm_` prefix above is a deliberate HotCRM -> convention for **grep-ability**, and it is enforced for **objects**. It is -> *not* required for collision avoidance: as of ObjectStack 9.4 the cross-package -> collision throw was retired (ADR-0048 ยง3.4) โ€” packages coexist via -> `packageId`-scoped resolution. So the `os lint` `naming/namespace-prefix` -> warning on non-object items (pages/flows/datasets/etc.) is advisory only; its -> "fail at install" wording is stale. Don't mass-rename UI/automation items to -> chase that warning. +- **Object Naming**: `crm_` prefix on every business object, written out (Tech Stack rule 2). +- **i18n**: every new object ships in all four locale packs, `src/translations/{en,zh-CN,es-ES,ja-JP}.ts` โ€” label, pluralLabel, every field and option label, view labels, navigation labels. +- **Docs**: every new object or feature gets a user-facing page under `content/docs/` for business users and admins โ€” business concepts, + never a hand-copied machine roster; English first, then `.zh-Hans.mdx` and `.zh-Hant.mdx` beside it, under **Documentation discipline** below. +- **Validation predicates must be TOTAL**: every `record.x` read in an authored CEL predicate (`validations[].condition`, `requiredWhen`, + `readonlyWhen`, `visibleWhen`) carries a `has(record.x)` guard. +- **Dependencies**: the `@objectstack/*` packages in `package.json` are version-locked and bumped together; keep `specVersion` in + `objectstack.manifest.json` aligned with the installed `@objectstack/spec`. ### Validation predicates must be TOTAL (#630) -A validation rule is evaluated against `{...previous, ...data}`, and the engine -fills absent fields with `null` **only on insert**. On update, `previous` is -whatever the driver returned โ€” and a driver that stores only the columns a row -was actually written with (`driver-memory`, `driver-mongodb`) hands back a -record with the key **absent**, not null. Strict CEL then aborts the whole -predicate with `No such key` โ€” and what the engine does next changed under us: - -``` -โ‰ค 17.0.0-rc.1 WARN Validation rule 'x' predicate failed to evaluate (โ€ฆ) โ€” skipped -โ‰ฅ 17.0.0-rc.2 WARN โ€ฆ โ€” write rejected (#4649) - ValidationError: Validation rule 'x' could not be evaluated โ€ฆ โ€” write rejected. -``` - -Through rc.1 a rule that could not answer required nothing at all, silently. -From rc.2 it **fails closed**: the same abort rejects the save. Neither is the -rule the author wrote โ€” one under-enforces, the other blocks an ordinary save on -a record shape nobody considered โ€” and one guard prevents both. So **every -`record.x` read carries a `has(record.x)` guard**: +A rule evaluates against `{...previous, ...data}`; on update, a driver that stores only the columns a row was written with hands back +the key **absent**, not null. Strict CEL aborts the predicate with `No such key`, and since 17.0.0-rc.2 an unevaluable rule **rejects +the write** (before rc.2 it was skipped in silence). Neither is the rule you wrote, so: | intent | write this | | --- | --- | | `x` holds no value | `(!has(record.x) \|\| isBlank(record.x))` | | `x` holds a value | `has(record.x) && record.x โ€ฆ` | -`!= null` is **not** a substitute โ€” measured on an absent key, `record.f != null` -aborts exactly like `record.f < 0` does. It guards a different hazard -(`dyn < int`); numeric comparisons need both guards. `has()` is the only -total accessor: `coalesce(record.f, "")` aborts too, because its argument is -evaluated before the call. - -`test/object-validation-predicates.test.ts` enforces this two ways โ€” a grep for -the guard, and a run of every predicate through the engine's own -`evaluateValidationRules` against a record with no keys at all. That file also -carries the full measurement table, the driver-by-driver findings, and why this -route was chosen over making the in-memory test driver column-complete. Read it -before adding a rule. +`!= null` and `coalesce(record.x, "")` are โ›” not substitutes โ€” both abort on an absent key. `test/object-validation-predicates.test.ts` enforces the guard and carries the measurement; read it before adding a rule. ### Documentation discipline (2026-08-31 ruling) -**5. Docs explain business concepts. โ›” They never hand-copy a machine list.** -The single source of truth for a machine fact โ€” a dataset's dimensions, an object's -fields, the roster of objects itself โ€” is the self-describing metadata under `src/`. -A table transcribed into prose drifts, and this repo has measured it drifting four -times. The boundary is one question: **can a reader see this directly in the product -UI?** A navigation fact (which views a user is offered) may be documented and guarded; -a machine semantic layer may not. - -> Drift measured: #610, #965, #977, #1228. Boundary precedents: #1422 (documentable) -> against #1329 / #1326 (not). +**5. Docs explain business concepts. โ›” They never hand-copy a machine list.** The source of truth for a machine fact โ€” a dataset's +dimensions, an object's fields, the roster of objects โ€” is the metadata under `src/`; a table transcribed into prose drifts, and this +repo has measured it drifting repeatedly. The boundary is one question: **can a reader see this directly in the product UI?** A +navigation fact may be documented and guarded; a machine semantic layer may not. This rule binds this file too. (#1620) **6. The Chinese doc surface has three rules.** -- UI nouns take the **zh-CN language-pack** wording. โ›” Never coin a fresh translation - for something the app already labels (#1329). -- A Chinese heading carries an **explicit English anchor id**, and one anchor word is - used across every language, so a link survives translation (#1359). +- **List view names** take the **zh-CN language-pack** wording โ€” โ›” never coin a fresh translation for a view the app already labels + (#1329). Every other pack-carried UI noun โ€” dashboard and tile titles, gauges, dataset dimensions and measures, sharing-rule names, + the dashboard's own label โ€” keeps its **English** spelling in every locale even though the pack carries a translation: + `test/docs-dashboard-tiles.test.ts` resolves each `**Name** ็ฃ่ดด` to an English widget title and goes red on a converted one. The + guard's ablation is the discriminator, not pack-carriage; measures are the same class and currently unguarded (#1618). +- A Chinese heading carries an **explicit English anchor id**, and one anchor word is used across every language, so a link survives translation (#1359). - zh-Hant conventions are stated by their **real** reason, not a style preference: the console falls back to Simplified, so a Traditional page labels platform navigation in English rather than ship mixed Simplified/Traditional script (#1368). ## โฌ†๏ธ Platform Upgrades (ObjectStack version bumps) -When upgrading the `@objectstack/*` dependency line, **start from the official -release notes โ€” do not reverse-engineer breaking changes from `node_modules` -changelogs**: - -1. **Read the release notes first**: - (per-major pages, e.g. `/docs/releases/v14`, carry the breaking-change list - and a migration checklist with before/after examples). -2. Per-package details live in each package's `CHANGELOG.md` - (`node_modules/@objectstack//CHANGELOG.md`) โ€” use these to supplement, - not replace, the release notes. Breaking changes reference ADRs for rationale. -3. Bump all `@objectstack/*` packages **together** (they are version-locked), - update `specVersion` in `objectstack.manifest.json` to the new major, - then run the full verify suite and browser-verify (see below). -4. Record the upgrade in the PR's **changeset**, not in `CHANGELOG.md`. The - changeset *is* the upgrade's release-notes entry, and it carries exactly what - this step has always asked for โ€” what changed on the platform, what metadata - was migrated and why. It is the entry ยงVerifying changes already requires of - every PR; the upgrade does not get a second one. - โ›” `CHANGELOG.md` is not hand-edited. `changeset version` owns it: it splices - each release in under the `# Changelog` title and keeps everything already in - the file below that, byte-for-byte and unread. So the generated section at the - top is machine territory, and the hand-written history under it was closed when - the 3.0.0 release consumed the pending changesets โ€” an entry appended there is - published by nothing, and one written at the top duplicates your changeset. - -> A platform fix that has **merged** is not yet a fix you can use. Before resuming a card -> that was blocked on one, confirm the pinned `@objectstack/*` version actually carries it -> โ€” that is the second half of Scope rule 2. - -## ๐Ÿšซ Scope โ€” a pure metadata application (2026-08-31 ruling) - -Maintainer ruling, 2026-08-31 (verbatim, kept untranslated): - -> ใ€ŒๅŸบไบŽไปฅไธŠๅ†ณ่ฃ๏ผŒๅฏนไบŽhotcrm ๅ…ƒๆ•ฐๆฎ้กน็›ฎ๏ผŒ้œ€่ฆ่กฅๅ……ๅ“ชไบ› agents.md ๏ผŒๆฏ”ๅฆ‚็บฏๅ…ƒๆ•ฐๆฎๅบ”็”จๅบ”่ฏฅ็ฎ€ๅŒ–๏ผŒๅชๅผ€ๅ‘ไธšๅŠกๅŠŸ่ƒฝ๏ผŒๅนณๅฐ่ƒฝๅŠ›ๅ…จ้ƒจๅœจๅนณๅฐๅผ€ๅ‘ใ€‚ใ€ - -The four rules below are distilled from that day's rulings. The verbatim quotes and the -precedent ledger they were drawn from are single-sourced in **objectstack#13848** โ€” -read that card before arguing with one of these. Every rule carries its precedent cards -on purpose: they are tombstones, and they are what stops the same card being filed again -next quarter. - -### 1. What HotCRM is - -HotCRM is the **simplified implementation of business capability**: metadata authored -under the platform spec, guided by the platform's published skills, checked for legality -by the `os` commands (`pnpm validate`, `pnpm lint` here). โ›” Do not re-invent what the -platform already owns. - -**Build business features here; build platform capability on the platform.** A gap you -find in the platform is filed as a platform card upstream โ€” โ›” it is never compensated -for in this repo. - -> Precedents: #806 โ†’ objectstack#13855 ยท #1203 โ†’ objectstack#13889 ยท #1212 โ†’ -> objectui#7063 ยท #1247 โ†’ objectui#7064 ยท #1301 โ†’ objectstack#13894 ยท #1185 โ†’ -> objectstack#13881 ยท #1069 โ†’ objectstack#13879 ยท #1231 (credential mechanism voided). - -### 2. A platform defect means you WAIT for the platform fix - -โ›” Do not route around it โ€” no defensive coding, no shape tolerance, no hand-written -predicate re-implementing a rule that lives inside the platform. โ›” Do not split a ruling -into "the half we can land now" and land that half. - -File the blocked card and make the dependency machine-visible with a `Blocked-by:` line -pointing at the platform card. When the fix lands, resume the **original** ruling โ€” and -before resuming, confirm the pinned version actually contains it (**merged is not the -same as available in the pin**; see Platform Upgrades) and re-run the defect card's own -fixture. Red means stop. - -> Precedent: #549 / objectstack#11082 / objectstack PR #11183 โ€” waiting was proved right. - -### 3. โ›” Do not build platform-level tooling here - -Lint, validation, gates and diagnostics belong to the platform, uniformly. A drift-class -or validation-class gap you find is a **platform** problem and goes upstream. - -Tests in this repo pin **this repo's own business facts** and nothing else โ€” a seed-row -pin, a "doc wording equals the language-pack label" guard. โ›” Do not grow a gate farm. - -> Precedents: #806 (its lint half was retired) ยท #1423 (the comment-volume gate was -> deliberately not built). - -### 4. A bad platform default is fixed at the default - -Writing an `emptyState` string on every tile, or hand-tuning which tile a row lands on to -dodge a collapsing render, is paying the same tax over and over again. Change the default -upstream instead. - -> Precedents: #1212 โ†’ objectui#7063 ยท #1247 โ†’ objectui#7064. - -### Out of scope: platform features - -The following are **NOT** in HotCRM's scope โ€” they are platform-level features provided by `@objectstack/runtime` or other platform packages: - -- **Platform infrastructure**: visual workflow/process/approval builders, formula builder, report & page-layout designers. -- **Low-level services**: database engine, auth (OAuth/SAML/SSO), multi-tenancy, encryption, API gateway, caching, message queue, file storage. -- **Dev tools**: schema migration, CLI scaffolding, metadata deployment pipeline, VCS integration, IDE extensions. - -**Focus Area**: HotCRM focuses exclusively on **business domains** (CRM, Finance, HR, Marketing, Products, Support) and their **business logic, data models, and AI capabilities** โ€” authored as metadata in `src/`. +1. **Read the official release notes first** โ€” (per-major pages carry the breaking-change list and a migration checklist). โ›” Do not reverse-engineer breaking changes from changelogs. +2. Per-package `node_modules/@objectstack//CHANGELOG.md` supplements the release notes, never replaces them. +3. Bump all `@objectstack/*` packages **together** (they are version-locked), update `specVersion` in `objectstack.manifest.json`, then + run `pnpm verify` and browser-verify (see below). +4. Record the upgrade in the PR's **changeset**, โ›” not in `CHANGELOG.md`: `changeset version` owns that file and splices each release in, so a hand-written entry is published by nothing. +5. **Re-scope the claims that name the old pin โ€” re-scope, never renumber.** Search version-agnostically, across wrapped lines and comment leaders, for the *shape* of a pin claim, over identifiers **and** human-readable labels; every hit is a candidate to classify by hand โ€” a claim is false only when it asserts the old version **is** the current pin (re-measure it on the new pin, or re-word it to date itself), while "measured on X" is dated truth and stays. (#1681) + A grep finds only text carrying a version token; the same stale fact stated in other words, or by a label, is found by reading โ€” say so in the PR rather than call the sweep complete. ## โš–๏ธ Ruling discipline (2026-08-31 ruling) -**12. Look for an existing ruling before you escalate.** -When a card already records a maintainer ruling, โ›” do not re-escalate it and โ›” do not -re-decide it. The authoritative ledger of rulings is the director seat's pinned post -(**objectstack#13766**, ruling B); the comments on the card itself are the detail notes. - -> Precedent: #1198 โ€” the 8/22 mistake. - ---- +**12. Look for an existing ruling before you escalate.** When a card already records a maintainer ruling, โ›” do not re-escalate it and โ›” do not re-decide it. The ledger of rulings is the director seat's pinned post (objectstack#13766, ruling B); the card's own comments are the detail notes. (#1198) ## โœ… Verifying changes -### Verify before opening a PR +**Verify before opening a PR.** Run `pnpm verify` and make sure it is green โ€” `package.json` is the single source of truth for what that +chain runs; `pnpm validate` inside it also enforces ADR-0021 widget binding (`xAxis` is a dataset **dimension**, `yAxis[]` a **measure**). -Run the repo's own verify chain and make sure it's green: - -``` -pnpm verify -``` - -`package.json` is the single source of truth for what that chain runs. *Supersedes the -hand-copied `pnpm validate && pnpm typecheck && pnpm build && pnpm test` line that stood -here and named half of it โ€” 2026-08-31 ruling, item 5.* - -`pnpm validate` enforces ADR-0021 dashboard-widget binding integrity: a chart's -`chartConfig.xAxis.field` must resolve to a dataset **dimension** and -`yAxis[].field` to a **measure**, regardless of chart orientation (the renderer -handles the visual flip). A swapped axis is a hard validation error. - -### Every PR carries a changeset - -A PR is not finished until it adds a `.changeset/*.md` entry. Run `pnpm changeset` -(or hand-write the file) and commit it with the rest of the change โ€” the -`Changeset Check` workflow diffs against the PR base and fails when the PR adds -none. Counting the files already in `.changeset/` proves nothing: the directory -always holds a `README.md` plus whatever is awaiting the next release, so only -what *this* PR adds counts. - -Write the summary for the release-notes reader โ€” what changed and why it matters, -not which files moved. A breaking change must state the FROM โ†’ TO migration in -the body; that text ships to consumers as `CHANGELOG.md`. - -The lone exception is the **`skip-changeset`** label, for PRs that ship nothing to -users (CI-only chores, repo housekeeping). Do not reach for it to get a red check -green โ€” write the changeset instead. +**Every PR carries a changeset.** A PR is not finished until it adds a `.changeset/*.md` entry โ€” the `Changeset Check` workflow diffs +against the PR base, so files already in the directory prove nothing. Write it for the release-notes reader (what changed and why; +FROM โ†’ TO for a breaking change); it ships as `CHANGELOG.md`. The lone exception, for a PR that ships nothing to users, is the +**`skip-changeset`** label or an empty-frontmatter changeset โ€” โ›” never to turn a red check green. ### How a green PR lands -**A seat may take a PR out of draft and arm auto-merge once every check on it has -finished and none has failed โ€” unless its diff touches a governed path.** The governed -paths are `AGENTS.md`, `CLAUDE.md`, `.claude/**` and `.github/instructions/**`, and one -governed path governs the whole diff however small that part of it is. A governed PR -**stays a draft** and is the maintainer's own merge: โ›” a seat never flips it ready and -โ›” never arms auto-merge on it. Arming auto-merge is a seat's only landing route โ€” -โ›” never merge by hand. A check that concluded `skipped` (`Check Changeset` under the -`skip-changeset` label) or that never ran because a path filter excluded it -(`link-check` on a diff carrying no `.md`) is not a failure. - -**Arming does not itself merge โ€” it enqueues.** This repo's `main` ruleset carries a -**merge queue**, and the queue is what performs the (squash) merge. Because a seat arms -only once every check has finished, the enqueue fires at once and the queue merges within -seconds โ€” a green PR that pauses briefly after arming is landing, not stuck. โ›” Do not -re-derive a five-minute wait from the ruleset's `min_entries_to_merge_wait_minutes`: that -parameter caps how long the queue *gathers* a group, and never engages while -`min_entries_to_merge` is `1`. - -Written down because an unwritten landing rule is re-derived by every seat, and -re-derivation is where seats diverge โ€” invisibly, until the divergence produces a merge -nobody authorised. - -> Ruling: #1742, decision batch #81 (2026-09-08), which chose this over "every PR waits -> for the maintainer". Governed surfaces graded on #1233. *The queue is named here on -> #1815 โ€” PR #1798 omitted it after two probes that cannot see a queue at rest; the -> repo's **rulesets** are the channel that answers, and the ruling's own "the merge -> queue remains the only route in" was right.* +A seat may take a PR out of draft and arm auto-merge once every check on it has finished and none has failed. Arming **enqueues**: +`main`'s ruleset carries a merge queue that performs the squash merge within seconds โ€” โ›” never merge by hand, and do not re-derive a +wait from `min_entries_to_merge_wait_minutes`. A check that concluded `skipped` under a label, or never ran because a path filter +excluded it, is not a failure. **Governed paths โ€” `AGENTS.md`, `CLAUDE.md`, `.claude/**`, `.github/instructions/**` โ€” govern the whole +diff**, however small that part of it is: the PR stays a **draft** and is the maintainer's own merge; โ›” a seat never flips it ready and never arms auto-merge on it. (#1742) ### Verifying UI in the browser -The Console renders dashboards, charts, and views from metadata. When verifying a -change by driving the browser, follow these rules. - -#### Rule: wait for lazy-loaded UI before judging โ€” never conclude from an early screenshot - -Dashboard charts (`AdvancedChartImpl` / Recharts) and other heavy widgets are -**`React.lazy`-loaded** โ€” the chart bundle hydrates a beat *after* the page -navigates. A screenshot taken immediately after navigation shows **empty chart -cards even when nothing is wrong**. - -Do **not** report a widget as broken from a single early screenshot. Before -concluding anything about rendering: - -1. After navigating, wait ~1โ€“2s (or poll) for the lazy bundle to hydrate. -2. Confirm the chart actually drew via a DOM probe, not just a picture โ€” e.g. - count Recharts nodes: - `document.querySelectorAll('.recharts-pie-sector, .recharts-rectangle, .recharts-funnel-trapezoid, .recharts-area-area, .recharts-line-curve').length` - A non-zero count means it rendered; re-screenshot only once it's > 0. -3. Cross-check the data path: `POST /api/v1/analytics/dataset/query` returning - `200` with rows means the data is fine โ€” an empty visual is then either - hydration timing (wait) or a genuine renderer issue (investigate), but it is - **not** a data or metadata bug. - -All chart types (`funnel`, `donut`, `pie`, `bar`, `horizontal-bar`, `area`, -`line`, `table`) render correctly once settled. `gauge` renders as a single -numeric value (no dial yet โ€” by ADR-0021 design), which is expected, not a bug. - -> Origin: during the ObjectStack 9.4 upgrade an agent screenshotted a dashboard -> too early, saw blank funnel/donut cards, and wrongly reported the renderers as -> broken. They were fine โ€” it was the lazy-load race. Verify hydration first. - -#### Other browser-verify gotchas (same workflow) - -- **`better-sqlite3` native ABI mismatch.** If boot floods - `NODE_MODULE_VERSION ... requires ...` errors, the SQLite native binary was - built for a different Node ABI. Fix: `pnpm rebuild better-sqlite3`, then - restart the dev server. Not related to any app/code change. -- **Console dashboard route** is - `/_console/apps//dashboard/` - (e.g. `app.objectstack.hotcrm`), **not** `/_console/a/` โ€” the latter - bounces to `/_console/home`. -- Dev admin (seeded on an empty DB, dev only): `admin@objectos.ai` / `admin123`. +Dashboard charts and other heavy widgets are **`React.lazy`-loaded** and hydrate a beat after navigation โ€” an empty chart card right after navigating is the normal state, โ›” never a verdict: + +1. After navigating, wait ~1โ€“2 s (or poll) for the lazy bundle to hydrate. +2. Confirm the chart drew with a DOM probe, not a picture โ€” e.g. + `document.querySelectorAll('.recharts-pie-sector, .recharts-rectangle, .recharts-funnel-trapezoid, .recharts-area-area, .recharts-line-curve').length > 0`. +3. Cross-check the data path: `POST /api/v1/analytics/dataset/query` returning `200` with rows means data and metadata are fine; an empty + visual is then hydration timing or a renderer issue, โ›” never a metadata bug. `gauge` renders as a single number by ADR-0021 design. + +Gotchas, same workflow: a `NODE_MODULE_VERSION โ€ฆ requires โ€ฆ` flood on boot is `better-sqlite3` built for another Node ABI โ€” +`pnpm rebuild better-sqlite3`, restart the dev server, unrelated to any app change ยท the Console dashboard route is +`/_console/apps//dashboard/` (`app.objectstack.hotcrm`), โ›” not `/_console/a/`, which bounces to +`/_console/home` ยท dev admin, seeded on an empty DB (dev only): `admin@objectos.ai` / `admin123`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..4e612837f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,6 @@ +# CLAUDE.md โ€” HotCRM + +> **This file is a pointer. The single source of truth is [`AGENTS.md`](./AGENTS.md).** + +Read `AGENTS.md` in full before your first edit, and follow it. Nothing is restated here, so +there is exactly one instruction set to maintain. From 0f446c17f4b96c6f816a66246efd6b5f070411b9 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 14:20:14 +0000 Subject: [PATCH 2/2] docs(briefs): stop teaching a *.workflow.ts metadata type at its two surviving sites .github/instructions/automation.md section 1 and .github/tasks/autonomous_feature_dev.md step 4 taught a workflow metadata type no installed @objectstack/* package defines (WorkflowRuleSchema: 0 files under node_modules/@objectstack; control FlowSchema: 34). Both now route the reader to the three real destinations AGENTS.md names, mirroring its wording, and the automation example that also dropped the crm_ prefix is gone. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01MoTv7pn338AZ71owsp19gQ --- .github/instructions/automation.md | 36 +++++++------------------ .github/tasks/autonomous_feature_dev.md | 2 +- 2 files changed, 10 insertions(+), 28 deletions(-) diff --git a/.github/instructions/automation.md b/.github/instructions/automation.md index 262bd7b9b..31200bde9 100644 --- a/.github/instructions/automation.md +++ b/.github/instructions/automation.md @@ -2,33 +2,15 @@ You are the **Automation Specialist**. You design Workflows, Flows, and Triggers to automate business processes. -## 1. Workflow Rules (`.workflow.ts`) - -Declarative automation triggered by record changes. - -**Triggers**: `on_create`, `on_update`, `on_delete`, `schedule`. - -```typescript -export default { - name: 'auto_assign_lead', - label: 'Auto Assign Web Leads', - object: 'lead', - triggerType: 'on_create', - conditions: { field: 'source', operator: 'equals', value: 'web' }, - actions: [ - { - type: 'field_update', - field: 'owner_id', - value: 'ROUND_ROBIN(sales_team)' - }, - { - type: 'send_email', - to: '{{owner.email}}', - template: 'new_lead_assignment' - } - ] -} -``` +## 1. There is no `workflow` metadata type โ€” โ›” do not author a `*.workflow.ts` + +`WorkflowRuleSchema` is exported by no installed `@objectstack/*` package, and `ObjectSchema` +rejects `workflows:` / `workflow:` by name (ADR-0019/0020). A `*.workflow.ts` file is registered +by nothing and validated by nothing โ€” it is silently ignored. Route record-triggered automation +to the three real destinations (`AGENTS.md` ยงSchema Validation Requirements): field updates +belong in `*.hook.ts`; status flips and notifications in a `record_change` / `schedule` flow +(ยง2 below); approvals in an `approval` node inside a flow. Whatever the destination, the object +it names carries the explicit `crm_` prefix (`crm_lead`, never `lead`). ## 2. Visual Flows (`.flow.ts`) diff --git a/.github/tasks/autonomous_feature_dev.md b/.github/tasks/autonomous_feature_dev.md index 5b90ffe83..27dba5a13 100644 --- a/.github/tasks/autonomous_feature_dev.md +++ b/.github/tasks/autonomous_feature_dev.md @@ -14,7 +14,7 @@ Here is a prompt you can copy-paste to GitHub Copilot to trigger the **Autonomou 1. **Architecture Phase**: strictly follow the **"Feature-to-File" Mapping Strategy** in basic `architect.md`. Output a table of all files to be created. 2. **Metadata Phase**: Create the `*.object.ts` files first. These are the foundation. 3. **Review Phase**: Stop and ask me if the schema looks correct before proceeding to Logic and UI. -4. **Implementation Phase**: Once approved, generate the Logic (`.hook.ts`), Automation (`.workflow.ts`), and UI (`.page.ts`) files. +4. **Implementation Phase**: Once approved, generate the Logic (`.hook.ts`), Automation (`.flow.ts` โ€” there is no `workflow` metadata type, see `AGENTS.md` ยงSchema Validation Requirements), and UI (`.page.ts`) files. **Self-Correction**: - Ensure all lookup / master-detail targets (`Field.lookup(...)` / `Field.masterDetail(...)`) name real objects defined in Phase 2.