Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
27 changes: 27 additions & 0 deletions .changeset/a-directory-under-src-is-a-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
---

Repository layout only — this PR releases nothing to HotCRM users, so the frontmatter
above is deliberately empty (the sanctioned "releases nothing" declaration that
`.github/workflows/changeset-check.yml` documents, on par with the `skip-changeset`
label). The compiled artifact is unchanged, and not by argument: `dist/objectstack.json`
built on this branch is **byte-identical** to the one built on the base commit
(590b095), verified with `cmp` and recorded in the PR body. No object, field, hook, flow,
view, page, dashboard, report, dataset, action, skill, permission set, sharing rule,
navigation entry, translation or seed row changed, and the seed replay order is the same
19 datasets in the same sequence.

What moved is where the source files sit. Every authored file under `src/` is now in the
directory of the package that owns it — `src/sales/` (the `type: app` package),
`src/service/`, `src/revenue/`, `src/marketing/` — implementing ADR-0130 and
`docs/architecture/module-split-plan.md` items 6–9: a directory under `src/` **is** a
package, there is no `packages/` level and no `shared/`. A `*.hook.ts` now sits beside the
`*.object.ts` it names, which is what enforces the rule that a hook may not attach to
another package's object; a file may import from its own directory or from `src/sales/`,
never sideways between modules.

No manifest, no `composeStacks`, no per-package `index.ts`, no navigation change — those
are the packaging PR. `objectstack.config.ts` keeps its single `defineStack()`, and the
collection step it consumes is the new `objectstack.composition.ts` (a separate module
because the config file may carry no named export: the build parses it against a strict
stack schema and fails on any key but the default).
45 changes: 28 additions & 17 deletions .github/instructions/architect.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,47 @@

You are the **Chief Architect**. Your role is to break down vague business requirements into a concrete **Metadata Implementation Plan**.

HotCRM is a **single ObjectStack app**, not a multi-package workspace. Every artefact you
plan lands in the flat `src/<kind>/` tree — one directory per metadata kind, with the kind
repeated as the file-name suffix (`src/objects/<name>.object.ts`,
`src/flows/<name>.flow.ts`). Plan file paths against the directories that actually exist
under `src/`: never plan a workspace layout, and never invent a directory for a kind this
repo has no home for.
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/<name>.object.ts`,
`src/revenue/flows/<name>.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`
- `Candidate` (Person) -> `src/<pkg>/objects/candidate.object.ts`
- `Job Position` (The Opening) -> `src/<pkg>/objects/job_position.object.ts`
- `Application` (The Junction) -> `src/<pkg>/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`
- "Send email on reject..." -> `src/<pkg>/flows/application-rejected.flow.ts`
- "Managers verify" -> `src/<pkg>/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)
- "HR needs to see pipeline" -> `src/<pkg>/views/application.view.ts` (Kanban)

### Step 4: Security (Auth Layer)
Identify the actors.
- "Hiring Manager" -> `src/profiles/hiring-manager.profile.ts`
- "Candidate controls own data" -> `src/sharing/candidate.sharing.ts`
- "Hiring Manager" -> `src/sales/profiles/hiring-manager.profile.ts` (permission sets are app-level)
- "Candidate controls own data" -> `src/<pkg>/sharing/candidate.sharing.ts`

## Output Format

Expand All @@ -42,7 +53,7 @@ Always start your response with the **Architecture Plan**:

