diff --git a/.changeset/agents-md-objectql-example.md b/.changeset/agents-md-objectql-example.md new file mode 100644 index 000000000..b6ade0d71 --- /dev/null +++ b/.changeset/agents-md-objectql-example.md @@ -0,0 +1,39 @@ +--- +'hotcrm': patch +--- + +Make `AGENTS.md`'s only ObjectQL example match the repo's real read path. + +The example under §"Tech Stack & Protocol" item 2 read +`broker.find('opportunity', { filters: [['amount', '>', 50000]] })`, and every part of +it was wrong for this codebase: + +- **`broker`** has zero occurrences in `src/`. The data surface an agent actually gets + is `ctx.api`: 43 call sites in `*.hook.ts` (cast once as `HookApi`, then + `api.object('crm_x')`) and 17 in action script bodies (`ctx.api.object('crm_x')`). +- **`filters`** is not a key of the in-process query object at all. It is the + *deprecated plural alias* of the `filter` HTTP query-param, whose value is a JSON + string. Used in process it fails silently, which is the failure mode this repo has + already paid for once — `.changeset/hook-query-where-not-filter.md` records seventeen + hook calls whose predicate vanished: `findOne` returned the object's first row and + `count` counted the whole object, with no error and no `null`. `HookQuery` in + `src/objects/_hook-api.ts` now omits the alias precisely so the compiler rejects it. +- **`'opportunity'`** violated the `crm_` prefix rule stated five lines above it in the + same file. The object is `crm_opportunity`. + +The predicate *value* changed shape too: `[['amount', '>', 50000]]` is a legal filter +AST at the platform level, but it is not assignable to `HookQuery['where']`, and no +`ctx.api` call site uses it — every predicate on that surface is written as an object. +So the example now does: `where: { amount: { $gt: 50000 } }`. + +The rule is stated *scoped to `ctx.api`* on purpose, because two other surfaces spell +their predicate differently and an unscoped rule would have invited the next agent to +"fix" them: + +- A flow node's `config` is a schema-unvalidated bag (`config: z.record(z.string(), + z.unknown())`) and `*.flow.ts` query/update nodes use `filter:` — 44 occurrences across + 17 of the 21 flow files, `where:` in none. +- A page component config uses `filter:` in the AST-array form, the one spelling of that + form in `src/` (`src/pages/lead_detail.page.ts:217`). + +Documentation only — no runtime, metadata, or dependency change. diff --git a/AGENTS.md b/AGENTS.md index e4e4f146b..e3c4b2efb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -59,9 +59,27 @@ hotcrm/ - **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. Cross-references via `reference_to` / `lookup` / `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**. + - 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: `broker.find('opportunity', { filters: [['amount', '>', 50000]] })`. + - 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`. `filter` (canonical) + and `filters` (deprecated alias) are *HTTP query-param* spellings carrying a JSON + **string**, not keys of the in-process query object — passing either in process fails + **silently**: `findOne` spreads the query into the AST without aliasing and returns the + object's **first row**, `count` reads `query.where` only and counts the **whole object**. + Neither throws. This repo has already paid for that once — see + `.changeset/hook-query-where-not-filter.md`. `HookQuery` in `src/objects/_hook-api.ts` + deliberately omits the alias so the mistake is a compile error. + Other surfaces are **not** governed by this rule and have their own spelling: a + `*.flow.ts` node `config` takes `filter:` (44 occurrences across 17 of the 21 flow + files; `where:` in none), and a page component config takes `filter:` in the + AST-array form (`src/pages/lead_detail.page.ts:217`). Do not "fix" one surface's + spelling into another's. 3. **AI-Native**: - Every feature should consider AI augmentation (Co-Pilot, Agents).