From d66ac1010511f365abdb00172357eb57b5cd4664 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 31 Aug 2026 07:09:12 +0000 Subject: [PATCH] =?UTF-8?q?docs(skills):=20objectstack-data=20factual=20sw?= =?UTF-8?q?eep=20(3/3)=20=E2=80=94=20hooks=20reference,=20datasources,=20a?= =?UTF-8?q?nd=20the=20remaining=20rule=20files?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Four measured-false behavioral claims corrected against the implementation: - data-hooks.md: ctx.log is documented as { info, warn, error } in two places. It has FOUR levels — quickjs-runner.ts wires ['debug','info','warn','error'] and its own comment reads '[#7661] FOUR levels, not three'. - data-hooks.md: the sandbox ctx.api.object(n) method table omits three real methods — aggregate (api.read), updateMany and deleteMany (api.write). - datasources.md: 'field.columnName on managed objects is unaffected' — the key was removed in the 16.x line and is a parse error on ANY object. - datasources.md: 'external settings are ... forbidden otherwise' — a managed datasource carrying an external block parses fine. The required half holds. The other six files in this PR's scope (validation.md, hooks.md, lifecycle.md, naming.md, evals/README.md, references/_index.md) were inventoried and verified with no falsehood found, so they carry no edit. Token ratchet: both edited files shrink (-27 / -8). Fixes #13675 Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01EXxTW8mvPBhoHxmyPZ63de --- .../objectstack-data/references/data-hooks.md | 18 ++++++++---------- skills/objectstack-data/rules/datasources.md | 10 +++++----- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/skills/objectstack-data/references/data-hooks.md b/skills/objectstack-data/references/data-hooks.md index 2df6c8aa80..ec2dcd30fd 100644 --- a/skills/objectstack-data/references/data-hooks.md +++ b/skills/objectstack-data/references/data-hooks.md @@ -369,7 +369,7 @@ The sandbox is handed a **JSON snapshot** of these (built by | `ctx.event` | string | e.g. `'afterUpdate'` — dispatch on it when one hook subscribes to several events. | | `ctx.object` | string | The target object name. | | `ctx.api` | object | Cross-object CRUD. Gated by `api.read` / `api.write` — see below. | -| `ctx.log` | `{ info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). | +| `ctx.log` | `{ debug, info, warn, error }` | Gated by `log`. Call **`ctx.log.info(msg, data?)`** — `ctx.log` is an **object, not** callable as `ctx.log(msg)`. Emission is **best-effort** (see Troubleshooting). | | `ctx.crypto` | `{ randomUUID }` | Gated by `crypto.uuid`. | | `ctx.title` | `(field?) => Promise` | **Name the record instead of printing its id.** `await ctx.title()` resolves this object's `nameField` — including when it is a **formula**, evaluated server-side, with no extra read. `await ctx.title('account_id')` resolves the related record's title through a lookup column (one `findOne`, gated by `api.read`; the no-argument form needs no capability). `null` when there is no title — it never falls back to the id. | @@ -395,6 +395,7 @@ org / user / transaction. Methods: | `update(data, opts?)` | `api.write` | **`update({ id, ...fields })`** — put the id **inside** `data` | | `upsert(data, opts?)` | `api.write` | `upsert({ … })` | | `delete(opts)` | `api.write` | `delete({ where: { id } })` | +| `aggregate` · `updateMany` · `deleteMany` | read · write · write | also installed; same `where` shape | **Query shape — the key is `where`.** It takes an object with `$`-operators, the same DSL as the [objectstack-query](../../objectstack-query/SKILL.md) skill: @@ -443,11 +444,11 @@ set of legal tokens (`HookBodyCapability`) is exactly five: | Token | Unlocks | |:--|:--| -| `api.read` | `ctx.api.object(n).find` / `findOne` / `count`; also `ctx.title('')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. | -| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` | +| `api.read` | `ctx.api.object(n).find` / `findOne` / `count` / `aggregate`; also `ctx.title('')`, which reads that related record. Plain `ctx.title()` reads nothing and needs no token. | +| `api.write` | `ctx.api.object(n).insert` / `update` / `delete` / `upsert` / `updateMany` / `deleteMany` | | `api.transaction` | `ctx.api.transaction(async () => { … })` — runs the callback's `ctx.api` ops in **one driver transaction** (commit on return, rollback on throw). Pair it with `api.write`. | | `crypto.uuid` | `ctx.crypto.randomUUID()` | -| `log` | `ctx.log.info` / `warn` / `error(msg, data?)` | +| `log` | `ctx.log.debug` / `info` / `warn` / `error(msg, data?)` | There is **no `http.fetch` capability** by design — outbound calls go through Connector recipes so they stay auditable and replayable. @@ -553,8 +554,7 @@ Register it like any hook — add it to `defineStack({ hooks: [fillPositionOnHir capability it emits only when the runtime wired a logger into the hook context — otherwise it is a **silent no-op**. Treat `ctx.log` as best-effort diagnostics, not a reliable side-channel or proof a hook ran; to observe an effect, assert on - the data it writes. (And call `ctx.log.info(msg)` — `ctx.log` is an object, not - a function, so `ctx.log(msg)` is not callable.) + the data it writes. --- @@ -589,10 +589,8 @@ interface HookContext { organizationId?: string; // Active org — the single blessed name. Matches the // `organization_id` column + `current_user.organizationId` (RLS). // The former `tenantId` alias was removed in v16. - // There is no `roles` here: `session.roles` was declared but - // never produced, and was retired in 17.0.0. Privilege - // is judged by the security service (permissions / positions / - // posture), never by a role-name string in a hook. + // No `roles` here (retired in 17.0.0) — privilege is judged + // by the security service, never by a role name in a hook. accessToken?: string; isSystem?: boolean; // Elevated system context (engine self-writes). }; diff --git a/skills/objectstack-data/rules/datasources.md b/skills/objectstack-data/rules/datasources.md index fb40dedb40..4e8310862b 100644 --- a/skills/objectstack-data/rules/datasources.md +++ b/skills/objectstack-data/rules/datasources.md @@ -15,7 +15,7 @@ Full field reference: `node_modules/@objectstack/spec/src/data/datasource.zod.ts | `external` | A mature external DB ObjectStack does **not** own; DDL forbidden; boot mismatch **fails**. | | `validate-only` | Like `external`, but a mismatch **warns** instead of failing boot. | -`external` settings are required iff `schemaMode !== 'managed'` (and forbidden otherwise). +`external` settings are **required** when `schemaMode !== 'managed'`. ## Federated (external) objects @@ -38,10 +38,10 @@ ObjectSchema.create({ ### ✅ / ❌ Column mapping (ADR-0062 D7) - ✅ Map remote columns with **`external.columnMap`** (`remoteColumn → localField`). -- ❌ **Never set `field.columnName` on an external object.** The driver's query - pipeline ignores it for federated objects, so it is a silent dual-source trap. - `os build` / `os validate` **rejects** it with a clear error. (`field.columnName` - on **managed** objects is unaffected.) +- ❌ **`field.columnName` does not exist — on ANY object.** It was removed in the + 16.x line (the SQL driver hardcodes the physical column to the field key, so a + custom name was ignored), and authoring it is a parse error everywhere, not + only on a federated object. ## Auto-connect (no `onEnable`)