| 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 |
| Object | `src/<pkg>/objects/foo.object.ts` | Stores X data |
| Logic | `src/<pkg>/objects/foo.hook.ts` | Validates Y |
| UI | `src/<pkg>/pages/foo.page.ts` | Layout for Z |
```
20 changes: 10 additions & 10 deletions .github/instructions/logic.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ You are the **Backend Engineer** for HotCRM. You implement business rules using

## Capabilities

1. **Hooks (`src/objects/<object>.hook.ts`)**: Run code *before* or *after* database operations.
2. **Actions (`src/actions/<object>.actions.ts`)**: Custom API endpoints or AI Tools.
1. **Hooks (`src/*/objects/<object>.hook.ts`)**: Run code *before* or *after* database operations.
2. **Actions (`src/*/actions/<object>.actions.ts`)**: Custom API endpoints or AI Tools.

## 0. The data surface, before anything else

Expand All @@ -23,7 +23,7 @@ either, so `'opportunity'` names an object that does not exist; the object is `c
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/objects/_hook-api.ts`. Write from it, not from memory of another stack.
shapes in `src/sales/objects/_hook-api.ts`. Write from it, not from memory of another stack.

## 1. ObjectQL Query Protocol

Expand All @@ -33,7 +33,7 @@ NEVER write SQL.
`HookContext.api` as `unknown`:

```typescript
import type { HookApi } from './_hook-api'; // src/objects/_hook-api.ts — the shared shape
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;
Expand Down Expand Up @@ -69,7 +69,7 @@ Three of these are asymmetries you cannot guess, so they are declared rather tha
- ✅ `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
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".
Expand Down Expand Up @@ -118,7 +118,7 @@ const openDeals = await api.object('crm_opportunity').count({

## 2. Hook Implementation Standard

A hook lives at `src/objects/<object>.hook.ts` and **default-exports** one `Hook` or an array of
A hook lives at `src/*/objects/<object>.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.

Expand All @@ -140,7 +140,7 @@ are not this stack's events; the enum rejects them.
| `id` | the target record id, where the event carries one |

```typescript
// src/objects/opportunity.hook.ts
// src/sales/objects/opportunity.hook.ts
import type { Hook, HookContext } from '@objectstack/spec/data';
import type { HookApi } from './_hook-api';

Expand Down Expand Up @@ -188,15 +188,15 @@ lowering pass over every registered hook. Type-only imports are erased and are f

## 3. Action Implementation Standard

An action lives at `src/actions/<object>.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
An action lives at `src/*/actions/<object>.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
// src/*/actions/account.actions.ts
import type { Action } from '@objectstack/spec/ui';

