diff --git a/.changeset/retire-role-briefs.md b/.changeset/retire-role-briefs.md new file mode 100644 index 000000000..9efd4c7c7 --- /dev/null +++ b/.changeset/retire-role-briefs.md @@ -0,0 +1,7 @@ +--- +--- + +Agent instructions only — this PR releases nothing to HotCRM users, so the frontmatter +above is deliberately empty. The six per-role briefs under `.github/instructions/` are +retired and `AGENTS.md` is the one instruction set; no file under `src/`, no config and +no published artifact changes. diff --git a/.github/AGENT_GUIDE.md b/.github/AGENT_GUIDE.md index 03869fb94..441ef20ed 100644 --- a/.github/AGENT_GUIDE.md +++ b/.github/AGENT_GUIDE.md @@ -2,7 +2,7 @@ The following directories contain the "Brain" of the HotCRM development process. -- `instructions/`: **Role-Based Handbooks**. Define HOW to write code for specific domains (Metadata, Logic, UI). +- `/AGENTS.md`: **The one instruction set**. Defines HOW to write code in this repo; there are no per-role handbooks. - `tasks/`: **Prompt Templates**. Ready-to-use prompts to copy-paste into your AI chat window. - `copilot-instructions.md`: **System Prompt**. The meta-instructions for GitHub Copilot. @@ -11,4 +11,4 @@ The following directories contain the "Brain" of the HotCRM development process. 1. **Define**: Create an Issue using `ISSUE_TEMPLATE/ai_task.yml`. 2. **Prompt**: Copy a template from `tasks/` and fill it with details from the Issue. 3. **Execute**: Paste into Copilot/Cursor/Windsurf. -4. **Review**: Verify against the standards in `instructions/`. +4. **Review**: Verify against the standards in `/AGENTS.md`. diff --git a/.github/instructions/architect.md b/.github/instructions/architect.md deleted file mode 100644 index 85d400ae9..000000000 --- a/.github/instructions/architect.md +++ /dev/null @@ -1,59 +0,0 @@ -# Architect & Planner Instructions - -You are the **Chief Architect**. Your role is to break down vague business requirements into a concrete **Metadata Implementation Plan**. - -HotCRM is **one ObjectStack artifact built from four packages** (ADR-0130), not a -multi-package npm workspace: one `package.json`, one build. A directory under `src/` IS a -package — `src/sales/` (the `type: app` package), `src/service/`, `src/revenue/`, -`src/marketing/` — and inside each are the metadata-kind subdirectories it uses, with the -kind repeated as the file-name suffix (`src/sales/objects/.object.ts`, -`src/revenue/flows/.flow.ts`). - -Two rules decide every path you plan: - -1. **An item goes with the object it is authored against** — and a `*.hook.ts` goes beside - its `*.object.ts`, because a hook may not attach to another package's object. -2. **Imports go to the file's own directory or to `src/sales/`** — never sideways between - modules, never upward. A source more than one package needs lives in `src/sales/`, in - one copy. - -Plan file paths against the directories that actually exist: never plan an npm workspace -layout, and never invent a directory for a kind this repo has no home for. - -## The "Feature-to-File" Mapping Strategy - -When a user asks for "A Recruiting System", you must decompose it into the 4 Layers: - -### Step 1: Domain Modeling (Data Layer) -Identify entities. For "Recruiting", we need: -- `Candidate` (Person) -> `src//objects/candidate.object.ts` -- `Job Position` (The Opening) -> `src//objects/job_position.object.ts` -- `Application` (The Junction) -> `src//objects/application.object.ts` - -### Step 2: Process Definition (Automation Layer) -Identify state changes. -- "Send email on reject..." -> `src//flows/application-rejected.flow.ts` -- "Managers verify" -> `src//flows/offer-approval.flow.ts` - -### Step 3: User Experience (UI Layer) -Identify the screens. -- "HR needs to see pipeline" -> `src//views/application.view.ts` (Kanban) - -### Step 4: Security (Auth Layer) -Identify the actors. -- "Hiring Manager" -> `src/sales/profiles/hiring-manager.profile.ts` (permission sets are app-level) -- "Candidate controls own data" -> `src//sharing/candidate.sharing.ts` - -## Output Format - -Always start your response with the **Architecture Plan**: - -```markdown -## 🏗️ Architecture Plan: [Feature Name] - -| Component | File Path | Responsibility | -|-----------|-----------|----------------| -| Object | `src//objects/foo.object.ts` | Stores X data | -| Logic | `src//objects/foo.hook.ts` | Validates Y | -| UI | `src//pages/foo.page.ts` | Layout for Z | -``` diff --git a/.github/instructions/automation.md b/.github/instructions/automation.md deleted file mode 100644 index 31200bde9..000000000 --- a/.github/instructions/automation.md +++ /dev/null @@ -1,46 +0,0 @@ -# Automation Architect Instructions - -You are the **Automation Specialist**. You design Workflows, Flows, and Triggers to automate business processes. - -## 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`) - -Complex, multi-step processes (drag-and-drop equivalent). - -**Types**: `autolaunched` (Background), `screen` (UI Wizard). - -```typescript -export default { - name: 'order_approval_process', - label: 'Order Approval', - type: 'screen', // Requires user interaction - startNode: 'start', - nodes: [ - { id: 'start', type: 'start' }, - { - id: 'check_amount', - type: 'decision', - conditions: [ - { label: 'High Value', formula: 'amount > 100k', nextNode: 'manual_approve' }, - { label: 'Low Value', formula: 'amount <= 100k', nextNode: 'auto_approve' } - ] - }, - // ... nodes - ] -} -``` - -## 3. Trigger Registry (`.trigger.ts`) - -Registration of system-wide event listeners. - -Events: `record.*`, `user.*`, `system.*`, `schedule.*`. diff --git a/.github/instructions/logic.md b/.github/instructions/logic.md deleted file mode 100644 index 3b3233545..000000000 --- a/.github/instructions/logic.md +++ /dev/null @@ -1,237 +0,0 @@ -# Logic Engineer Instructions - -You are the **Backend Engineer** for HotCRM. You implement business rules using Hooks and Actions. - -## Capabilities - -1. **Hooks (`src/*/objects/.hook.ts`)**: Run code *before* or *after* database operations. -2. **Actions (`src/*/actions/.actions.ts`)**: Custom API endpoints or AI Tools. - -## 0. The data surface, before anything else - -Data access is **ObjectQL**, reached through the **`ctx.api`** surface, and every object name -carries the `crm_` prefix, written out: - -```typescript -await ctx.api.object('crm_opportunity').find({ where: { amount: { $gt: 50000 } } }); -``` - -⛔ There is **no `broker`** in this repo — the identifier has zero occurrences under `src/`, and -`@objectstack/runtime` exports no `Broker` to import. ⛔ There is no automatic prefix injection -either, so `'opportunity'` names an object that does not exist; the object is `crm_opportunity`. - -This brief taught `broker.find('opportunity', { filters })` for months — the same example #855 had -already retired from `AGENTS.md`, surviving here because these briefs sat outside every gate until -#1233. Everything below is measured against the pinned `@objectstack` **17.3.0** packages and the -shapes in `src/sales/objects/_hook-api.ts`. Write from it, not from memory of another stack. - -## 1. ObjectQL Query Protocol - -NEVER write SQL. - -**Reaching the surface.** In a `*.hook.ts` handler, cast once and guard, because the SDK types -`HookContext.api` as `unknown`: - -```typescript -import type { HookApi } from './_hook-api'; // src/sales/objects/_hook-api.ts — the shared shape -// inside the handler: -const api = ctx.api as HookApi | undefined; -if (!api) return; -const rows = await api.object('crm_opportunity').find({ where: { stage: 'proposal' } }); -``` - -In a `*.actions.ts` script body the runtime injects it directly: `ctx.api.object(...)`, no import. - -### Methods, and the options each one accepts - -| Call | Options | Returns | -|:--|:--|:--| -| `find(q)` | `where?`, `fields?`, `top?` | array of rows | -| `findOne(q)` | `where?`, `fields?`, `top?` | one row, or `null` | -| `count(q)` | `where?` — **and nothing else** | `number` | -| `insert(doc)` | — | the inserted row | -| `update(doc, options)` | the target `id` travels **inside `doc`**; `options` is `{ where }`, required | | -| `delete(options)` | `{ where }` — a predicate, never a bare id | | - -Three of these are asymmetries you cannot guess, so they are declared rather than remembered: - -- **`count` is narrower than `find`.** The engine's legal keys for `count` are `context, where`; - `count({ where, fields })` and `count({ where, top })` throw on every call. `HookCountQuery` - exists to make both a compile error — a projection or a row cap on a call that returns a - *number* is meaningless. -- **`top`, not `limit`,** is how this repo caps a read (`top` reaches the engine as a declared - alias of `limit`). It is legal on `find` / `findOne` only. -- **`update` has no `(id, doc)` overload.** `ctx.api.object(x)` is a repository facade, not an - id-addressed CRUD client: `update({ id, ...changes }, { where: { id } })`. - -### The predicate key is `where` - -- ✅ `where` is canonical and the only spelling this repo writes. -- ⛔ `filter` (singular) is a live *alias* of `where` on this engine — the predicate is applied, - nothing is dropped — but do not write it. `HookQuery` omits it so it is a **compile** error, - and `test/hook-query-predicate.test.ts` scans every `.ts` under `src/*/objects/` for it. One - idiom is the point: a query assembled in two places that ends up carrying **both** keys with - different values throws `Conflicting options … spellings of the same parameter`, and an empty - `where: {}` counts as a different value rather than as "no opinion". -- ⛔ `filters` (**plural**) is *not* an alias. The engine rejects any option it does not - recognise, so it throws `… does not recognise option 'filters'` — loudly, on the first call. -- ⛔ `findOne({})` with no predicate throws (`… selects no particular record`); it does not - return an arbitrary first row. - -### Filter syntax - -`where` is a **document**: `{ field: value }` is equality, `{ field: { $op: value } }` is -everything else, and sibling keys are ANDed. - -| Operator | Example | Meaning | -|:--|:--|:--| -| `$eq` / `$ne` | `{ stage: { $ne: 'closed_lost' } }` | equal / not equal | -| `$gt` `$gte` `$lt` `$lte` | `{ amount: { $gt: 50000 } }` | comparison | -| `$between` | `{ close_date: { $between: ['2026-01-01', '2026-03-31'] } }` | inclusive range | -| `$in` / `$nin` | `{ stage: { $nin: ['closed_won', 'closed_lost'] } }` | in / not in a list | -| `$contains` / `$notContains` | `{ name: { $contains: 'Acme' } }` | substring, case-**sensitive** | -| `$icontains` | `{ name: { $icontains: 'acme' } }` | substring, case-insensitive | -| `$startsWith` / `$endsWith` | `{ email: { $endsWith: '@acme.com' } }` | prefix / suffix | -| `$null` | `{ close_date: { $null: true } }` | is / is not null | -| `$exists` | `{ next_step: { $exists: true } }` | key present and non-null | - -Logical grouping is `$and`, `$or`, `$not`. ⛔ `$regex` and `$options` are **retired** — refused -everywhere with `INVALID_FILTER` / 400; reach for `$icontains` instead, choosing the substring the -pattern meant. - -### Usage Example - -```typescript -const api = ctx.api as HookApi | undefined; -if (!api) return; - -const expensiveDeals = await api.object('crm_opportunity').find({ - where: { amount: { $gt: 50000 }, stage: { $nin: ['closed_won', 'closed_lost'] } }, - fields: ['name', 'amount', 'owner_id'], - top: 500, -}); - -const openDeals = await api.object('crm_opportunity').count({ - where: { crm_account: accountId, stage: { $nin: ['closed_won', 'closed_lost'] } }, -}); -``` - -## 2. Hook Implementation Standard - -A hook lives at `src/*/objects/.hook.ts` and **default-exports** one `Hook` or an array of -them; the runtime associates the file with its object by naming convention — there is no barrel -entry and no imperative registration to add. - -**Events** are camelCase, and these eight are the entire vocabulary the schema accepts: -`beforeInsert`, `afterInsert`, `beforeUpdate`, `afterUpdate`, `beforeDelete`, `afterDelete`, -`beforeFind`, `afterFind`. ⛔ `before_create` / `after_create` and the other snake_case spellings -are not this stack's events; the enum rejects them. - -**The handler takes one argument, `ctx: HookContext`** — ⛔ not a broker, and there is no -`broker.context`. What you read off it: - -| `ctx.…` | What it is | -|:--|:--| -| `event` | which of the eight fired | -| `input` | the incoming document — **mutable**: a before-hook derives fields by assigning to it | -| `previous` | the pre-image, on update / delete | -| `user` | `{ id, name, email, … }` — **absent on system and seed writes**, this repo's system-write signal | -| `api` | the data surface in §1 | -| `id` | the target record id, where the event carries one | - -```typescript -// src/sales/objects/opportunity.hook.ts -import type { Hook, HookContext } from '@objectstack/spec/data'; -import type { HookApi } from './_hook-api'; - -const bigDealAlert: Hook = { - name: 'opportunity_big_deal_alert', - object: 'crm_opportunity', - events: ['afterUpdate'], - priority: 800, - async: true, - onError: 'log', - description: 'File a follow-up task when a deal crosses the big-deal threshold.', - handler: async (ctx: HookContext) => { - // Declared INSIDE the handler — see the sandbox note below. - const BIG_DEAL = 10000; - - const { input } = ctx; - const previous = ctx.previous; - const crossed = - typeof input.amount === 'number' && - input.amount > BIG_DEAL && - !(typeof previous?.amount === 'number' && previous.amount > BIG_DEAL); - if (!crossed) return; - - const api = ctx.api as HookApi | undefined; - if (!api) return; - - await api.object('crm_task').insert({ - subject: `Big deal alert: ${typeof input.name === 'string' ? input.name : 'opportunity'}`, - status: 'not_started', - priority: 'high', - owner_id: (typeof input.owner_id === 'string' && input.owner_id) || ctx.user?.id, - related_to_type: 'crm_opportunity', - related_to_opportunity: (typeof input.id === 'string' && input.id) || previous?.id, - }); - }, -}; - -export default [bigDealAlert]; -``` - -⚠️ **A handler body is lowered to metadata and runs body-only in a QuickJS sandbox, with no module -scope.** Every constant, table and helper it uses must be declared **inside** the handler — a -module-scope reference makes `extractHookBody` throw, and `test/action-sandbox.test.ts` runs that -lowering pass over every registered hook. Type-only imports are erased and are fine. - -## 3. Action Implementation Standard - -An action lives at `src/*/actions/.actions.ts` as a **named** `const`, re-exported from -`src/*/actions/index.ts`. One `Action` per export: the stack is built from `Object.values(...)` over -that barrel, so an exported *array* arrives as a nested list and fails the schema parse. - -`type` is one of `script`, `url`, `modal`, `flow`, `api`, `form`. A `script` action carries its -implementation inline, which is what makes it callable by REST and by AI agents alike: - -```typescript -// src/*/actions/account.actions.ts -import type { Action } from '@objectstack/spec/ui'; - -export const GenerateBriefingAction: Action = { - name: 'generate_briefing', - label: 'Generate Briefing', - objectName: 'crm_account', - type: 'script', - body: { - language: 'js', - source: ` - const id = ctx.recordId; - if (!id) throw new Error('generate_briefing requires a recordId'); - // 1. Fetch data — ctx.api, prefixed object name, where-predicate. - const account = await ctx.api.object('crm_account').findOne({ where: { id } }); - const openDeals = await ctx.api.object('crm_opportunity').count({ - where: { crm_account: id, stage: { $nin: ['closed_won', 'closed_lost'] } }, - }); - // 2. Perform logic - return { account: account?.name ?? null, openDeals }; - `, - capabilities: ['api.read'], - timeoutMs: 5000, - }, - locations: ['record_header'], -}; -``` - -⛔ An action is not a bare exported arrow function taking a params object. Its body is sandboxed -like a hook's, and reaches data only through `ctx.api.object(...)` under the capabilities it -declares — `api.read`, `api.write`, `api.transaction`, `log`, `crypto.uuid`. - -## 4. Before you hand the work back - -- [ ] Every object name written out with its `crm_` prefix — in `object:`, `objectName:`, and every - `api.object('…')` call. -- [ ] Every predicate spelled `where`. -- [ ] Every constant a handler or action body reads declared inside that body. -- [ ] `pnpm verify` green. diff --git a/.github/instructions/metadata.md b/.github/instructions/metadata.md deleted file mode 100644 index b718f9473..000000000 --- a/.github/instructions/metadata.md +++ /dev/null @@ -1,108 +0,0 @@ -# Metadata Specialist Instructions - -You are the **Data Modeler** for HotCRM. You define the data structure using the ObjectQL Protocol. -Your primary output is `*.object.ts` files. - -## 1. Object Definition (`.object.ts`) - -You must adhere to the `ServiceObject` interface: - -```typescript -import { ObjectSchema, Field } from '@objectstack/spec/data'; - -export const Contract = ObjectSchema.create({ - name: 'crm_contract', // snake_case, Singular, crm_ prefixed. DB Table Name. - label: 'Contract', // Human readable label. - pluralLabel: 'Contracts', - description: 'Manages customer binding agreements', - icon: 'contract', - - // Functional Switches - enable: { - trackHistory: true, // Audit field changes - apiEnabled: true, // Expose to REST/GraphQL - searchable: true, // Index in global search - activities: true, // Allow tasks/events attachment - }, - - // Fields Dictionary - fields: { - // ... fields here - }, -}); -``` - -### Constraint Checklist -- **Name**: Must be `snake_case` (e.g., `project_task`, not `ProjectTask`). -- **Prefix**: Every HotCRM business object name MUST carry the `crm_` prefix, written out - (`crm_contract`, `crm_project_task`). The runtime injects nothing: the name in source is the - name at runtime, in the DB and in the REST URL. The FILE stays unprefixed - (`src/revenue/objects/contract.object.ts` declares `name: 'crm_contract'`). -- **Label**: User-friendly Title Case. -- **Export**: a NAMED `const` built by the `ObjectSchema.create(...)` factory, which PARSES the - definition. The package's `objects/index.ts` re-exports that name for registration, so a default - export is never picked up. An `as ObjectSchema` cast is not a substitute: a cast is a type - position for a value and checks nothing at all. - -## 2. Field Definitions - -Use the correct `type` for the business requirement. - -| Type | Example Use Case | -|------|------------------| -| `text` | Name, Title (Short) | -| `textarea` | Description (Long) | -| `select` | Status, Priority (Dropdown) | -| `multiselect` | Tags, Skills | -| `date` / `datetime` | Deadlines, Timestamps | -| `number` / `currency` / `percent` | Financials, Metrics | -| `boolean` | Flags (Is Active?) | -| `lookup` | Many-to-One (e.g., Order -> Customer) | -| `master_detail` | Strict Parent-Child (e.g., OrderItem -> Order) | -| `formula` | Calculated (e.g., Price * Qty) | -| `summary` | Rollup (e.g., Total Order Amount) | - -### Field Examples - -```typescript -// 1. Text -name: { type: 'text', label: 'Title', required: true, unique: true }, - -// 2. Enum (Select) -status: { - type: 'select', - label: 'Status', - options: [ - { value: 'draft', label: 'Draft' }, - { value: 'active', label: 'Active' } - ], - defaultValue: 'draft' -}, - -// 3. Relationship (Lookup) — the target is the prefixed object name, -// declared in account.object.ts -account: Field.lookup('crm_account', { - label: 'Account', - required: true, -}), - -// 4. Formula — `F` is the CEL template tag, imported from '@objectstack/spec' -total_amount: Field.formula({ - label: 'Total', - expression: F`record.quantity * record.unit_price`, -}), -``` - -## 3. Validation Rules - -Enforce data integrity at the schema level. - -```typescript -validation: [ - { - name: 'valid_discount', - errorMessage: 'Discount cannot exceed 100%', - formula: 'discount_rate <= 100' // ObjectQL Formula Syntax - } -] -``` diff --git a/.github/instructions/security.md b/.github/instructions/security.md deleted file mode 100644 index 4c7ccb402..000000000 --- a/.github/instructions/security.md +++ /dev/null @@ -1,74 +0,0 @@ -# Security Officer Instructions - -You are the **Security Specialist**. You define Authentication, Permissions, and Sharing Rules. - -## 1. Role-Based Access Control (`.role.ts`) - -Roles define a hierarchy and a set of base permissions. - -```typescript -export default { - name: 'sales_manager', - label: 'Sales Manager', - permissions: [ - 'account.create', - 'account.read', - 'opportunity.manage' - ] -} -``` - -## 2. Object Permission Profile (`.permission.ts`) - -Granular CRUD permissions per Object and Field. - -```typescript -export default { - object: 'account', - permissions: { - create: ['sales_rep', 'sales_manager'], - read: ['sales_rep', 'sales_manager', 'support'], - update: ['sales_manager'], // Reps cannot update after creation? Check logic. - delete: ['admin'] - }, - fieldPermissions: { - annual_revenue: { - read: ['sales_manager'], // Hidden from Reps - update: ['sales_manager'] - } - } -} -``` - -## 3. Row-Level Security (RLS) (`.rls.ts`) - -Rules filtering *which* records a user can see. - -```typescript -// Example: Sales Reps only see their own accounts -{ - object: 'account', - roles: ['sales_rep'], - rule: 'owner_id = $user.id' -} - -// Example: Managers see their team's accounts -{ - object: 'account', - roles: ['sales_manager'], - rule: 'owner_id IN (SELECT id FROM users WHERE manager_id = $user.id)' -} -``` - -## 4. Sharing Rules (`.sharing.ts`) - -Exceptions to RLS permissions (Opening up access). - -```typescript -{ - name: 'share_vip_accounts', - object: 'account', - records: 'is_vip = true', - shareWith: { type: 'group', id: 'vip_support_team', access: 'read_write' } -} -``` diff --git a/.github/instructions/ui.md b/.github/instructions/ui.md deleted file mode 100644 index 536c44c70..000000000 --- a/.github/instructions/ui.md +++ /dev/null @@ -1,92 +0,0 @@ -# UI Designer Instructions - -You are the **Frontend Specialist** for HotCRM. You define the "User Experience" using the ObjectUI Protocol. -Your primary outputs are `*.view.ts`, `objectstack.config.ts`, `*.actions.ts` (UI), `*.dashboard.ts`. - -## 1. View Definition (`.view.ts` or inside `.object.ts`) - -Views define how data is presented in lists, grids, or boards. - -```typescript -export default { - name: 'active_customers', - label: 'Active Customers', - type: 'grid', // Options: 'list', 'grid', 'kanban', 'calendar', 'gantt' - object: 'account', - columns: [ - { field: 'name', label: 'Name', width: 200 }, - { field: 'annual_revenue', label: 'Revenue', width: 150 }, - { field: 'industry', label: 'Industry', width: 120 } - ], - filter: { field: 'status', operator: 'equals', value: 'active' }, - sort: [{ field: 'created_at', direction: 'desc' }] -} -``` - -### Specialized Views -- **Kanban**: Requires `group_by_field` (e.g., 'stage'). -- **Calendar**: Requires `start_date_field` and `end_date_field`. -- **Gantt**: Used for Project Management, requires dependency fields. - -## 2. App & Navigation (`objectstack.config.ts`) - -Defines the container for your business capability. - -```typescript -{ - type: 'app', - name: 'sales_crm', - label: 'Sales CRM', - navigation: [ - { type: 'object', object: 'account', label: 'Accounts' }, - { type: 'dashboard', dashboard: 'sales_overview', label: 'Overview' }, - { - type: 'group', - label: 'Orders', - children: [{ type: 'object', object: 'order' }] - } - ] -} -``` - -## 3. UI Actions (`.actions.ts`) - -Defining buttons on the UI (Toolbar, Row Actions). - -```typescript -{ - name: 'approve_quote', - label: 'Approve', - type: 'button', // Options: 'button', 'url', 'modal', 'flow' - icon: 'check', - condition: 'status == "draft"', // Visible only when Draft - handler: async (context) => { - // Client-side logic or transition - await triggerFlow('quote_approval', context.record); - } -} -``` - -## 4. Analytics & Dashboards (`.dashboard.ts`, `.report.ts`) - -### Dashboard Layout -12-Column Grid System using `x`, `y`, `width`, `height`. - -```typescript -{ - type: 'chart', - title: 'Revenue Trend', - chartType: 'line', // bar, line, pie, donut, area, funnel - query: { - object: 'opportunity', - groupBy: 'close_month', - aggregation: { field: 'amount', function: 'sum' } - }, - position: { x: 0, y: 0, width: 6, height: 4 } -} -``` - -### Report Types -- **Tabular**: Simple table. -- **Summary**: Grouped data with subtotals. -- **Matrix**: 2D grouping (Pivot table). diff --git a/.github/tasks/autonomous_feature_dev.md b/.github/tasks/autonomous_feature_dev.md index 27dba5a13..efeaabc4f 100644 --- a/.github/tasks/autonomous_feature_dev.md +++ b/.github/tasks/autonomous_feature_dev.md @@ -2,7 +2,7 @@ Here is a prompt you can copy-paste to GitHub Copilot to trigger the **Autonomou --- -**Assign Role**: @workspace /load .github/instructions/architect.md +**Read first**: @workspace /load AGENTS.md **Goal**: I need you to implement a new feature: **[INSERT FEATUE NAME HERE]** @@ -11,7 +11,7 @@ Here is a prompt you can copy-paste to GitHub Copilot to trigger the **Autonomou - [INSERT BUSINESS REQUIREMENT 2] **Instructions**: -1. **Architecture Phase**: strictly follow the **"Feature-to-File" Mapping Strategy** in basic `architect.md`. Output a table of all files to be created. +1. **Architecture Phase**: place every file by the layout rules in `AGENTS.md` §Project Architecture (an item lives with the object it is authored against; a `*.hook.ts` sits beside its `*.object.ts`). 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 (`.flow.ts` — there is no `workflow` metadata type, see `AGENTS.md` §Schema Validation Requirements), and UI (`.page.ts`) files. diff --git a/.github/tasks/new-feature.md b/.github/tasks/new-feature.md index e28e51bff..bea9648fb 100644 --- a/.github/tasks/new-feature.md +++ b/.github/tasks/new-feature.md @@ -18,5 +18,5 @@ implementing requirement `REQ-[NNNN]`. 2. **Logic**: Create `src/*/objects/[OBJECT_NAME].hook.ts` to implement: [DESCRIBE_LOGIC]. 3. **UI**: Create `src/*/views/[OBJECT_NAME].view.ts` and `src/*/pages/[OBJECT_NAME].page.ts` for a standard record layout. -Please follow the strictly typed metadata standards in `.github/instructions/metadata.md`. +Please follow the metadata standards in `AGENTS.md`. ``` diff --git a/AGENTS.md b/AGENTS.md index 95d3aa0b3..d0d414ba0 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -117,6 +117,13 @@ are recorded in `docs/architecture/module-split-plan.md`; ⛔ do not restate tha - **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. + - **The method contract is the `_hook-api.ts` types**, ⛔ not memory of another stack: `count` takes `where` only, a read is capped + with `top`, and `update` is `(doc carrying its id, { where })` — each wrong spelling is a compile error there. + - **A hook handler and a `script` action body run body-only in a QuickJS sandbox, with no module scope.** Every constant, table and + helper a body reads is declared **inside** it (type-only imports are erased and fine) — a module-scope reference type-checks and + then fails the lowering, which `test/action-sandbox.test.ts` runs over every registered hook. An action body reaches data only under + the `capabilities` it declares (`api.read`, `api.write`). `ctx.user` is **absent** on system and seed writes — that absence is this + repo's system-write signal. - **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()`, @@ -277,7 +284,7 @@ FROM → TO for a breaking change); it ships as `CHANGELOG.md`. The lone excepti 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 +excluded it, is not a failure. **Governed paths — `AGENTS.md`, `CLAUDE.md`, `.claude/**` — 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 diff --git a/test/docs-drift.test.ts b/test/docs-drift.test.ts index 30b1db1d3..c49f86e57 100644 --- a/test/docs-drift.test.ts +++ b/test/docs-drift.test.ts @@ -25,9 +25,7 @@ import stack from '../objectstack.config'; * Where the others went. Add a new rule to the file whose subject it shares — * this one only if it reads a value out of a flow: * - * docs-src-tree-paths.test.ts `src//` paths a doc names or draws, and the - * layout the `.github/instructions/` agent briefs - * name (#1233) + * docs-src-tree-paths.test.ts `src//` paths a doc names or draws * docs-runnable-samples.test.ts samples a reader runs: documented agent * names (#606), the action example (#813) * docs-dashboard-tiles.test.ts the dashboards page lists tiles that exist, diff --git a/test/docs-src-tree-paths.test.ts b/test/docs-src-tree-paths.test.ts index 9ee660b76..6ca8ccc15 100644 --- a/test/docs-src-tree-paths.test.ts +++ b/test/docs-src-tree-paths.test.ts @@ -418,211 +418,9 @@ describe('docs that draw the src/ tree only draw directories that exist', () => }); } }); -/* - * ─── THE AGENT INSTRUCTION FILES (#1233) ───────────────────────────────────── - * - * `.github/instructions/*.md` are per-role agent briefs — `logic.md` opens - * "You are the **Backend Engineer** for HotCRM" — and until now every one of - * them sat outside every gate in this repo. Nothing under `test/`, `scripts/` - * or `package.json` referenced the directory at all, which is how `logic.md` - * came to tell the next backend agent to put its hook and its action under - * `packages/crm/src/`: a path out of the retired multi-package layout that - * `docs/archive/README.md` exists to keep OUT of current docs. Six files, no - * guard — the drift was not missed, it was never looked for. - * - * Two rules, because the defect has two halves and the rules above only cover - * one of them. - * - * 1. `src//` named ⇒ it exists. The ordinary drift shape, the same - * check the maintainer and product docs already get: a brief that starts - * naming `src/agents/` after #512 deleted it goes red here. - * - * 2. No `packages/…` path, at all. This is the half the existing extractor - * CANNOT see, and it is the reason this block is not just three more - * entries appended to TREE_DOCS. `inlineSrcDirs()` wants a DIRECTORY, and - * the defect names a FILE directly under `src/`, so - * `packages/crm/src/ai_briefing.actions.ts` yields no capture at all. - * Still true after #1923 dropped the trailing-slash requirement, and by - * design: the `(?!\.[a-z])` lookahead added there is exactly the rule that - * a file with an extension is not a directory claim. - * - * Measured over the unfixed file, not assumed: `inlineSrcDirs()` returns - * `[]` on it, so the names-implies-exists assertion has nothing to find - * missing and is GREEN. Appending the brief to TREE_DOCS — a list that - * carries no vacuity guard of its own — would therefore have shipped a - * check that is green on the very defect it was added for. The vacuity - * assertion in rule 1 below is what stops that shape recurring here; rule - * 2 is what actually catches the layout. - * - * Rule 2 is keyed on the tree, not on a taste for `src/`: `packages/` is - * banned here BECAUSE this repo does not have one, and the assertion that says - * so is checked first. Give hotcrm a real `packages/` directory and this block - * throws and demands a rewrite rather than going on banning correct prose — - * the same "loud in both directions" contract the CEL scan in - * `docs-drift.test.ts` is written to. - */ - -const INSTRUCTIONS_DIR = '.github/instructions'; - -/** - * Every brief in the directory, walked rather than listed. - * - * Opposite choice from TREE_DOCS / PRODUCT_TREE_DOCS above, and deliberately: - * those lists exist because "does this page point readers into the tree?" is - * an editorial fact about each page. Rule 2 asks nothing editorial — no brief - * may name the retired layout, whatever else it says — so a file added to this - * directory tomorrow is covered without anyone remembering to enrol it, which - * is precisely what did not happen for these six. - */ -const instructionFiles = (): string[] => - readdirSync(join(REPO_ROOT, INSTRUCTIONS_DIR), { withFileTypes: true }) - .filter((entry) => entry.isFile() && entry.name.endsWith('.md')) - .map((entry) => `${INSTRUCTIONS_DIR}/${entry.name}`) - .sort(); - -/** `packages/crm/src/…`, `packages/hr/src/…`, `packages/[name]`. */ -const PACKAGES_PATH = /\bpackages\/[A-Za-z0-9_[\]/.-]+/g; - -const packagesPathsIn = (docFile: string): string[] => [ - ...new Set(readFileSync(join(REPO_ROOT, docFile), 'utf8').match(PACKAGES_PATH) ?? []), -]; - -/** - * The briefs still carrying the retired layout, each with the card that clears - * it. A RATCHET, not a mute — the assertion below is an equality, so it fails - * in BOTH directions: - * - * a new file picks up `packages/…` → red, naming the new file; - * a listed file is cleaned up → red, demanding its entry be deleted. - * - * So the list can only shrink, and an exception cannot outlive the defect it - * was granted for. - * - * The list is now EMPTY, and that is the finished state, not a disabled one: - * every brief names the real single-package tree. `architect.md` was the last - * entry, and it outlived `logic.md` because its remedy was not a path rewrite - * — the standing "Dependency Management Rules" named three packages that do - * not exist, and the MANDATORY Output Format headed every plan the architect - * emits with a `📦 Package:` heading, so it needed a scope ruling before a - * line could be written. #1518 carries that ruling (the architect brief - * describes THIS workspace) and the rewrite; deleting its entry here is what - * the equality below demanded the moment the file was fixed, which is the - * ratchet working, not an obstacle to it. - * - * ⛔ Do not add an entry to turn a red build green. An entry is a dated - * exception with a card behind it, and an empty object is not a weaker guard: - * the equality still fails loudly the day any brief picks the retired layout - * back up. - */ -const PENDING_PACKAGES_REFS: Record = {}; -/** - * The one brief that points into `src/` in prose, and so can carry rule 1 - * without going vacuous. The other four name no repository path at all — they - * are pure protocol and code fences — which is why enrolling them here would - * be enrolling them into a check that asserts nothing. Rule 2 covers them. - */ -const INSTRUCTION_TREE_DOCS = ['.github/instructions/logic.md']; - -/** - * The package directories under `src/` — DERIVED, because the last hand-kept - * copy of this list is half of what #1923 was filed for. - * - * Rule 2's failure message below is read by the next agent to decide where a - * file goes, and it spent the whole of ADR-0130 naming five directories that - * had stopped existing (`src/objects/`, `src/actions/`, `src/views/`, - * `src/flows/`, `src/sharing/`) while calling the app "single-package". A - * guard whose own contract is that "a path here is an instruction, not prose" - * was issuing the exact instruction it exists to forbid. - * - * A package is a directory under `src/` that carries an `objects/` barrel; - * `src/docs/` is the platform's ADR-0046 documentation path and carries none, - * so the predicate excludes it without anyone having to name it. +/* + * The `.github/instructions/*.md` agent-brief block (#1233) was retired with + * the briefs themselves (#1832): AGENTS.md is the one instruction set, and it + * is already enrolled in TREE_DOCS above. */ -const srcPackages = (): string[] => - readdirSync(join(REPO_ROOT, 'src'), { withFileTypes: true }) - .filter((entry) => entry.isDirectory() && existsSync(join(REPO_ROOT, 'src', entry.name, 'objects'))) - .map((entry) => entry.name) - .sort(); - -describe('agent instruction files name this repo layout, not the retired one', () => { - it('reads the briefs it is written to guard', () => { - const files = instructionFiles(); - expect( - files.length, - `no .md files found under ${INSTRUCTIONS_DIR}/ — this guard has gone vacuous. If the ` + - 'per-role briefs genuinely moved, point this block at wherever they went; do not leave ' + - 'it green over an empty directory.', - ).toBeGreaterThan(0); - - // A pending entry for a file that no longer exists would silently excuse - // nothing forever, and would hide the day the file was deleted outright. - const stale = Object.keys(PENDING_PACKAGES_REFS).filter((f) => !files.includes(f)); - expect( - stale, - `PENDING_PACKAGES_REFS names briefs that no longer exist: ${stale.join(', ')}. ` + - 'Delete the entry (and close the card it points at).', - ).toEqual([]); - }); - - it('this repo still has no packages/ directory, which is what rule 2 rests on', () => { - // Rule 2's remedy line names the real packages rather than a copy of them, - // so an empty derivation would replace the five dead paths #1923 found with - // no path at all. Loud here rather than silent in a failure message nobody - // reads until the day it matters. - expect( - srcPackages().length, - 'no directory under src/ carries an objects/ barrel — the package layout moved, and the ' + - 'remedy line in rule 2 below would name nothing. Teach srcPackages() the new shape.', - ).toBeGreaterThan(0); - - expect( - existsSync(join(REPO_ROOT, 'packages')), - 'a real `packages/` directory now exists, so banning `packages/…` in the agent briefs is ' + - 'no longer correct. Rewrite the rule below against the real tree — do not delete it, and ' + - 'do not leave it banning prose that has become true.', - ).toBe(false); - }); - - it('no brief points into the retired packages/ layout, beyond the tracked backlog', () => { - const found = instructionFiles().filter((f) => packagesPathsIn(f).length > 0); - const detail = found - .map((f) => `${f}: ${packagesPathsIn(f).join(', ')}${PENDING_PACKAGES_REFS[f] ? ` (tracked by ${PENDING_PACKAGES_REFS[f]})` : ''}`) - .join('\n '); - - expect( - found, - `the set of briefs naming \`packages/…\` changed:\n ${detail}\n` + - 'There is no `packages/` directory in hotcrm — one `package.json`, and since ADR-0130 a ' + - `directory under \`src/\` IS a package: ${srcPackages().join(' · ')}. A brief is what ` + - 'the next agent reads to decide where a file goes, so a path here is an instruction, ' + - 'not prose.\n' + - 'If a NEW file appears above: repoint it at the package that owns the record — ' + - '`src//objects/` for objects AND the `*.hook.ts` beside each one, ' + - '`src//actions/`, `src//views/`, `src//flows/`, `src//sharing/`, ' + - 'with `` one of the packages named above.\n' + - 'If a file above was CLEANED UP: delete its PENDING_PACKAGES_REFS entry — that is this ' + - 'assertion doing its job, not an obstacle to it.', - ).toEqual(Object.keys(PENDING_PACKAGES_REFS).sort()); - }); - - for (const docFile of INSTRUCTION_TREE_DOCS) { - it(`${docFile}: every src// it names exists`, () => { - const text = readFileSync(join(REPO_ROOT, docFile), 'utf8'); - const named = [...new Set(inlineSrcDirs(text))]; - expect( - named.length, - `${docFile} names no src// at all — this guard has gone vacuous over it. A brief ` + - 'listed here is one that tells an agent where files live; if this one stopped doing ' + - 'that, drop it from INSTRUCTION_TREE_DOCS instead of leaving it green over nothing.', - ).toBeGreaterThan(0); - const missing = named.filter((dir) => !existsSync(join(REPO_ROOT, 'src', dir))); - expect( - missing, - `${docFile} advertises src/ directories that do not exist: ${missing.join(', ')}. ` + - 'Delete the reference (or restore the directory) — a path in an agent brief is a ' + - 'promise the next agent acts on.', - ).toEqual([]); - }); - } -});