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. 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.