export const GenerateBriefingAction: Action = {
Expand Down
4 changes: 2 additions & 2 deletions .github/instructions/metadata.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,10 @@ export const Contract = ObjectSchema.create({
- **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/objects/contract.object.ts` declares `name: 'crm_contract'`).
(`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. `src/objects/index.ts` re-exports that name for auto-registration, so a default
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.

Expand Down
12 changes: 9 additions & 3 deletions .github/labeler.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,23 @@ ci/cd:

# Server-side behaviour: hook handlers, automation flows, and invocable
# actions. This is the code that actually executes at request time.
#
# `src/*/…` since the ADR-0130 layout: a directory under `src/` is a package
# (`sales`, `service`, `revenue`, `marketing`) and each carries its own
# metadata-type subdirectories. `src/hooks/**` is gone rather than re-pointed —
# a `*.hook.ts` now sits beside the `*.object.ts` it names, which the last glob
# below already matches.
backend:
- changed-files:
- any-glob-to-any-file: ['src/actions/**', 'src/flows/**', 'src/hooks/**', 'src/objects/*.hook.ts']
- any-glob-to-any-file: ['src/*/actions/**', 'src/*/flows/**', 'src/*/objects/*.hook.ts']

# Declarative metadata: schema, the security posture layered on it, and the
# UI surfaces (apps, pages, views, dashboards, reports, datasets) that are
# themselves just metadata in ObjectStack.
metadata:
- changed-files:
- any-glob-to-any-file: ['src/objects/**', 'src/profiles/**', 'src/sharing/**', 'src/skills/**', 'src/data/**', 'src/apps/**', 'src/pages/**', 'src/views/**', 'src/interfaces/**', 'src/dashboards/**', 'src/reports/**', 'src/datasets/**', 'src/translations/**', 'objectstack.manifest.json']
- any-glob-to-any-file: ['src/*/objects/**', 'src/*/profiles/**', 'src/*/sharing/**', 'src/*/skills/**', 'src/*/data/**', 'src/*/apps/**', 'src/*/pages/**', 'src/*/views/**', 'src/*/interfaces/**', 'src/*/dashboards/**', 'src/*/reports/**', 'src/*/datasets/**', 'src/*/translations/**', 'objectstack.manifest.json']

configuration:
- changed-files:
- any-glob-to-any-file: ['*.config.ts', 'objectstack.config.ts', 'tsconfig.json', '.gitignore', '.changeset/config.json']
- any-glob-to-any-file: ['*.config.ts', 'objectstack.config.ts', 'objectstack.composition.ts', 'tsconfig.json', '.gitignore', '.changeset/config.json']
9 changes: 5 additions & 4 deletions .github/tasks/new-feature.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

**Start from the requirement**: this feature should trace to a record under
[`docs/requirements/`](../../docs/requirements/README.md). Read its **disposition**
first — `B` (standard enhancement) lands in core `src/{type}/`; `C`
first — `B` (standard enhancement) lands in the package that owns the object,
`src/<pkg>/<type>/`; `C`
(customer-specific) belongs in an overlay/extension package, **not** in HotCRM core.

**Prompt**:
Expand All @@ -13,9 +14,9 @@ I want to add a `[FEATURE_NAME]` feature to HotCRM (single app, namespace `crm`)
implementing requirement `REQ-[NNNN]`.

**Requirements**:
1. **Model**: Create `src/objects/[OBJECT_NAME].object.ts` with fields: [LIST_FIELDS]. Use the `crm_` prefix.
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.
1. **Model**: Create `src/*/objects/[OBJECT_NAME].object.ts` with fields: [LIST_FIELDS]. Use the `crm_` prefix.
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`.
```
32 changes: 25 additions & 7 deletions .github/tasks/new-package.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,33 @@
# Task: Scaffold New Package

**Goal**: Create a new business module in `packages/`.
**Goal**: Add a new business module to HotCRM.

A package here is **a directory under `src/`** (ADR-0130, `docs/architecture/module-split-plan.md`):
`src/sales/` is the `type: app` package the artifact takes its identity from, and
`src/service/`, `src/revenue/`, `src/marketing/` are modules that depend on it. There is no
`packages/` directory, no per-package `package.json` and no npm workspace — one
`package.json`, one `tsconfig.json`, one build, one artifact. ⛔ Do not scaffold the retired
multi-package layout; `docs/archive/` is where that shape is kept, and nothing current
points into it.

**Prompt**:
```markdown
I want to create a new solution package named: `[PACKAGE_NAME]`.
I want to add a new module named: `[PACKAGE_NAME]`.
Description: [DESCRIPTION]
Objects it owns: [OBJECT_NAMES]

Please scaffold the package structure using the Architect Guidelines:
1. Create `packages/[PACKAGE_NAME]/package.json` with correct name and peerDeps.
2. Create `packages/[PACKAGE_NAME]/tsconfig.json` extending base.
3. Create `packages/[PACKAGE_NAME]/src/index.ts` as entry point.
4. Update `base.tsconfig.json` to include the new path if necessary (or just verify standard glob patterns cover it).
Please scaffold it using the Architect Guidelines:
1. Create `src/[PACKAGE_NAME]/` with only the metadata-type subdirectories it actually
uses (`objects/`, `views/`, `flows/`, … ), each with its own explicit `index.ts`
barrel — registration stays file-by-file, no glob discovery.
2. Put each `*.hook.ts` beside the `*.object.ts` it names, and re-export the package's
hooks from `src/[PACKAGE_NAME]/objects/hooks.ts`.
3. Import only from the file's own directory or from `src/sales/` — never sideways
between modules, never upward. A source more than one package needs goes in
`src/sales/`, in one copy.
4. Register the new barrels in `objectstack.composition.ts`, which collects the packages
into the arrays `objectstack.config.ts` hands to the single `defineStack()`.
5. Add the package directory to `PACKAGE_DIRS` in
`scripts/check-source-token-ratchet.mjs` so the gate prints a reading for it, and to
`PACKAGES` in `test/helpers/src-roster.ts` so the app-wide test sweeps see it.
```
Loading
Loading