makersclaw 0.5.2 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +73 -7
- package/package.json +3 -2
- package/r/annotate.json +4 -4
- package/r/approval-request.json +3 -3
- package/r/audio-player.json +3 -3
- package/r/auth-form.json +2 -2
- package/r/board.json +4 -4
- package/r/calendar-view.json +4 -4
- package/r/cart.json +3 -3
- package/r/chart.json +3 -3
- package/r/chat.json +3 -3
- package/r/code-editor.json +4 -4
- package/r/comment-box.json +4 -4
- package/r/contact-section.json +3 -3
- package/r/core.json +3 -3
- package/r/csv-import.json +4 -4
- package/r/data-table.json +3 -3
- package/r/diagram.json +3 -3
- package/r/doc-editor.json +4 -4
- package/r/doc-page.json +4 -4
- package/r/email-render.json +2 -2
- package/r/faq.json +2 -2
- package/r/file-upload.json +3 -3
- package/r/flow-canvas.json +4 -4
- package/r/footer.json +3 -3
- package/r/gantt.json +4 -4
- package/r/job-panel.json +3 -3
- package/r/json-editor.json +4 -4
- package/r/map.json +5 -5
- package/r/menubar.json +2 -2
- package/r/mind-map.json +4 -4
- package/r/navigation-menu.json +2 -2
- package/r/order-detail.json +2 -2
- package/r/org-chart.json +4 -4
- package/r/page-builder.json +6 -6
- package/r/payment-methods.json +2 -2
- package/r/pdf-document.json +2 -2
- package/r/pdf-viewer.json +6 -6
- package/r/product-detail.json +3 -3
- package/r/product-grid.json +4 -4
- package/r/record-form.json +3 -3
- package/r/record-notes.json +5 -5
- package/r/registry.json +121 -121
- package/r/search-dialog.json +3 -3
- package/r/settings-list.json +3 -3
- package/r/setup-checklist.json +3 -3
- package/r/share-dialog.json +3 -3
- package/r/sidebar.json +2 -2
- package/r/signature-pad.json +3 -3
- package/r/sketch-canvas.json +5 -5
- package/r/spreadsheet.json +3 -3
- package/r/stat-cards.json +4 -4
- package/r/task-list.json +4 -4
- package/r/task-model.json +3 -3
- package/r/testimonials.json +2 -2
- package/r/transaction-list.json +3 -3
- package/r/whiteboard.json +4 -4
- package/registry/blocks/annotate/README.md +14 -5
- package/registry/blocks/approval-request/README.md +15 -8
- package/registry/blocks/audio-player/README.md +17 -10
- package/registry/blocks/board/README.md +13 -9
- package/registry/blocks/board/board.jsx +1 -1
- package/registry/blocks/calendar-view/README.md +28 -4
- package/registry/blocks/cart/README.md +14 -10
- package/registry/blocks/chart/README.md +14 -7
- package/registry/blocks/chat/README.md +43 -13
- package/registry/blocks/code-editor/README.md +4 -0
- package/registry/blocks/code-editor/code-editor.jsx +1 -1
- package/registry/blocks/comment-box/README.md +20 -13
- package/registry/blocks/contact-section/README.md +7 -6
- package/registry/blocks/csv-import/README.md +34 -12
- package/registry/blocks/data-table/README.md +41 -8
- package/registry/blocks/diagram/diagram.jsx +1 -1
- package/registry/blocks/doc-editor/README.md +18 -6
- package/registry/blocks/doc-editor/doc-editor.jsx +1 -1
- package/registry/blocks/doc-page/README.md +16 -6
- package/registry/blocks/file-upload/README.md +7 -1
- package/registry/blocks/flow-canvas/README.md +16 -5
- package/registry/blocks/flow-canvas/flow-canvas.jsx +1 -1
- package/registry/blocks/footer/README.md +4 -2
- package/registry/blocks/gantt/README.md +18 -12
- package/registry/blocks/job-panel/README.md +1 -1
- package/registry/blocks/json-editor/README.md +16 -4
- package/registry/blocks/map/README.md +19 -3
- package/registry/blocks/map/map-view.jsx +1 -1
- package/registry/blocks/map/maplibre.js +1 -1
- package/registry/blocks/mind-map/README.md +16 -5
- package/registry/blocks/org-chart/README.md +10 -7
- package/registry/blocks/page-builder/README.md +13 -6
- package/registry/blocks/page-builder/page-builder.jsx +1 -1
- package/registry/blocks/page-builder/page-view.jsx +1 -1
- package/registry/blocks/page-builder/page.js +1 -1
- package/registry/blocks/pdf-viewer/README.md +22 -20
- package/registry/blocks/pdf-viewer/pdf-viewer.jsx +1 -1
- package/registry/blocks/pdf-viewer/pdfjs.js +1 -1
- package/registry/blocks/pdf-viewer/ssr-shim.js +1 -1
- package/registry/blocks/product-detail/README.md +13 -7
- package/registry/blocks/product-detail/demo.jsx +1 -1
- package/registry/blocks/product-grid/README.md +12 -10
- package/registry/blocks/record-form/README.md +26 -9
- package/registry/blocks/record-notes/README.md +42 -30
- package/registry/blocks/search-dialog/README.md +38 -7
- package/registry/blocks/settings-list/README.md +29 -17
- package/registry/blocks/setup-checklist/README.md +12 -14
- package/registry/blocks/share-dialog/README.md +1 -1
- package/registry/blocks/signature-pad/README.md +15 -4
- package/registry/blocks/sketch-canvas/README.md +13 -4
- package/registry/blocks/sketch-canvas/sketch-canvas.jsx +1 -1
- package/registry/blocks/sketch-canvas/ssr-shim.js +3 -3
- package/registry/blocks/spreadsheet/README.md +17 -8
- package/registry/blocks/stat-cards/README.md +17 -8
- package/registry/blocks/task-list/README.md +14 -11
- package/registry/blocks/task-model/README.md +3 -3
- package/registry/blocks/transaction-list/README.md +15 -7
- package/registry/blocks/whiteboard/README.md +19 -13
- package/src/cli.mjs +65 -9
- package/src/install.mjs +360 -21
- package/src/lockfile.mjs +468 -0
- package/src/pm.mjs +473 -0
- package/src/registry.mjs +125 -1
- package/templates/next/browser/app/auto-refresh.jsx +67 -0
- package/templates/next/browser/app/layout.jsx +94 -0
- package/templates/next/browser/app/page.jsx +60 -0
- package/templates/next/browser/app/s/[key]/page.jsx +95 -0
- package/templates/next/browser/lib/actions.js +16 -0
- package/templates/next/browser/lib/data.js +43 -0
- package/templates/next/files/README.md +818 -82
- package/templates/next/files/app/claw-alive/route.js +2 -2
- package/templates/next/files/app/frame.js +134 -9
- package/templates/next/files/app/layout.jsx +2 -2
- package/templates/next/files/check/run.mjs +1491 -53
- package/templates/next/files/components/ui/item.jsx +2 -2
- package/templates/next/files/components/ui/notice.jsx +1 -1
- package/templates/next/files/components/ui/separator.jsx +1 -1
- package/templates/next/files/lib/actions.js +171 -52
- package/templates/next/files/lib/brain-server.js +96 -0
- package/templates/next/files/lib/brain.js +413 -0
- package/templates/next/files/lib/claw.js +11 -0
- package/templates/next/files/lib/data.js +120 -30
- package/templates/next/files/lib/db.js +97 -17
- package/templates/next/files/lib/door.js +680 -0
- package/templates/next/files/next.config.mjs +85 -9
- package/templates/next/files/package.json +3 -3
- package/templates/next/files/{middleware.js → proxy.js} +13 -6
- package/templates/next/files/surfaces.js +3 -2
- package/templates/next/pnpm-lock.yaml +5870 -0
- package/templates/next/template.json +13 -6
- package/templates/next/files/schema.sql +0 -22
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# artifact template
|
|
2
2
|
|
|
3
|
-
The app every artifact starts from: Next.js
|
|
4
|
-
actions), Postgres via `DATABASE_URL
|
|
3
|
+
The app every artifact starts from: Next.js 16 (app router, JSX, server
|
|
4
|
+
actions, Turbopack), Postgres via `DATABASE_URL` or, in a browser app,
|
|
5
|
+
the platform's data door (§2), Tailwind v4, and a kit of parts
|
|
5
6
|
(`components/ui`, `components/parts`). It carries the behaviour every
|
|
6
7
|
app shares, the parts every app is assembled from, and the workspace's
|
|
7
|
-
colours. You design the surfaces: what is on them, how they
|
|
8
|
-
out, how dense, how they read — never what colour anything is.
|
|
8
|
+
colours. You design the surfaces: what is on them, how they
|
|
9
|
+
are laid out, how dense, how they read — never what colour anything is.
|
|
9
10
|
|
|
10
11
|
## 1. What you are inside
|
|
11
12
|
|
|
@@ -48,6 +49,14 @@ Paths here are relative to this folder — the artifact's `app/`.
|
|
|
48
49
|
`app/`: this file's `app/shell.jsx` is written as `app/app/shell.jsx`,
|
|
49
50
|
`surfaces/<key>.jsx` as `app/surfaces/<key>.jsx`.
|
|
50
51
|
|
|
52
|
+
An app is one of two modes, and `makersclaw.json`'s `mode` says which.
|
|
53
|
+
A **server app** runs on a Next server of its own (`"server"`, or no
|
|
54
|
+
key). A **browser app** runs in the member's browser: its pages are
|
|
55
|
+
files built once, and every read and write goes through the platform's
|
|
56
|
+
data door (`"browser"`). People add, edit and delete in it as in any
|
|
57
|
+
app. Both lay the files below; a browser app swaps six of them and
|
|
58
|
+
leaves out four (Per mode, below). Which to build: §4.
|
|
59
|
+
|
|
51
60
|
Yours — write and rewrite freely:
|
|
52
61
|
|
|
53
62
|
```
|
|
@@ -56,7 +65,7 @@ app/DESIGN.md the design, written before the code (§3)
|
|
|
56
65
|
surfaces/<key>.jsx one file per domain surface
|
|
57
66
|
surfaces.js the manifest entries (import + component when built)
|
|
58
67
|
components/*.jsx anything shared between surfaces
|
|
59
|
-
components/*-actions.js
|
|
68
|
+
components/*-actions.js a server app's own server actions, for a write lib/actions.js cannot make (§5)
|
|
60
69
|
components/depictions/ pictures of things that exist elsewhere, in their own colours (§3c)
|
|
61
70
|
```
|
|
62
71
|
|
|
@@ -72,14 +81,56 @@ app/escape-relay.jsx, app/frame.js, app/claw-alive/route.js the app inside its
|
|
|
72
81
|
components/ui/ the kit: Button, Field, Input, Select, Sheet, Tabs, Menu, Notice, Icon, Depiction… (§3b, §3c)
|
|
73
82
|
components/parts/ the parts: RecordSwitcher, LiveEmpty + LiveWork, useFixture + FixtureNotice (§3a, §3b)
|
|
74
83
|
check/pins.json the fixed files by hash; `npm run check` refuses one that differs
|
|
75
|
-
lib/* db, data, actions, claw, platform, hooks, sample (§5)
|
|
76
84
|
check/ `npm run check` (§6)
|
|
77
|
-
|
|
78
|
-
next.config.mjs basePath + Server Actions allowedOrigins,
|
|
85
|
+
proxy.js product-token check, x-claw-surface and x-claw-theme headers (Next 16's name for middleware.js)
|
|
86
|
+
next.config.mjs basePath + Server Actions allowedOrigins (and the dev server's allowedDevOrigins), from env (§4b);
|
|
87
|
+
a browser app's export (output "export"), from makersclaw.json's mode
|
|
79
88
|
postcss.config.mjs Tailwind v4
|
|
80
|
-
schema.sql public.artifact_activity, for reference
|
|
81
89
|
```
|
|
82
90
|
|
|
91
|
+
And `lib/`, fixed too (§5):
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
lib/data.js the reads of your own tables (Reads)
|
|
95
|
+
lib/actions.js the writes of your own tables: the one-row writes and runAction (Writes)
|
|
96
|
+
lib/db.js the database pool: query(), logActivity() — a server app's, for its server files
|
|
97
|
+
lib/door.js your own tables from the browser, through the data door; the hooks (Door)
|
|
98
|
+
lib/brain.js the company brain from the browser, in both modes; its hooks (Brain)
|
|
99
|
+
lib/brain-server.js the brain's reads for a server component — a server app's
|
|
100
|
+
lib/claw.js the _claw API the gateway answers, from the browser
|
|
101
|
+
lib/platform.js typed wrappers over lib/claw.js (Platform)
|
|
102
|
+
lib/hooks.js the React hooks over them: useJob, useArtifact… (Hooks)
|
|
103
|
+
lib/sample.js markSample, for sample rows (§3a)
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Per mode
|
|
107
|
+
|
|
108
|
+
A browser app lays its own copy of six fixed files (the template's
|
|
109
|
+
`templates/next/browser/`). Each is fixed and pinned like the rest, and
|
|
110
|
+
keeps the server copy's names, so a surface imports what it always did:
|
|
111
|
+
|
|
112
|
+
```
|
|
113
|
+
app/layout.jsx built once: the theme set by a script in <head> before the page paints; examples decided at build (§3a)
|
|
114
|
+
app/page.jsx / → a page that forwards to the home surface, keeping the query
|
|
115
|
+
app/s/[key]/page.jsx one file per surface, whose surface renders in the browser inside <Suspense>
|
|
116
|
+
app/auto-refresh.jsx asks the data door what changed, and every hook on the page reads again (§7)
|
|
117
|
+
lib/data.js lib/door.js's reads and hooks, under the usual name
|
|
118
|
+
lib/actions.js lib/door.js's writes, under the usual name
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
And it has none of these, since nothing of it runs on a server:
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
proxy.js the platform serves the files; there is no request of the app's to check
|
|
125
|
+
app/claw-alive/route.js no route handler runs; the auto-refresh asks the door instead
|
|
126
|
+
lib/db.js no database login in the app: the door holds it, and knows who is asking
|
|
127
|
+
lib/brain-server.js the brain's reads run in the browser, through lib/brain.js
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
`npx makersclaw mode <browser|server>` is the only switch: it lays that
|
|
131
|
+
mode's fixed files, removes the other's, and says what your own code
|
|
132
|
+
must change (§4).
|
|
133
|
+
|
|
83
134
|
## 3. Design first
|
|
84
135
|
|
|
85
136
|
Before any code, write `app/DESIGN.md` — one screen of text:
|
|
@@ -168,7 +219,9 @@ shadcn project. There a surface may still open on a worked example —
|
|
|
168
219
|
`useFixture(rows, SAMPLE)`, §3b — so its author sees it filled.
|
|
169
220
|
`app/layout.jsx` decides per request: examples are off whenever
|
|
170
221
|
`CLAW_ARTIFACT_ID` is set (`product_run` sets it for every workspace app)
|
|
171
|
-
and on otherwise; `CLAW_EXAMPLES=1` forces them on.
|
|
222
|
+
and on otherwise; `CLAW_EXAMPLES=1` forces them on. A browser app's
|
|
223
|
+
layout decides the same once, at build: the platform builds every browser
|
|
224
|
+
app with its `CLAW_ARTIFACT_ID`. Inside a workspace
|
|
172
225
|
`useFixture` hands back the real rows — none — so the same surface falls
|
|
173
226
|
through to `LiveEmpty`. Up to 0.3.x a workspace app opened on its example
|
|
174
227
|
until its table had a row; since 0.4.0 it opens empty.
|
|
@@ -494,7 +547,7 @@ default import, framed wherever it is rendered, and is never dropped
|
|
|
494
547
|
into the shell, the kit or `lib/`: rendered outside its frame it inherits
|
|
495
548
|
the workspace, and the check says so.
|
|
496
549
|
|
|
497
|
-
**One door, one bundle.** The folder is reached one way: a
|
|
550
|
+
**One door, one bundle.** The folder is reached one way: a plain
|
|
498
551
|
`import XPost from "…/depictions/x-post"` at the top of the file that
|
|
499
552
|
frames it. No `import()`, no `next/dynamic`, no re-export module, no
|
|
500
553
|
alias (`const P = XPost`), no picture passed along as a value. Through
|
|
@@ -507,15 +560,83 @@ never reaches the page.
|
|
|
507
560
|
|
|
508
561
|
## 4. Server and client
|
|
509
562
|
|
|
563
|
+
### Which mode
|
|
564
|
+
|
|
565
|
+
**An app is a browser app unless it needs server code.** Server code
|
|
566
|
+
means one of three things:
|
|
567
|
+
|
|
568
|
+
- a route handler: a proxy to a service with a secret, a render to PDF;
|
|
569
|
+
- an upload larger than the data door takes;
|
|
570
|
+
- SQL a member types (`sql-console`).
|
|
571
|
+
|
|
572
|
+
Nothing else needs a server. A browser app reads and writes its own
|
|
573
|
+
tables and the company brain through the door, runs a write of more
|
|
574
|
+
than one row as an action (§5), and has no process of its own to start,
|
|
575
|
+
stop or crash. `makersclaw.json`'s `mode` says which this app is
|
|
576
|
+
(`"browser"` is a browser app; §2). An app moves with `npx makersclaw
|
|
577
|
+
mode server` (or `browser`), and its own code moves in the same build:
|
|
578
|
+
the command prints what must change.
|
|
579
|
+
|
|
580
|
+
### A browser app
|
|
581
|
+
|
|
582
|
+
Its pages are built once, with no request, into files the platform
|
|
583
|
+
serves. Everything below runs in the member's browser:
|
|
584
|
+
|
|
585
|
+
- **Every surface is a client component.** Its file starts with `"use
|
|
586
|
+
client"`. It reads through the hooks: `useRows`, `useRow`, `useCount`,
|
|
587
|
+
`useCountBy` and `useActivity` from `lib/data.js`, and the brain's
|
|
588
|
+
`useRecords`, `useRecord`, `useRecordCounts` and `useBrainSearch` from
|
|
589
|
+
`lib/brain.js`. It hands rows to blocks once they have loaded, and
|
|
590
|
+
wires the blocks' callbacks to `lib/actions.js` (§5, Door, has a
|
|
591
|
+
whole surface).
|
|
592
|
+
- **It receives `surface` alone.** No `searchParams`: the page has no
|
|
593
|
+
request. `?row=`, `?list=` and any other view state come from
|
|
594
|
+
`useSearchParams()` (`next/navigation`).
|
|
595
|
+
- **No `process.env` in its code but `CLAW_BASE_PATH`, `CLAW_PRODUCT`
|
|
596
|
+
and `NEXT_PUBLIC_*`.** A prerendered page prints what it reads into a
|
|
597
|
+
file anyone who opens the app can fetch. A secret stays with the
|
|
598
|
+
platform; the build never sees `.env`.
|
|
599
|
+
- No `"use server"`, no `route.js`, no `proxy.js`, no import of
|
|
600
|
+
`next/headers`, `next/cache`, `next/server`, `pg` or `lib/db`, no
|
|
601
|
+
route segment config (`dynamic`, `revalidate`…). There is no server to
|
|
602
|
+
run them, and `npm run check` names each one (§6, `browser`).
|
|
603
|
+
|
|
604
|
+
```jsx
|
|
605
|
+
// surfaces/investors.jsx (a browser app)
|
|
606
|
+
"use client";
|
|
607
|
+
import { useSearchParams } from "next/navigation";
|
|
608
|
+
import { useRows } from "../lib/data";
|
|
609
|
+
import InvestorBoard from "../components/InvestorBoard";
|
|
610
|
+
|
|
611
|
+
export default function Investors() {
|
|
612
|
+
const highlight = useSearchParams().get("row");
|
|
613
|
+
const { rows, error, loading } = useRows({ source: "investors", orderBy: "updated_at desc" });
|
|
614
|
+
if (loading) return null; // nothing until the first answer: useFixture must not take a read in flight for an empty table
|
|
615
|
+
return <InvestorBoard rows={rows} error={error} highlight={highlight} />;
|
|
616
|
+
}
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
`components/InvestorBoard.jsx` is the same file in both modes (below):
|
|
620
|
+
it calls `moveRow` from `lib/actions.js`, which in a browser app goes
|
|
621
|
+
through the door.
|
|
622
|
+
|
|
623
|
+
### A server app
|
|
624
|
+
|
|
510
625
|
One split, stated once:
|
|
511
626
|
|
|
512
627
|
```
|
|
513
628
|
lib/data.js, lib/db.js server components and server actions only
|
|
514
629
|
lib/platform.js, lib/hooks.js "use client" files only
|
|
515
630
|
lib/actions.js either (they are server actions)
|
|
631
|
+
lib/brain.js "use client" files only, in a server app too (§5, Brain)
|
|
632
|
+
lib/door.js "use client" files only: the own tables from the browser (§5, Door)
|
|
633
|
+
lib/brain-server.js server components only: the brain's reads
|
|
516
634
|
surfaces.js server files only (entries import components)
|
|
517
635
|
```
|
|
518
636
|
|
|
637
|
+
In a browser app every `lib/` file but `sample.js` is for `"use client"`
|
|
638
|
+
files, and `surfaces.js` is the pages'.
|
|
639
|
+
|
|
519
640
|
Pass plain data across the boundary — strings, numbers, arrays, objects.
|
|
520
641
|
`plain(row)` already turns a timestamp into its ISO string. A `date`
|
|
521
642
|
column is its day, `"2026-09-28"`: text, the same day for every reader,
|
|
@@ -537,7 +658,7 @@ import { loadRows } from "../lib/data";
|
|
|
537
658
|
import InvestorBoard from "../components/InvestorBoard";
|
|
538
659
|
|
|
539
660
|
export default async function Investors({ surface, searchParams }) {
|
|
540
|
-
const { rows, error } = await loadRows({ source: "
|
|
661
|
+
const { rows, error } = await loadRows({ source: "investors", orderBy: "updated_at desc" });
|
|
541
662
|
return <InvestorBoard rows={rows} error={error} highlight={searchParams.row ?? null} />;
|
|
542
663
|
}
|
|
543
664
|
```
|
|
@@ -559,7 +680,11 @@ export default function InvestorBoard({ rows: real, error, highlight }) {
|
|
|
559
680
|
|
|
560
681
|
Every surface component receives `surface` (`{ key, kind, label, source
|
|
561
682
|
}`) and `searchParams` (the URL query; `?row=<id>` is the convention for
|
|
562
|
-
"highlight this row" — an activity line opens its surface with it).
|
|
683
|
+
"highlight this row" — an activity line opens its surface with it). A
|
|
684
|
+
browser app's surface receives `surface` alone and reads the query with
|
|
685
|
+
`useSearchParams()` (above).
|
|
686
|
+
`source` is the table's bare name, `"investors"`, with no schema in
|
|
687
|
+
front: the app's tables are in its own schema (§5, Your tables).
|
|
563
688
|
|
|
564
689
|
### Views by link
|
|
565
690
|
|
|
@@ -572,10 +697,12 @@ part of the contract, the same way `?row=` is:
|
|
|
572
697
|
- `?list=<value>` shows one list. An app that keeps separate lists in
|
|
573
698
|
shared tables declares which column names them (`lists: { table,
|
|
574
699
|
column }`), and every surface over that table reads `list` and filters
|
|
575
|
-
by that column — `loadRows({ source, filter:
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
700
|
+
by that column — `loadRows({ source, filter: { [column]:
|
|
701
|
+
searchParams.list } })`; in a browser app, `useRows({ source, filter:
|
|
702
|
+
{ [column]: useSearchParams().get("list") ?? undefined } })`. With no
|
|
703
|
+
`list` it shows every list: a column
|
|
704
|
+
whose condition is `undefined` is no condition (up to 0.6.x it matched
|
|
705
|
+
no row). It is the one key every app with lists honours.
|
|
579
706
|
- A job's view may declare a filter (`week=this&list={list}`; `{list}`
|
|
580
707
|
and `{since}` are filled in per request). Its keys are query params
|
|
581
708
|
the surface ALREADY reads: declare one only for a param the surface
|
|
@@ -608,9 +735,14 @@ action="/…">`. A real external URL (`https://`, `mailto:`) is fine.
|
|
|
608
735
|
|
|
609
736
|
### 4b. Writes are rejected — "Invalid Server Actions request"
|
|
610
737
|
|
|
738
|
+
A server app only. A browser app has no Server Actions: its writes go
|
|
739
|
+
to the data door on the page's own origin, and nothing here applies to
|
|
740
|
+
them. Its preview still runs on `next dev`, so the dev server's
|
|
741
|
+
`allowedDevOrigins` below holds for it too.
|
|
742
|
+
|
|
611
743
|
Already handled. Do not fix it in `next.config.mjs`.
|
|
612
744
|
|
|
613
|
-
Next
|
|
745
|
+
Next refuses a Server Action whose `Origin` header disagrees with
|
|
614
746
|
`x-forwarded-host`, and in this stack they always disagree: the sandbox
|
|
615
747
|
ingress rewrites `x-forwarded-host` to its own vercel.run name while the
|
|
616
748
|
browser is on the product host. Untreated, the app builds, loads and
|
|
@@ -620,7 +752,9 @@ button rather than a proxy policy.
|
|
|
620
752
|
`next.config.mjs` already reads `CLAW_ALLOWED_ORIGINS`, which `product_run`
|
|
621
753
|
derives per environment (`src/lib/products.ts`, `productAllowedOrigins`):
|
|
622
754
|
the product's own FQDN in production, the dashboard's dev origin under a
|
|
623
|
-
path prefix.
|
|
755
|
+
path prefix. The same hosts are the dev server's `allowedDevOrigins`:
|
|
756
|
+
Next 16's `next dev` refuses its own resources (the HMR socket, the error
|
|
757
|
+
overlay) to any other host, and says "Blocked cross-origin request".
|
|
624
758
|
|
|
625
759
|
**Never hardcode an origin here.** `allowedOrigins: ["localhost:5173"]`
|
|
626
760
|
passes every test you can run in development and refuses every write in
|
|
@@ -629,53 +763,205 @@ source — say so and stop; it is not an app-level fix.
|
|
|
629
763
|
|
|
630
764
|
## 5. API reference
|
|
631
765
|
|
|
766
|
+
### Your tables — one schema, named bare
|
|
767
|
+
|
|
768
|
+
The app's tables live in its own schema, and no other app reads them.
|
|
769
|
+
In Makersclaw that is `app_<name>`, and `makersclaw.json` says
|
|
770
|
+
`"tables": "own"`. Besides its own tables, the app's database role
|
|
771
|
+
reaches only what the platform lays for it under `claw.` and
|
|
772
|
+
`claw_app.` (the activity door, `claw_app.activity`, below).
|
|
773
|
+
|
|
774
|
+
**Name every table bare: `deals`, with no schema in front — neither
|
|
775
|
+
`public.` nor `app_<name>.`.** The role's search path sends a bare name
|
|
776
|
+
to the app's schema. A schema written by hand breaks the app: its role
|
|
777
|
+
reaches nothing in `public`, and a second copy of the app lives in a
|
|
778
|
+
schema of another name. Bare holds everywhere a table is named: a
|
|
779
|
+
surface's `source`, every `lib/data.js` and `lib/actions.js` call, and
|
|
780
|
+
SQL of your own (`update deals set …`). `npm run check` refuses a
|
|
781
|
+
qualified name (§6, `tables`).
|
|
782
|
+
|
|
783
|
+
One schema per app, not one per copy of its code. A change build's
|
|
784
|
+
preview reads and writes the live app's tables, so rows saved while
|
|
785
|
+
testing are real rows. In a change to a live app, add what the change
|
|
786
|
+
needs (new tables, new columns that allow null or have a default);
|
|
787
|
+
remove or rename in a build after it ships.
|
|
788
|
+
|
|
789
|
+
**The company's things are not your tables.** A customer, a person, a
|
|
790
|
+
product — anything another app or a person would look up — is a record
|
|
791
|
+
in the company brain, read and written through `lib/brain.js` (Brain,
|
|
792
|
+
below). A row of yours points at a record by a `uuid` column named for
|
|
793
|
+
its kind (`contact_id`), declared in `links`. Never copy a record's
|
|
794
|
+
fields into a table of yours: the copy goes stale the day someone edits
|
|
795
|
+
the record.
|
|
796
|
+
|
|
797
|
+
`makersclaw init --tables own|public` records where the tables live;
|
|
798
|
+
Makersclaw passes it. It never changes afterwards: an app's tables move
|
|
799
|
+
from `public` to their own schema only with its workspace. An app laid
|
|
800
|
+
before 0.7.0 has no `tables` key: its tables are in `public`, and a
|
|
801
|
+
bare name reaches them there too. So write names bare in either.
|
|
802
|
+
|
|
632
803
|
### Reads — `lib/data.js` (server)
|
|
633
804
|
|
|
805
|
+
In a browser app `lib/data.js` is the door's (Door, below): these same
|
|
806
|
+
names, shapes and filters, called from `"use client"` files, with a hook
|
|
807
|
+
for each.
|
|
808
|
+
|
|
634
809
|
Every read resolves `{ ..., error }` and never throws. `error` is text
|
|
635
810
|
when the table is missing or the database unreachable; it is also
|
|
636
811
|
`console.error`ed, so the log tail carries it to the platform agent.
|
|
637
|
-
`source` is
|
|
812
|
+
`source` is the table's bare name (`"drafts"`); names go through
|
|
813
|
+
`ident()`. It still takes `"schema.table"`, for an app from before
|
|
814
|
+
0.7.0 whose code names `public.<table>`.
|
|
638
815
|
|
|
639
816
|
```js
|
|
640
817
|
loadRows({ source, idColumn = "id", orderBy, limit = 500, filter }) → { rows, error }
|
|
641
818
|
loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none
|
|
642
819
|
countRows({ source, filter }) → { count, error }
|
|
643
820
|
countBy({ source, column, filter }) → { counts: { [value]: n }, error }
|
|
644
|
-
loadActivity({ limit = 100, surface, since }) → { rows, error } //
|
|
821
|
+
loadActivity({ limit = 100, surface, since }) → { rows, error } // claw_app.activity: this app's lines
|
|
645
822
|
```
|
|
646
823
|
|
|
647
|
-
`filter` is `{ column:
|
|
648
|
-
|
|
649
|
-
|
|
824
|
+
`filter` is an AND of `{ column: condition }`, at most 20: the
|
|
825
|
+
platform data door's grammar, so a filter means the same in a server
|
|
826
|
+
app and a browser one.
|
|
827
|
+
|
|
828
|
+
```
|
|
829
|
+
value equals (text, a number, true or false, a Date)
|
|
830
|
+
[value, …] one of, at most 1,000
|
|
831
|
+
null empty
|
|
832
|
+
{ not: value | null } anything else, empty rows included; or not empty
|
|
833
|
+
{ gt | gte | lt | lte: v } a bound; one lower and one upper may combine
|
|
834
|
+
{ has: value } an array column (text[], int[]…) holds it
|
|
835
|
+
{ prefix: "text" } starts with, case ignored
|
|
836
|
+
```
|
|
837
|
+
|
|
838
|
+
A column whose condition is `undefined` is left out. Anything else is
|
|
839
|
+
refused, and the read returns why as its `error`. `orderBy` is
|
|
840
|
+
`"updated_at desc, id"`; default `idColumn desc`. `limit` caps at 5000
|
|
841
|
+
(activity: 1000).
|
|
650
842
|
|
|
651
843
|
```js
|
|
652
|
-
const { rows, error } = await loadRows({ source: "
|
|
653
|
-
const {
|
|
844
|
+
const { rows, error } = await loadRows({ source: "drafts", filter: { stage: ["draft", "approved"] } });
|
|
845
|
+
const { rows: due } = await loadRows({ source: "tasks", filter: { done: false, due: { lte: today }, owner: { not: null } } });
|
|
846
|
+
const { counts } = await countBy({ source: "investors", column: "stage" });
|
|
654
847
|
const { rows: log } = await loadActivity({ surface: "drafts", limit: 50 });
|
|
655
848
|
```
|
|
656
849
|
|
|
657
850
|
Helpers: `plain(row)` (timestamps → ISO, buffers → base64; a `date` is
|
|
658
851
|
already its `"YYYY-MM-DD"`), `orderClause(orderBy,
|
|
659
|
-
fallback)` (validated `ORDER BY` text), `
|
|
852
|
+
fallback)` (validated `ORDER BY` text), `whereClause(filter, params)`
|
|
853
|
+
(the filter above as `WHERE` text), `markSample(sample, idColumn)`
|
|
660
854
|
(stable ids + `_sample: true`), `currentSurface(explicit)` (the surface
|
|
661
855
|
key of the request, from `x-claw-surface`, unless one is passed).
|
|
662
856
|
|
|
663
857
|
### Writes — `lib/actions.js` (server actions, callable from either side)
|
|
664
858
|
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
859
|
+
In a browser app `lib/actions.js` is the door's (Door, below): these
|
|
860
|
+
same names, shapes and codes, as plain functions of the browser. The
|
|
861
|
+
door runs each write and writes its activity line.
|
|
862
|
+
|
|
863
|
+
In a server app each quotes names with `ident()`, runs in its own
|
|
864
|
+
transaction, writes one activity row through `logActivity` after it
|
|
865
|
+
commits, calls `revalidatePath("/", "layout")`, and resolves `{ ok }`
|
|
866
|
+
or `{ ok: false, error, code }`. Never throws. `code` is the data
|
|
867
|
+
door's (Door, below), so a surface branches on it the same way in both
|
|
868
|
+
modes: `not_found` (no such row), `no_such_table`, `no_such_action`,
|
|
869
|
+
`action_refused`, `conflict`, `bad_request`, `too_slow`, `try_again`,
|
|
870
|
+
else `failed`. `surface` defaults to the request's surface key
|
|
871
|
+
(the `<key>` of `/s/<key>`, which proxy.js reads).
|
|
669
872
|
|
|
670
873
|
```js
|
|
671
|
-
updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data })
|
|
874
|
+
updateRow({ source, idColumn = "id", id, patch, surface, action = "edited", detail, data, log })
|
|
672
875
|
moveRow({ source, idColumn = "id", id, stateColumn, to, surface }) // action "moved"
|
|
673
876
|
insertRow({ source, idColumn = "id", values, surface, action = "added", detail }) → { ok, id }
|
|
674
877
|
deleteRow({ source, idColumn = "id", id, surface, detail }) // action "removed"
|
|
675
878
|
saveSetting({ mode = "table" | "json", source, keyColumn = "key", valueColumn = "value",
|
|
676
879
|
column, idColumn = "id", id, key, value, surface })
|
|
880
|
+
runAction({ name, args, surface, action, detail, rowRef, log }) → { ok, result }
|
|
881
|
+
```
|
|
882
|
+
|
|
883
|
+
`log: false` (`updateRow` and `runAction` only) writes no activity line:
|
|
884
|
+
for a save that is typing, not a decision, like an autosave every few
|
|
885
|
+
keystrokes. An insert and a delete always log. Name such a write "no
|
|
886
|
+
activity line" in the brief (`references/actions.md`), so the agent logs
|
|
887
|
+
none for it either. The door takes the same `log: false` in a browser
|
|
888
|
+
app.
|
|
889
|
+
|
|
890
|
+
**Who is asking.** Each write's transaction first sets two settings,
|
|
891
|
+
local to it: `claw.member_id` and `claw.member_email`, the member the
|
|
892
|
+
platform named on the request (`''` when none: a local run, a job).
|
|
893
|
+
The platform's data door sets the same two for a browser app, and the
|
|
894
|
+
agent's `sql` for the member it acts for. A default, a trigger or an
|
|
895
|
+
action reads them:
|
|
896
|
+
|
|
897
|
+
```sql
|
|
898
|
+
nullif(current_setting('claw.member_email', true), '')
|
|
677
899
|
```
|
|
678
900
|
|
|
901
|
+
A column the page must not choose (`created_by`, `approved_by`) is set
|
|
902
|
+
by a `before insert` or `before update` trigger that reads it, never
|
|
903
|
+
sent from the page: a default alone keeps whatever the page sends.
|
|
904
|
+
Made with `sql`, once per table:
|
|
905
|
+
|
|
906
|
+
```sql
|
|
907
|
+
create or replace function deals_created_by() returns trigger
|
|
908
|
+
language plpgsql as $$
|
|
909
|
+
begin
|
|
910
|
+
new.created_by := nullif(current_setting('claw.member_email', true), '');
|
|
911
|
+
return new;
|
|
912
|
+
end $$;
|
|
913
|
+
create trigger deals_created_by before insert on deals
|
|
914
|
+
for each row execute function deals_created_by();
|
|
915
|
+
```
|
|
916
|
+
|
|
917
|
+
It is a label, not a permission: nothing checks it, and the app's own
|
|
918
|
+
agent can set it too.
|
|
919
|
+
|
|
920
|
+
**Actions.** A write of more than one row, or one that decides
|
|
921
|
+
something, is an action: a function in the app's own schema, `(args
|
|
922
|
+
jsonb) returns jsonb`, made with the `sql` verb. The screen calls it
|
|
923
|
+
with `runAction`, which runs `select <name>($1::jsonb)` with `args` as
|
|
924
|
+
JSON (`{}` when none); the app's agent runs the same function with
|
|
925
|
+
`sql`, so the two can never drift. Its name is one bare, lower-case
|
|
926
|
+
name (`close_cycle`): the data door calls only a function of the app's
|
|
927
|
+
own schema, so a name with a schema or capitals (`CloseCycle`) is
|
|
928
|
+
`no_such_action` in both modes. It refuses with `raise exception '<sentence>'`, which
|
|
929
|
+
comes back as `error`, code `action_refused`. It never writes
|
|
930
|
+
`claw_app.activity`: `runAction` logs the call (`action` defaults to the
|
|
931
|
+
function's name, `row_ref` is `rowRef` or the result's `id`), and the
|
|
932
|
+
agent logs its own.
|
|
933
|
+
|
|
934
|
+
```sql
|
|
935
|
+
create or replace function close_cycle(args jsonb) returns jsonb
|
|
936
|
+
language plpgsql as $$
|
|
937
|
+
declare
|
|
938
|
+
c uuid := (args->>'cycle')::uuid;
|
|
939
|
+
begin
|
|
940
|
+
update cycles set status = 'closed', closed_at = now() where id = c and status = 'open';
|
|
941
|
+
if not found then raise exception 'that cycle is already closed'; end if;
|
|
942
|
+
update goals set status = 'carried' where cycle_id = c and status = 'open';
|
|
943
|
+
return jsonb_build_object('id', c);
|
|
944
|
+
end $$;
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
```js
|
|
948
|
+
const r = await runAction({ name: "close_cycle", args: { cycle: id }, action: "closed", detail: "Q3" });
|
|
949
|
+
if (!r.ok) setError(r.error); // "that cycle is already closed"
|
|
950
|
+
```
|
|
951
|
+
|
|
952
|
+
**Name every action in the agent's brief, `references/actions.md`,
|
|
953
|
+
with its `select`**: `` `close_cycle` `` and `select
|
|
954
|
+
close_cycle($1::jsonb)`, its params and what it refuses. The agent
|
|
955
|
+
runs it from there, and `npm run check` refuses an action the screens
|
|
956
|
+
run that the brief does not name (§6, `agent`).
|
|
957
|
+
|
|
958
|
+
In a change to a live app, never change or drop a function the live app
|
|
959
|
+
runs, a helper one included: the preview writes the live schema, so the
|
|
960
|
+
live screens would run the new body at once, and a roll back would run
|
|
961
|
+
it too. Write a new one (`close_cycle_2`), call it from the new code and
|
|
962
|
+
name it in `references/actions.md`. Drop the old one once neither the
|
|
963
|
+
live build nor the one kept for roll back calls it.
|
|
964
|
+
|
|
679
965
|
`saveSetting` table mode updates the `(key, value)` row and inserts one
|
|
680
966
|
when missing — a plain `key text, value text` table is enough. JSON mode
|
|
681
967
|
sets `key` inside the jsonb `column` of row `id`.
|
|
@@ -690,19 +976,288 @@ the column's default: `updated_at timestamptz not null default now()`.
|
|
|
690
976
|
```js
|
|
691
977
|
"use client";
|
|
692
978
|
import { moveRow } from "../lib/actions";
|
|
693
|
-
const r = await moveRow({ source: "
|
|
979
|
+
const r = await moveRow({ source: "investors", id, stateColumn: "stage", to: "sent" });
|
|
694
980
|
if (!r.ok) setError(r.error);
|
|
695
981
|
```
|
|
696
982
|
|
|
697
983
|
Never write raw SQL from a surface. `lib/*.js` is fixed (`npm run check`
|
|
698
984
|
refuses an edit, and `makersclaw update` lays the file again), so a write
|
|
699
|
-
these cannot make
|
|
700
|
-
`"use server"` on its first line,
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
985
|
+
these cannot make is an action (above), called with `runAction`. In a
|
|
986
|
+
server app only, a server-action file of the app's own (`"use server"` on its first line,
|
|
987
|
+
beside the components that call it: `components/deal-actions.js`) may
|
|
988
|
+
compose the writes above. A new row still goes through `insertRow`:
|
|
989
|
+
the check refuses a raw `INSERT INTO` anywhere outside `lib/`, and, in
|
|
990
|
+
an app on its own tables, any SQL that writes in the app's code (§6,
|
|
991
|
+
`agent`).
|
|
992
|
+
|
|
993
|
+
### Door — `lib/door.js` (browser, both modes)
|
|
994
|
+
|
|
995
|
+
The app's own tables, read and written from the browser through the
|
|
996
|
+
platform's data door, `_claw/data`. The gateway answers it under the
|
|
997
|
+
app's path, as the member whose cookie comes with the request. Each
|
|
998
|
+
write runs on the app's own database login, in one transaction that
|
|
999
|
+
knows who is asking (`claw.member_email`, above), and the door writes
|
|
1000
|
+
its activity line.
|
|
1001
|
+
|
|
1002
|
+
A browser app (`"mode": "browser"` in `makersclaw.json`) has no server of
|
|
1003
|
+
its own. Its `lib/data.js` and `lib/actions.js` are this file under
|
|
1004
|
+
their usual names, so a surface imports as it always did:
|
|
1005
|
+
`import { useRows } from "../lib/data"`, `import { moveRow } from
|
|
1006
|
+
"../lib/actions"`. In a server app a `"use client"` component may take
|
|
1007
|
+
the hooks from here.
|
|
1008
|
+
|
|
1009
|
+
```js
|
|
1010
|
+
// Reads: lib/data.js's names and shapes
|
|
1011
|
+
loadRows({ source, idColumn = "id", orderBy, limit = 500, filter, offset, cursor }) → { rows, error, next }
|
|
1012
|
+
loadRow({ source, idColumn = "id", id }) → { row, error } // row null when none, or no id
|
|
1013
|
+
countRows({ source, filter }) → { count, error }
|
|
1014
|
+
countBy({ source, column, filter }) → { counts, error }
|
|
1015
|
+
loadActivity({ limit = 100, surface, since }) → { rows, error }
|
|
1016
|
+
|
|
1017
|
+
// Writes: lib/actions.js's names and shapes
|
|
1018
|
+
insertRow({ source, idColumn, values, surface, action, detail }) → { ok, id }
|
|
1019
|
+
updateRow({ source, idColumn, id, patch, surface, action, detail, log }) → { ok }
|
|
1020
|
+
moveRow({ source, idColumn, id, stateColumn, to, surface }) → { ok }
|
|
1021
|
+
deleteRow({ source, idColumn, id, surface, detail }) → { ok }
|
|
1022
|
+
saveSetting({ mode, source, keyColumn, valueColumn, column, idColumn, id, key, value, surface })
|
|
1023
|
+
runAction({ name, args, surface, action, detail, rowRef, log }) → { ok, result }
|
|
1024
|
+
|
|
1025
|
+
// Hooks: a read on mount, and again on dataChanged()
|
|
1026
|
+
useRows(opts) useRow(opts) useCount(opts) useCountBy(opts) useActivity(opts)
|
|
1027
|
+
→ { …the read's values, error, loading, reload }
|
|
1028
|
+
dataChanged() every hook on the page reads again
|
|
1029
|
+
whenLanded() resolves once those reads have landed
|
|
1030
|
+
```
|
|
1031
|
+
|
|
1032
|
+
- `filter` is the grammar above (Reads), sent as it is. `orderBy` is
|
|
1033
|
+
`"updated_at desc"`. `cursor` is the last answer's `next`, which the
|
|
1034
|
+
door gives only when `orderBy` names one column; `offset` is at most
|
|
1035
|
+
10,000.
|
|
1036
|
+
- Every function resolves and never throws. A read that fails keeps its
|
|
1037
|
+
empty values: `{ rows: [], error, code }`. A write that fails is
|
|
1038
|
+
`{ ok: false, error, code }`, never `undefined`. Branch on `code`;
|
|
1039
|
+
show `error`.
|
|
1040
|
+
- No door answers (a copy on its own, an older Makersclaw):
|
|
1041
|
+
`unavailable: true`, code `unavailable`. Show a quiet "not connected
|
|
1042
|
+
yet".
|
|
1043
|
+
- The codes a page meets: `bad_request` (a column, filter or order the
|
|
1044
|
+
table does not have), `no_such_table`, `no_such_action`, `not_found`,
|
|
1045
|
+
`read_only` (a view: write the table it reads), `action_refused` (an
|
|
1046
|
+
action's own `raise exception`: show its sentence), `conflict` (a
|
|
1047
|
+
unique or foreign key), `too_slow`, `try_again`, `rate_limited`.
|
|
1048
|
+
- Writes go one at a time, in the order they were called, as Server
|
|
1049
|
+
Actions did. A slow save never lands after the newer one.
|
|
1050
|
+
- A write that answers ok calls `dataChanged()`: every hook on the page
|
|
1051
|
+
reads again, the brain's too. When the next step needs the new rows
|
|
1052
|
+
(open the row just made), `await whenLanded()` first.
|
|
1053
|
+
- `loading` is true until a hook's first answer, and again when its
|
|
1054
|
+
options change. A re-read keeps the last rows, and `loading` false,
|
|
1055
|
+
until the new ones land; each answer is a new array. A failed re-read
|
|
1056
|
+
keeps the rows and sets `error`.
|
|
1057
|
+
- A hook compares its options by value on every render: a new object
|
|
1058
|
+
with the same values reads nothing. A server component read once per
|
|
1059
|
+
request; a hook's own answer is a render. So a time in a bound
|
|
1060
|
+
(`filter`, the brain's `where`, `since`; a Date or ISO text) is
|
|
1061
|
+
compared to the minute: `useActivity({ since: new Date(Date.now() - 7
|
|
1062
|
+
* 864e5) })` reads once, and again only after the minute has moved
|
|
1063
|
+
on, keeping its rows meanwhile. The read sends the time as given.
|
|
1064
|
+
Anything else worked out on each render (a `Date.now()` number) is new
|
|
1065
|
+
options every time, and reads every time: work it out once
|
|
1066
|
+
(`useMemo`, `useState`).
|
|
1067
|
+
- `useRow` with no `id` asks nothing: `row` is null.
|
|
1068
|
+
- `surface` defaults to the page's surface key (the `<key>` of
|
|
1069
|
+
`/s/<key>` in its path). `data` on `updateRow` is taken and never
|
|
1070
|
+
sent: the door never puts a value in a line. `log: false` as in
|
|
1071
|
+
Writes.
|
|
1072
|
+
- The door writes the activity line. Nothing in the browser logs.
|
|
1073
|
+
|
|
1074
|
+
```jsx
|
|
1075
|
+
// surfaces/deals.jsx (a browser app)
|
|
1076
|
+
"use client";
|
|
1077
|
+
import { useSearchParams } from "next/navigation";
|
|
1078
|
+
import { useRows, whenLanded } from "../lib/data";
|
|
1079
|
+
import { insertRow, moveRow } from "../lib/actions";
|
|
1080
|
+
import { Notice } from "../components/ui";
|
|
1081
|
+
import { Board } from "../components/blocks/board";
|
|
1082
|
+
|
|
1083
|
+
export default function Deals() {
|
|
1084
|
+
const opened = useSearchParams().get("row");
|
|
1085
|
+
const { rows, error, loading } = useRows({ source: "deals", orderBy: "updated_at desc" });
|
|
1086
|
+
if (error) return <Notice tone="danger">{error}</Notice>;
|
|
1087
|
+
return (
|
|
1088
|
+
<Board
|
|
1089
|
+
rows={loading ? undefined : rows}
|
|
1090
|
+
groupBy="stage"
|
|
1091
|
+
columns={["lead", "call", "won"]}
|
|
1092
|
+
onMove={(d, to) => moveRow({ source: "deals", id: d.id, stateColumn: "stage", to })}
|
|
1093
|
+
onCreate={async (name, stage) => {
|
|
1094
|
+
const r = await insertRow({ source: "deals", values: { name, stage } });
|
|
1095
|
+
if (r.ok) await whenLanded();
|
|
1096
|
+
return r;
|
|
1097
|
+
}}
|
|
1098
|
+
/>
|
|
1099
|
+
);
|
|
1100
|
+
}
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
### Brain — `lib/brain.js` (browser, both modes)
|
|
1104
|
+
|
|
1105
|
+
The company brain is the workspace's shared records: contacts,
|
|
1106
|
+
companies, deals, whatever kinds the workspace keeps. Each record sits
|
|
1107
|
+
in an area (Everyone, Finance…). Every app sees the same Jane Doe.
|
|
1108
|
+
|
|
1109
|
+
An app reaches the brain only through the data door, `_claw/data`. The
|
|
1110
|
+
gateway answers it under the app's path, so it works while the app's
|
|
1111
|
+
machine sleeps. It hands the app only the kinds `makersclaw.json`
|
|
1112
|
+
declares, and only the records the member may read. Never SQL: the
|
|
1113
|
+
app's database login holds none of the brain.
|
|
1114
|
+
|
|
1115
|
+
**What the app declares: `brain` in `makersclaw.json`.** Write it
|
|
1116
|
+
before the first call. The platform grants it when the app ships; a
|
|
1117
|
+
ship that widens it waits for the member's Yes.
|
|
1118
|
+
|
|
1119
|
+
```json
|
|
1120
|
+
"brain": {
|
|
1121
|
+
"kinds": [
|
|
1122
|
+
{ "kind": "contact", "label": "Contact", "plural": "Contacts",
|
|
1123
|
+
"fields": [{ "name": "name", "type": "text" }, { "name": "email", "type": "email" }],
|
|
1124
|
+
"key": ["email"], "filters": [] }
|
|
1125
|
+
],
|
|
1126
|
+
"reads": ["note"],
|
|
1127
|
+
"writes": ["contact"],
|
|
1128
|
+
"links": [{ "table": "tasks", "column": "contact_id", "kind": "contact", "on_erase": "null" }]
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
- `kinds`: the full def of every kind the app reads or writes, since
|
|
1133
|
+
the workspace may not have it yet. `note`, the brain's own kind, needs
|
|
1134
|
+
none.
|
|
1135
|
+
- `reads` and `writes`: kinds by name. Writing implies reading. The door
|
|
1136
|
+
answers any other kind `not_declared`.
|
|
1137
|
+
- `links`: a `uuid` column of one of the app's own tables (named bare,
|
|
1138
|
+
`"tasks"`) that holds a record's id, and what erasing that record does
|
|
1139
|
+
to the row (`null` or `delete`). Own tables only (`"tables": "own"`).
|
|
1140
|
+
|
|
1141
|
+
`npm run check` checks the block's shape and that every call names a
|
|
1142
|
+
kind it declares (§6). The rest (field types, keys, each link against
|
|
1143
|
+
your tables) the platform checks when the app ships.
|
|
1144
|
+
|
|
1145
|
+
Call these from `"use client"` files, in a server app too. The grant
|
|
1146
|
+
cookie is who is asking, and it makes a write the member's press:
|
|
1147
|
+
confirmed, and able to overwrite a confirmed value, erase or merge. A
|
|
1148
|
+
write from the app's server would be app code acting: unconfirmed, and
|
|
1149
|
+
skipped over a confirmed value. So there is no Server Action for the
|
|
1150
|
+
brain.
|
|
1151
|
+
|
|
1152
|
+
```js
|
|
1153
|
+
import { listRecords, writeRecord } from "../lib/brain";
|
|
1154
|
+
|
|
1155
|
+
// Reads
|
|
1156
|
+
kinds() → { kinds, brain, error }
|
|
1157
|
+
listRecords({ kind, where, order, limit, cursor, fields, prefix, state })
|
|
1158
|
+
→ { records, next, error }
|
|
1159
|
+
getRecord({ id, links = false, state }) → { record, links, error }
|
|
1160
|
+
countRecords({ kind, by, where }) → { count, by, error }
|
|
1161
|
+
searchBrain({ q, kinds, limit, meaning }) → { results, meaning, error }
|
|
1162
|
+
recordHistory({ id, limit, cursor }) → { changes, next, error }
|
|
1163
|
+
brainChanges({ since, kinds }) → { changes, next, error }
|
|
1164
|
+
brainLists({ kind }) → { lists, error }
|
|
1165
|
+
listMembers({ list, limit, cursor }) → { list, records, next, error }
|
|
1166
|
+
|
|
1167
|
+
// Writes
|
|
1168
|
+
writeRecord({ kind, id, fields, area, from, ifVersion, owner, surface, detail })
|
|
1169
|
+
→ { ok, id, created, changed, version, skipped }
|
|
1170
|
+
writeRecords({ writes }) → { ok, results } // at most 100, all or none
|
|
1171
|
+
confirmFields({ id, fields }) → { ok, confirmed, version }
|
|
1172
|
+
eraseRecord({ id, reason }) → { ok, erased } // gone for every app, no undo
|
|
1173
|
+
mergeRecords({ keep, drop }) → { ok, id, merged, filled, version }
|
|
1174
|
+
linkRecords({ from, to, type }) → { ok, created }
|
|
1175
|
+
unlinkRecords({ from, to, type }) → { ok, removed }
|
|
1176
|
+
createList({ name, kind, area }) → { ok, id }
|
|
1177
|
+
changeList({ list, add, remove, position }) → { ok, added, removed }
|
|
1178
|
+
|
|
1179
|
+
// Hooks: a read on mount, and again on dataChanged() (lib/door.js's rules)
|
|
1180
|
+
useRecords(args) useRecord(args) // no id: record null, nothing asked
|
|
1181
|
+
useRecordCounts(args) useBrainSearch(args) // a blank q asks nothing
|
|
1182
|
+
→ { …the read's values, error, loading, reload }
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
Every function resolves and never throws:
|
|
1186
|
+
|
|
1187
|
+
- A read resolves the door's JSON with `error: null`.
|
|
1188
|
+
- A write resolves the door's JSON.
|
|
1189
|
+
- A refusal is `{ ok: false, error, code }`. A read keeps its empty
|
|
1190
|
+
values (`records: []`, `next: null`), so the surface renders it like
|
|
1191
|
+
no rows. Branch on `code`; show `error`.
|
|
1192
|
+
- A 404 with no code, or no network, is
|
|
1193
|
+
`{ ok: false, unavailable: true }`: a claw with no door yet. Show a
|
|
1194
|
+
quiet "not connected yet".
|
|
1195
|
+
|
|
1196
|
+
Arguments are camelCase (`ifVersion`). Results are the door's keys as
|
|
1197
|
+
they come (`label_field`, `updated_at`).
|
|
1198
|
+
|
|
1199
|
+
A record is `{ id, kind, area, label, fields, state, unconfirmed, owner,
|
|
1200
|
+
source, created_at, updated_at, version }`. `fields` holds the values;
|
|
1201
|
+
`state` says, field by field, who wrote it and whether a person
|
|
1202
|
+
confirmed it.
|
|
1203
|
+
|
|
1204
|
+
- `where` is an AND of conditions on the kind's fields:
|
|
1205
|
+
`{ stage: "lead" }` equals, `{ stage: ["lead", "won"] }` one of,
|
|
1206
|
+
`{ phone: null }` empty, `{ not: v }`, `{ gt | gte | lt | lte: v }`,
|
|
1207
|
+
`{ has: v }` (a choices field), `{ prefix: "ja" }`. Also `area`,
|
|
1208
|
+
`owner_id`, `unconfirmed`, `updated_at`. A kind's declared `filters` are fast;
|
|
1209
|
+
another field scans the kind and may answer `too_slow`.
|
|
1210
|
+
- `order` is `-updated_at` (the default), `updated_at`, `created_at`,
|
|
1211
|
+
`-created_at`, `label`, `-label`, `<field>` or `-<field>`.
|
|
1212
|
+
- `limit` is 50 by default, at most 200. `cursor` is the last answer's
|
|
1213
|
+
`next`; `next` is null at the end.
|
|
1214
|
+
- `state: false` leaves each field's state out. `fields: ["name"]`
|
|
1215
|
+
returns only those.
|
|
1216
|
+
- `searchBrain` looks by words and by meaning. `meaning: false` is words
|
|
1217
|
+
only: nothing is embedded and nothing is charged. Use it for a picker
|
|
1218
|
+
that searches as the member types. A blank `q` asks nothing.
|
|
1219
|
+
- `brainChanges` is what changed in the member's areas and the app's
|
|
1220
|
+
kinds since `since`, the last answer's `next`.
|
|
1221
|
+
- `ifVersion` refuses a write with `changed` when someone saved the
|
|
1222
|
+
record after the page read it.
|
|
1223
|
+
- `surface` defaults to the page's surface key (the `<key>` of
|
|
1224
|
+
`/s/<key>` in its path: `contacts`, not `/s/contacts`); `detail` is
|
|
1225
|
+
one sentence.
|
|
1226
|
+
|
|
1227
|
+
The door writes the activity line for every write it takes (Activity,
|
|
1228
|
+
below). Never call `logActivity` for a brain write. Writes go one at a
|
|
1229
|
+
time, in the same turn as `lib/door.js`'s. One that answers ok calls
|
|
1230
|
+
`dataChanged()`, so every hook on the page reads again (these four and
|
|
1231
|
+
the own tables'); a server app may also `router.refresh()`.
|
|
1232
|
+
|
|
1233
|
+
The codes a page meets: `not_declared` (the app did not declare that
|
|
1234
|
+
kind, or not for writing), `not_granted` (the workspace has not granted
|
|
1235
|
+
the app's areas yet), `area_not_granted`, `unknown_kind`, `not_found`,
|
|
1236
|
+
`bad_field`, `bad_request`, `changed`, `hidden_match` (a record with
|
|
1237
|
+
that key is in an area the member cannot read), `key_taken`,
|
|
1238
|
+
`not_a_person`, `rate_limited`, `too_slow`, `not_signed_in`.
|
|
1239
|
+
|
|
1240
|
+
**Server reads — `lib/brain-server.js`.** The same nine reads, same
|
|
1241
|
+
arguments and results, for a server component in a server app (a
|
|
1242
|
+
browser app has no such file: its brain reads are the hooks above). They go
|
|
1243
|
+
with the door grant the product proxy stamps on each member's request
|
|
1244
|
+
(`x-claw-door`), to `CLAW_DOOR_URL`. It has no writes. With no grant (a
|
|
1245
|
+
render outside a member's request, or a machine whose proxy does not
|
|
1246
|
+
stamp it yet) each read answers
|
|
1247
|
+
`{ ok: false, unavailable: true, code: "no_door" }`: read in the browser
|
|
1248
|
+
instead.
|
|
1249
|
+
|
|
1250
|
+
```js
|
|
1251
|
+
// surfaces/contacts.jsx (server)
|
|
1252
|
+
import { listRecords } from "../lib/brain-server";
|
|
1253
|
+
import ContactList from "../components/ContactList";
|
|
1254
|
+
|
|
1255
|
+
export default async function Contacts({ searchParams }) {
|
|
1256
|
+
const first = await listRecords({ kind: "contact", order: "label" });
|
|
1257
|
+
// no_door: ContactList reads with lib/brain.js in the browser.
|
|
1258
|
+
return <ContactList first={first.code === "no_door" ? null : first} highlight={searchParams.row ?? null} />;
|
|
1259
|
+
}
|
|
1260
|
+
```
|
|
706
1261
|
|
|
707
1262
|
### Platform — `lib/platform.js` (browser)
|
|
708
1263
|
|
|
@@ -789,10 +1344,10 @@ while the App tab is on screen (behind the window's other tabs it asks
|
|
|
789
1344
|
nothing; coming back looks at once); a start through `useJob` on the
|
|
790
1345
|
same page makes it look at once. Hand it to `LiveEmpty` (§3a).
|
|
791
1346
|
|
|
792
|
-
There is no chat
|
|
793
|
-
|
|
794
|
-
with `loadActivity
|
|
795
|
-
|
|
1347
|
+
There is no chat or runs hook: the OS draws those tabs. A domain
|
|
1348
|
+
surface that wants a slice of the activity (a row's history beside the
|
|
1349
|
+
row, say) reads it with `loadActivity`: on the server in a server app,
|
|
1350
|
+
or with `useActivity` (`lib/door.js`, §5 Door) from the browser.
|
|
796
1351
|
|
|
797
1352
|
### Commissioning work — `useJob()`
|
|
798
1353
|
|
|
@@ -951,12 +1506,18 @@ request, desk_origin } }`) or refused it; it never waits for the work.
|
|
|
951
1506
|
|
|
952
1507
|
### Activity — the contract
|
|
953
1508
|
|
|
954
|
-
`
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
`
|
|
959
|
-
|
|
1509
|
+
`claw_app.activity` is how the founder sees the work. It is the
|
|
1510
|
+
platform's view of the workspace's activity table, and it holds only
|
|
1511
|
+
this app's lines. The OS's **Activity tab** on the app window reads
|
|
1512
|
+
them, newest first — and so can any domain surface that wants to
|
|
1513
|
+
(`loadActivity`). In a server app every `lib/actions.js` write inserts a
|
|
1514
|
+
line through `logActivity()` in `lib/db.js` (but a `log: false` patch or
|
|
1515
|
+
action). In a browser app the door writes it, for every write it takes
|
|
1516
|
+
(`lib/door.js`), with the same `log: false`. The app's agent writes the
|
|
1517
|
+
same view
|
|
1518
|
+
with the `sql` verb, `by = 'agent'` or `'run:<id>'`. A brain write's
|
|
1519
|
+
line is the door's: it writes one for every write it takes (`lib/brain.js`
|
|
1520
|
+
never logs, and neither does the page).
|
|
960
1521
|
|
|
961
1522
|
```js
|
|
962
1523
|
await logActivity({
|
|
@@ -969,13 +1530,22 @@ await logActivity({
|
|
|
969
1530
|
});
|
|
970
1531
|
```
|
|
971
1532
|
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1533
|
+
The view fills in `artifact_id` (the app whose role writes the line)
|
|
1534
|
+
and `id`, so an insert names neither; one that names another app's id
|
|
1535
|
+
is refused. Columns: `id, artifact_id, at, action, surface, row_ref,
|
|
1536
|
+
detail, data (jsonb), by`. `loadActivity` returns them without
|
|
1537
|
+
`artifact_id` — every line is this app's. The OS Activity tab shows the
|
|
1538
|
+
surface's label on each line and does not link into the app yet; the
|
|
1539
|
+
convention for anything that does (a domain surface's own history slice
|
|
1540
|
+
today, the tab later) is `surface` + `row_ref` → `/s/<surface>?row=<row_ref>`,
|
|
1541
|
+
so stamp both on every line and make the surface honour `?row=`.
|
|
1542
|
+
|
|
1543
|
+
Up to 0.6.x the lines went into the `artifact_activity` table in
|
|
1544
|
+
`public`, named with `CLAW_ARTIFACT_ID`. An app on its own schema can
|
|
1545
|
+
reach nothing in `public`, so since 0.7.0 both functions go through the
|
|
1546
|
+
view; the lines are the same. Outside Makersclaw there is no view:
|
|
1547
|
+
`logActivity` warns and returns false, and `loadActivity` returns the
|
|
1548
|
+
database's `error`.
|
|
979
1549
|
|
|
980
1550
|
### Errors
|
|
981
1551
|
|
|
@@ -996,9 +1566,9 @@ files has to). The platform agent reads them as console events. Call
|
|
|
996
1566
|
line per rule, under twenty lines, exit 1 on a failure that names the
|
|
997
1567
|
file and line and what to use instead. It builds for itself, into
|
|
998
1568
|
`.next-check`, and only when a source changed since the last check —
|
|
999
|
-
never run `npm run build` first, and never build into `.next`:
|
|
1000
|
-
|
|
1001
|
-
|
|
1569
|
+
never run `npm run build` first, and never build into `.next`: it is not
|
|
1570
|
+
the check's folder. In a working copy the preview's dev server owns it;
|
|
1571
|
+
in a live app it holds the build the platform made and serves.
|
|
1002
1572
|
|
|
1003
1573
|
```
|
|
1004
1574
|
check: 5 files · 2 surfaces
|
|
@@ -1009,10 +1579,20 @@ ok manifest 2 surfaces: drafts, sent
|
|
|
1009
1579
|
ok icons every icon name exists
|
|
1010
1580
|
ok kit 70 fixed files match the template
|
|
1011
1581
|
ok depictions 1 picture of elsewhere, framed on 1 surface: x-post
|
|
1582
|
+
ok agent no SQL write in app code; the agent's brief names every action with its call (1), every table written (2) and every server action that writes (0)
|
|
1583
|
+
ok tables its own schema (makersclaw.json): every table named bare, and each bare name one of the 2 its schema makes (../install/schema.sql); claw. and claw_app. are the platform's
|
|
1584
|
+
ok brain no brain block in makersclaw.json; no lib/brain.js call names a kind
|
|
1012
1585
|
ok smoke built in 41s · / → 307 · /s/drafts 200 · /s/sent 200 (0.6s)
|
|
1013
1586
|
all clear
|
|
1014
1587
|
```
|
|
1015
1588
|
|
|
1589
|
+
A browser app's check has one more line, and its smoke serves files:
|
|
1590
|
+
|
|
1591
|
+
```
|
|
1592
|
+
ok browser files and a door: 2 client surfaces; no server code, route, proxy or segment config; env only by the names a page may print
|
|
1593
|
+
ok smoke built in 38s (copy at ~/.claw-check/app) · export 2.1 MB in 64 files · / → 200 to /s/drafts · /s/drafts 200 · /s/sent 200 (0.1s)
|
|
1594
|
+
```
|
|
1595
|
+
|
|
1016
1596
|
`kit` refuses a fixed file that differs from the template (they are
|
|
1017
1597
|
hashed in `check/pins.json`) and any file of yours under `components/ui`
|
|
1018
1598
|
or `components/parts`. `depictions` reads both directions: nothing of
|
|
@@ -1020,8 +1600,117 @@ the workspace inside a depiction (a token, `font-sans`, `dark:`, a rem
|
|
|
1020
1600
|
utility), nothing of the depiction outside its frame (a named import, a
|
|
1021
1601
|
picture in the shell or a `Card`, a dead file nobody renders).
|
|
1022
1602
|
|
|
1603
|
+
`agent` holds the app's agent to what the screens do. Its brief beside
|
|
1604
|
+
the app (`../AGENTS.md`, `../references/*.md`) names, in backticks, every
|
|
1605
|
+
server action that writes. In an app on its own tables (`"tables":
|
|
1606
|
+
"own"`) the screens write through two doors only, so it also refuses:
|
|
1607
|
+
|
|
1608
|
+
- SQL that writes in the app's code: `insert into`, `update … set` or
|
|
1609
|
+
`delete from`, in any case, in a string of any `.js`, `.jsx` or `.mjs`
|
|
1610
|
+
but the fixed `lib/`, where it reads as a statement (a `where`, a
|
|
1611
|
+
`values`, a `set col =`, a `;` after it; handed to `query()`; or the
|
|
1612
|
+
whole string). Both modes, brief or none. A one-row write is
|
|
1613
|
+
`insertRow`, `updateRow`, `moveRow`, `deleteRow` or `saveSetting`;
|
|
1614
|
+
anything else is an action (§5). Prose in JSX text and a label in an
|
|
1615
|
+
attribute (`title="Delete from board"`) are not SQL; the same words
|
|
1616
|
+
alone in a string of code are, so word such a label otherwise.
|
|
1617
|
+
- The data door by its path: a string naming `_claw/data`,
|
|
1618
|
+
`data/actions/` or `data/tables/`, or `claw("data/…")`. The screens
|
|
1619
|
+
reach the door through its functions, which this rule and the ship
|
|
1620
|
+
can read.
|
|
1621
|
+
- An action the brief does not name with its call. For `runAction({
|
|
1622
|
+
name: "close_cycle" })` the brief says `` `close_cycle` `` and `select
|
|
1623
|
+
close_cycle(`, so the agent runs the same function:
|
|
1624
|
+
```
|
|
1625
|
+
- `close_cycle`: the Close cycle button. Run `select close_cycle($1::jsonb)`
|
|
1626
|
+
with params `[{"cycle": "<id>"}]` (sql execute). It carries the open goals
|
|
1627
|
+
over and refuses a closed cycle. Then log `closed` on `cycles`.
|
|
1628
|
+
```
|
|
1629
|
+
- A table a one-row write names (`source: "deals"`) that the brief does
|
|
1630
|
+
not name in backticks, with the one statement that does the same.
|
|
1631
|
+
- Brief or none: a name the code decides at run time (`runAction({ name:
|
|
1632
|
+
kind })`: name it in the code, so the brief can), `runAction("x")`
|
|
1633
|
+
(it takes one object) and a name that is not one bare, lower-case name.
|
|
1634
|
+
|
|
1635
|
+
A name or a source is read where it is written: a literal, or a `const`
|
|
1636
|
+
string of the same file that the call sees; a surface's `source:
|
|
1637
|
+
surface.source` is its entry's in `surfaces.js`. A call counts however
|
|
1638
|
+
the file reaches the function: imported, renamed, through `import * as`,
|
|
1639
|
+
with `?.(`, or through an app file that re-exports it. A one-row write
|
|
1640
|
+
whose table the code decides at run time (a prop) is not checked, and
|
|
1641
|
+
the ok line says how many. `node check/run.mjs
|
|
1642
|
+
--actions` prints what the screens run and write, `{"actions": […],
|
|
1643
|
+
"tables": […]}`, and exits 0: the platform's ship checks that every
|
|
1644
|
+
action is a function in the app's schema before it builds. An app with
|
|
1645
|
+
no `tables` key keeps the first sentence of this rule only.
|
|
1646
|
+
|
|
1647
|
+
`tables` reads every table name in the app's code when `makersclaw.json`
|
|
1648
|
+
says `"tables": "own"`: a surface's `source`, the `source` of every
|
|
1649
|
+
`lib/actions.js` and `lib/data.js` call (a `const` string of the same
|
|
1650
|
+
file counts, where the call sees it), and the name after `from`, `join`,
|
|
1651
|
+
`into` and `update` in SQL (not a column's `from`: `extract(epoch from
|
|
1652
|
+
t.at)`, `is distinct from`). Your tables are named bare (`deals`): the app's own schema answers
|
|
1653
|
+
them, and a second copy of the app lives in another. It refuses
|
|
1654
|
+
`public.<t>` (an app on its own schema reaches nothing there),
|
|
1655
|
+
`app_<x>.<t>` (a schema by name, even this app's), and `brain.<t>` or
|
|
1656
|
+
`brain_api.<t>` (the company brain goes through `lib/brain.js`, §5).
|
|
1657
|
+
`claw.` and `claw_app.` are the platform's (`claw_app.activity`, the
|
|
1658
|
+
activity view) and pass. An app laid before 0.7.0 has no `tables` key:
|
|
1659
|
+
its tables are in `public`, and this rule leaves its names alone.
|
|
1660
|
+
`"tables": "moved"` is refused: that is the platform's word for an old
|
|
1661
|
+
app whose tables its workspace moved, never a manifest's.
|
|
1662
|
+
|
|
1663
|
+
And each bare name must be a table or view the app's own schema makes,
|
|
1664
|
+
so none leans on something else. The list is the first found of:
|
|
1665
|
+
`CLAW_APP_TABLES` (the platform passes it when it ships the app, read
|
|
1666
|
+
from the live schema), `../install/schema.sql` (a play folder's), and
|
|
1667
|
+
`../../schema.sql` (a store package's), from their `create table` and
|
|
1668
|
+
`create view`. The names read: a surface's `source` in `surfaces.js`
|
|
1669
|
+
(a literal or a const), the `source` of a `lib/data.js`, `lib/door.js`
|
|
1670
|
+
or `lib/actions.js` call by its name (`loadRows`, `useRows`,
|
|
1671
|
+
`insertRow`…, either side of a ternary), and in SQL — strings joined
|
|
1672
|
+
with `+` read as one, handed to `query()` or starting as a statement —
|
|
1673
|
+
the name after `from` (and after its commas), `join`, `insert into`,
|
|
1674
|
+
`update … set`, a delete's or merge's `using`, and `table`, `truncate`
|
|
1675
|
+
or `copy`. A CTE, a function (`from unnest(…)`), a SQL string or
|
|
1676
|
+
comment and `pg_*` are not tables. Not read: a call through an import
|
|
1677
|
+
alias, options held in a variable (`loadRows(opts)`), SQL a helper takes
|
|
1678
|
+
that does not start as a statement, and SQL built at run time. With no
|
|
1679
|
+
list here, the ok line says "no table list here". The same SQL is read
|
|
1680
|
+
for `public.`, `app_<x>.` and `brain.` names that only it shows: after a
|
|
1681
|
+
comma, in a `using`, with spaces around the dot.
|
|
1682
|
+
|
|
1683
|
+
`brain` reads the `brain` block of `makersclaw.json` (§5) and every
|
|
1684
|
+
call of `lib/brain.js` or `lib/brain-server.js` whose kind is written in
|
|
1685
|
+
the code (`kind: "contact"`, or a `const` string the call sees).
|
|
1686
|
+
The block's `kinds`, `reads`, `writes` and `links` are arrays, and each
|
|
1687
|
+
kind has `kind`, `label`, `plural` and `fields`. A read's kind
|
|
1688
|
+
(`listRecords`, `countRecords`, `brainLists`; the `kinds` of
|
|
1689
|
+
`searchBrain` and `brainChanges`) is in `reads` or `writes`. A write's
|
|
1690
|
+
(`writeRecord`, each entry of `writeRecords`, `createList`) is in
|
|
1691
|
+
`writes`. A kind that only `kinds` defines is refused too: the door
|
|
1692
|
+
hands the app only what `reads` and `writes` name. A kind held in a
|
|
1693
|
+
variable is not read here; the door refuses it on the member's screen.
|
|
1694
|
+
|
|
1695
|
+
`browser` reads a browser app only (`"mode": "browser"`): its pages are
|
|
1696
|
+
files built once, and everything they read or write goes through the
|
|
1697
|
+
platform's data door. So it refuses, file and line, a `"use server"`
|
|
1698
|
+
module or function; an import of `next/headers`, `next/cache`,
|
|
1699
|
+
`next/server`, `pg`, `lib/db` or `lib/brain-server`; a `route.js` under
|
|
1700
|
+
`app/`; a `proxy.js` or `middleware.js` at the root (under export Next
|
|
1701
|
+
ignores it); an export of `dynamic`, `revalidate`, `fetchCache`,
|
|
1702
|
+
`runtime` or `dynamicParams`; a surface whose first line is not `"use
|
|
1703
|
+
client"`; and a `process.env` name other than `CLAW_BASE_PATH`,
|
|
1704
|
+
`CLAW_PRODUCT`, `NEXT_PUBLIC_*` and `NODE_ENV` — a prerendered page
|
|
1705
|
+
prints what it reads into a file anyone who opens the app can fetch.
|
|
1706
|
+
|
|
1023
1707
|
The smoke test starts that build on a loopback port and loads every
|
|
1024
|
-
surface; `--
|
|
1708
|
+
surface; `--no-build` skips the build and the smoke. A browser app's smoke
|
|
1709
|
+
builds in a copy of the app (`~/.claw-check/<app>`), never in it, with
|
|
1710
|
+
no `.env`; serves the export by the platform's rules; and checks that
|
|
1711
|
+
`/` forwards to a surface and every `/s/<key>` answers 200 with no error
|
|
1712
|
+
page. That proves every page prerenders, not that a surface renders
|
|
1713
|
+
with data: the rows come in the browser. Then, by eye:
|
|
1025
1714
|
|
|
1026
1715
|
- Renders at 360px, 768px and 1280px without horizontal scroll.
|
|
1027
1716
|
- In a workspace, an empty table shows `LiveEmpty`: the work in flight,
|
|
@@ -1030,48 +1719,95 @@ surface; `--static` skips the build and the smoke. Then, by eye:
|
|
|
1030
1719
|
- Error state shows the text from `{ error }`.
|
|
1031
1720
|
- Keyboard focus visible on every control.
|
|
1032
1721
|
- Numbers tabular (`data-nums`).
|
|
1033
|
-
- Every write
|
|
1034
|
-
`
|
|
1722
|
+
- Every write goes through `lib/actions.js` (a one-row write or
|
|
1723
|
+
`runAction`), or in a server app the app's own `"use server"` file
|
|
1724
|
+
(§5) — never raw SQL in a surface.
|
|
1035
1725
|
- Every surface reachable from the nav.
|
|
1036
1726
|
|
|
1037
1727
|
## 7. Run
|
|
1038
1728
|
|
|
1039
1729
|
1. `makersclaw init` laid this into the artifact's `app/` (in Makersclaw,
|
|
1040
|
-
`artifact_scaffold {dir}` runs it
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1730
|
+
`artifact_scaffold {dir}` runs it). `makersclaw.json` records the
|
|
1731
|
+
version, the blocks, where the tables live (`"tables"`, from
|
|
1732
|
+
`init --tables`; §5, Your tables) and the mode (`"mode"`, from `init
|
|
1733
|
+
--mode`; §2). `makersclaw update` moves the fixed files and blocks
|
|
1734
|
+
to a newer version; it never touches your own files, and keeps every
|
|
1735
|
+
other key of `makersclaw.json` (`tables`, `mode`, `brain`). Only
|
|
1736
|
+
`makersclaw mode` changes the mode (§4). `pnpm-lock.yaml` is cut from the lock this version ships
|
|
1737
|
+
(by init, add and update), so every app on the version installs the
|
|
1738
|
+
same packages; `makersclaw install` installs from it. Never edit it
|
|
1739
|
+
by hand.
|
|
1740
|
+
2. `app_database {name, dir}` gives the app its own schema and role.
|
|
1741
|
+
A server app gets `DATABASE_URL` in its environment; a browser app
|
|
1742
|
+
gets none, since its pages reach the tables through the door and its
|
|
1743
|
+
agent through `sql`. The role reads and writes the app's own tables,
|
|
1744
|
+
by bare name. The platform lays the activity door,
|
|
1745
|
+
`claw_app.activity`, with the database (§5, Activity); the app makes
|
|
1746
|
+
no activity table of its own.
|
|
1747
|
+
3. Make the domain tables with the `sql` verb, bare
|
|
1748
|
+
(`create table if not exists deals (…)`): they land in the app's own
|
|
1749
|
+
schema. An app's first tables are often there before its first
|
|
1750
|
+
build, made by the chat that created it (`artifact_manage
|
|
1751
|
+
install_tables`): read them with `sql schema` first. An app from
|
|
1752
|
+
before 0.7.0 makes them in `public`, with the same bare names.
|
|
1047
1753
|
4. `product_run` with this directory. While building, run it as a dev
|
|
1048
1754
|
server — `command "npx next dev -H 127.0.0.1"`, `build ""` — so each
|
|
1049
1755
|
edit shows in the founder's live preview without a rebuild.
|
|
1050
1756
|
`product_run` sets `CLAW_BASE_PATH`, `CLAW_PRODUCT`,
|
|
1051
1757
|
`CLAW_PRODUCT_TOKEN` and `CLAW_ARTIFACT_ID` — the last is what turns
|
|
1052
|
-
examples off (§3a).
|
|
1758
|
+
examples off (§3a). A browser app previews the same way, on `next
|
|
1759
|
+
dev`; its surfaces read through the door there too.
|
|
1760
|
+
5. `npm run check` (§6) before `surface_built` and before launch. For a
|
|
1761
|
+
browser app its smoke builds the export as the platform will: in a
|
|
1762
|
+
copy of the app (`~/.claw-check/<app>`), never in it; with no `.env`;
|
|
1763
|
+
served by the platform's rules. It proves every page prerenders and
|
|
1764
|
+
`/` forwards to a surface, not that a surface renders with data: look
|
|
1765
|
+
at the preview for that.
|
|
1053
1766
|
|
|
1054
1767
|
Every `artifact_files` write under `app/` comes back with the console's
|
|
1055
1768
|
state; a 5xx carries the log tail. Fix it before moving on — the founder
|
|
1056
1769
|
is watching the preview.
|
|
1057
1770
|
|
|
1058
|
-
|
|
1059
|
-
build never needs the database.
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1771
|
+
In a server app every page is `dynamic = "force-dynamic"`: nothing is
|
|
1772
|
+
prerendered, so a build never needs the database.
|
|
1773
|
+
|
|
1774
|
+
A browser app's pages are the opposite: `next build` prerenders each once
|
|
1775
|
+
(`output: "export"`), `/` as a page that forwards to the home surface
|
|
1776
|
+
(a script keeping the query, and a meta refresh) and
|
|
1777
|
+
`/s/<key>` as one file per surface, whose surface renders in the browser
|
|
1778
|
+
inside `<Suspense>`. Its theme is set by a script in `<head>` before the
|
|
1779
|
+
page paints: `?theme=`, then the `claw_theme` cookie, then dark. The
|
|
1780
|
+
platform builds it with `CLAW_BASE_PATH`, `CLAW_PRODUCT` and
|
|
1781
|
+
`CLAW_ARTIFACT_ID` and nothing else of the app's environment, and
|
|
1782
|
+
serves the files it leaves; no code of the app's runs on the workspace's
|
|
1783
|
+
machine.
|
|
1784
|
+
|
|
1785
|
+
`app/auto-refresh.jsx` re-renders the page from the server while it is
|
|
1786
|
+
shown, so the agent's writes appear: one refresh at a time, 5 s after
|
|
1787
|
+
the last one landed plus what that one took (a minute at most), and only
|
|
1788
|
+
while the App tab is on screen in a visible browser tab (hidden, it asks
|
|
1789
|
+
nothing; back, it refreshes at once), nobody is typing (never while a
|
|
1790
|
+
sheet is open, a field they typed in holds focus, or within 10 s of a
|
|
1791
|
+
keystroke), and the app answers `/claw-alive` (a fixed route) with a
|
|
1792
|
+
204.
|
|
1793
|
+
|
|
1794
|
+
A browser app's `app/auto-refresh.jsx` keeps every one of those rules,
|
|
1795
|
+
but a tick does not re-render anything from a server. It asks the data
|
|
1796
|
+
door what changed (`_claw/data/changes`: the brain's changes in the
|
|
1797
|
+
app's kinds, and a stamp of the app's own tables), and only when
|
|
1798
|
+
something did, every hook on the page (`useRows` and the rest, the
|
|
1799
|
+
brain's too) reads again. A brain change shows on the next tick. A
|
|
1800
|
+
write the agent or a job makes to the app's own tables shows within
|
|
1801
|
+
about 11 s: the stamp comes from Postgres's counters, which lag.
|
|
1067
1802
|
|
|
1068
1803
|
**A surface that must not be refreshed opts out with
|
|
1069
1804
|
`data-claw-refresh="off"` on its root element**: one whose rows only its
|
|
1070
1805
|
own writes change, or a table too big to send every few seconds. While
|
|
1071
1806
|
any element on the page carries it, nothing is refreshed and nothing is
|
|
1072
1807
|
probed; the surface's own writes still re-render it (`lib/actions.js`
|
|
1073
|
-
revalidates
|
|
1074
|
-
|
|
1808
|
+
revalidates; in a browser app every write reads the hooks again), and so
|
|
1809
|
+
does every navigation. Set it from state to pause only while it matters:
|
|
1810
|
+
`data-claw-refresh={importing ? "off" : undefined}`.
|
|
1075
1811
|
|
|
1076
1812
|
**A page left open stops refreshing after 30 minutes with no sign of
|
|
1077
1813
|
anyone** in the app — a key, the pointer, a touch, a wheel, focus. The
|