Skip to content

Commit b9f613e

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/fix-conflict-twdtn7
# Conflicts: # packages/lint/src/validate-expressions.test.ts
2 parents 143f3af + efbcfe1 commit b9f613e

72 files changed

Lines changed: 1264 additions & 634 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
---
2+
"@objectstack/spec": patch
3+
"@objectstack/objectql": patch
4+
---
5+
6+
Collapse the hook event taxonomy from 18 declared events to the 8 the engine actually dispatches (#3195). The removed 10 (`beforeFindOne`/`afterFindOne`, `beforeCount`/`afterCount`, `beforeAggregate`/`afterAggregate`, `beforeUpdateMany`/`afterUpdateMany`, `beforeDeleteMany`/`afterDeleteMany`) were declared in `HookEvent` but never fired — the enum mirrored the engine method table instead of domain events, so a hook subscribing to them registered fine and then silently no-op'd.
7+
8+
- `findOne` now fires the same `beforeFind`/`afterFind` hooks as `find` — the read event attaches to record materialization, not the engine method, so one subscription covers every read shape (no separate `beforeFindOne`/`afterFindOne`).
9+
- Bulk (`multi: true`) updates/deletes already fire the singular `beforeUpdate`/`beforeDelete`/`afterUpdate`/`afterDelete` events with the row-scoping predicate in `ctx.input.ast`; this is now documented, and there is no `*Many` event.
10+
- Read authorization / row filtering is the RLS/permission-rule layer's job and field masking is field-level metadata — neither is a hook every author must re-attach.
11+
- `engine.registerHook` now warns when a hook subscribes to an event the engine never dispatches, so enum-vs-dispatch drift can't recur silently.
12+
13+
No shipped hook or authored metadata used any of the removed events; authoring one now fails loudly at parse/validate time instead of registering a dead hook. Skills and docs updated to teach the 8 events and the declarative alternatives.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/mcp": minor
3+
---
4+
5+
feat(mcp): `validate_expression` tool — validate a CEL expression against a schema before authoring (#1928)
6+
7+
Adds an agent-callable MCP tool that runs the same build-time expression checks
8+
as `objectstack build`, so an AI can validate a formula / predicate / flow
9+
condition **while authoring** instead of shipping one that silently evaluates to
10+
`null`. Given `{ objectName, expression, site? }` it resolves the object's real
11+
schema (field names + types, via the principal-bound `describeObject` bridge)
12+
and returns:
13+
14+
- **errors** — bare field refs (`amount``record.amount`), unknown fields
15+
(with a did-you-mean), unknown functions;
16+
- **warnings** — text/boolean fields misused in arithmetic, date-equality
17+
pitfalls;
18+
- **inScope** — the fields, stdlib functions, and namespace roots available, so
19+
the model can self-correct;
20+
- **inferredType** for a `formula` site.
21+
22+
`site` (`formula` | `validation` | `flow_condition` | `template`, default
23+
`formula`) maps to the validator's role + scope — `flow_condition` binds fields
24+
bare, the rest bind `record.<field>`. Read-only, gated by the `data:read` OAuth
25+
scope, and fail-closed on `sys_*` objects like the other schema tools. This is
26+
the authoring-time surface the guardrail series (#1928) always pointed at;
27+
`@objectstack/mcp` gains a `@objectstack/formula` dependency (acyclic; formula is
28+
a leaf).
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
"@objectstack/observability": minor
3+
"@objectstack/plugin-hono-server": minor
4+
"@objectstack/driver-sql": minor
5+
---
6+
7+
feat(observability): admin-only richer per-request timing detail via `X-OS-Debug-Timing: json` (#2408)
8+
9+
Completes the optional "richer JSON" diagnostic from #2408. In addition to the
10+
basic `Server-Timing` header, an admin/service caller can now request a
11+
per-query breakdown — the slowest SQL statements and a query count — by sending
12+
`X-OS-Debug-Timing: json`. The detail is returned in a separate
13+
`X-OS-Debug-Timing-Detail` response header (compact JSON) and is **admin-only,
14+
even under global mode**: an ordinary caller never sees SQL shapes.
15+
16+
- **observability**: `PerfTiming` gains opt-in per-event detail capture
17+
(`enableDetail` / `recordDetail` / `details`) plus the ambient
18+
`recordServerTimingDetail`. The disclosure gate gains a `privileged` level
19+
(set by `allowPerfDisclosure`, read via `isPerfDisclosurePrivileged`) so the
20+
richer detail can be gated independently of the basic header.
21+
- **driver-sql**: when detail capture is on, the query listener additionally
22+
records each query's **parametrized** statement (knex's `q.sql`, `?`
23+
placeholders) — never the bindings, so no literal row value ever enters the
24+
collector. Zero overhead when detail is off.
25+
- **plugin-hono-server**: `X-OS-Debug-Timing: json` enables detail capture; the
26+
middleware emits `X-OS-Debug-Timing-Detail` (slowest queries, capped and
27+
sanitized to header-safe ASCII) only when the principal is a proven admin.
28+
29+
Basic and global behavior are unchanged; `json` is purely additive.
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
---
2+
"@objectstack/spec": minor
3+
---
4+
5+
feat(spec)!: remove form-surfaced dead metadata props + correct 3 misclassified-live entries (#2377, ADR-0049)
6+
7+
The next enforce-or-remove slice of #2377. Versioned `minor` per the launch-window
8+
policy (the fixed group makes a `major` promote the whole monorepo).
9+
10+
## Removed (dead, no runtime reader — verified in both framework and objectui)
11+
12+
- **field**: `columnName`, `index`, `referenceFilters`. This empties the field
13+
dead-prop set. `columnName` also removed its now-moot **ADR-0062 D7** lint
14+
(`validate-expressions.ts`), the dead `StorageNameMapping.resolveColumnName` /
15+
`buildColumnMap` / `buildReverseColumnMap` helpers, and closes ADR-0062 R10 —
16+
external physical-column mapping is `external.columnMap` only.
17+
- **object**: `tags`, `active`, `abstract` — now rejecting tombstones in
18+
`UNKNOWN_KEY_GUIDANCE`.
19+
- **agent**: `tenantId`.
20+
21+
The removed props are dropped from the authoring forms (`field/object/agent.form.ts`)
22+
and the regenerated metadata-forms i18n bundles.
23+
24+
## Corrected to `live` (the ledger was wrong — readers existed)
25+
26+
- **object `isSystem`**`plugin-sharing` `effectiveSharingModel` defaults a
27+
no-`sharingModel` `isSystem` object to public; also read by the security-posture
28+
lint. KEPT.
29+
- **object `enable.searchable`**`metadata-protocol` global search (`searchAll`)
30+
uses `enable.searchable === false` as an opt-out. KEPT.
31+
- **action `type:'form'`** — objectui `ActionRunner.executeForm` routes it to the
32+
FormView at `/forms/:target`; a build-time lint validates the target. KEPT.
33+
34+
## Deliberately deferred
35+
36+
`object.enable.trash` / `enable.mru` — dead, but inert `default(true)` flags set by
37+
~35 `sys-*.object.ts` files; removing them is high-churn / low-value. Left `dead`
38+
(authorWarn-skipped).
39+
40+
## Migration
41+
42+
- field/agent props: authoring them was already a no-op; they now strip silently.
43+
`columnName` → the physical column is always the field key (rename the field, or
44+
use `external.columnMap` for external objects); `index` → declare it in object
45+
`indexes[]`; `referenceFilters``lookupFilters`.
46+
- object `tags`/`active`/`abstract`: `ObjectSchema.create()` now throws a located
47+
error naming the removal. None gated anything at runtime — remove them.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
"@objectstack/spec": minor
3+
"@objectstack/metadata-protocol": patch
4+
"@objectstack/client": patch
5+
---
6+
7+
**Breaking (discovery response shape): retire the residual feed capability surface (#3180, follow-up to #1959 / ADR-0052 §5).**
8+
9+
The feed backend was retired long ago; #1959 removed the feed contracts + SDK. This
10+
removes the last discovery/dispatcher references to it, and fixes a real bug where the
11+
`comments` capability was permanently `false`.
12+
13+
- `@objectstack/spec``WellKnownCapabilitiesSchema.feed` and `ApiRoutesSchema.feed`
14+
(`routes.feed`) are **removed**, and the `/api/v1/feed` entry is dropped from
15+
`DEFAULT_DISPATCHER_ROUTES`. FROM → TO: clients reading `discovery.capabilities.feed`
16+
or `discovery.routes.feed` → use `discovery.capabilities.comments`; comments/activity
17+
are served by the generic data API on `sys_comment` / `sys_activity`
18+
(`/api/v1/data/sys_comment/…`).
19+
- `@objectstack/metadata-protocol``getDiscovery()` no longer emits the always-`false`
20+
`feed` service/capability. **Bug fix:** the `comments` capability previously keyed off
21+
the deleted `'feed'` service (so it was permanently `false` after #1955); it now tracks
22+
the presence of the `sys_comment` object (provided by the always-on audit slate), so
23+
`declared === enforced`.
24+
- `@objectstack/client` — the internal `feed: '/api/v1/feed'` route constant is removed
25+
(it only existed to satisfy the now-removed `ApiRoutes.feed` type; no client code used it).

CONTRIBUTING.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,6 @@ Use `camelCase`:
181181
```typescript
182182
{
183183
maxLength: 100,
184-
referenceFilters: ['active'],
185184
defaultValue: 'none',
186185
}
187186
```

content/docs/api/data-flow.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ sequenceDiagram
9696
9797
QE-->>K: DataRecord
9898
99-
K->>H: afterFindOne hooks
99+
K->>H: afterFind hooks (fire for findOne too)
100100
H-->>K: Transformed record
101101
102102
K->>Sec: applyFieldSecurity(record, user)
@@ -310,7 +310,7 @@ flowchart TD
310310
end
311311
```
312312

313-
The lifecycle events are defined by the `HookEvent` enum. Reads also have hooks: `beforeFind/afterFind`, `beforeFindOne/afterFindOne`, `beforeCount/afterCount`, and `beforeAggregate/afterAggregate`. Bulk writes add `beforeUpdateMany/afterUpdateMany` and `beforeDeleteMany/afterDeleteMany`.
313+
The lifecycle events are defined by the `HookEvent` enum (8 events). Reads fire `beforeFind`/`afterFind` — for **both** `find` and `findOne`, so one subscription covers every read shape. Bulk writes (`multi: true`) fire the **same** `beforeUpdate`/`beforeDelete`/`afterUpdate`/`afterDelete` events as single-id writes, with the row-scoping predicate in `ctx.input.ast`. There are deliberately no per-method (`findOne`/`count`/`aggregate`) or `*Many` events: read authorization and row filtering are RLS/permission-rule concerns, and field masking is field-level metadata.
314314

315315
| Hook | Phase | Can Modify? | Can Abort? |
316316
|:---|:---|:---|:---|

content/docs/kernel/events.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,10 @@ The Data Engine runs hooks around record reads and mutations. Write operations f
5858
| Event | When |
5959
| :--- | :--- |
6060
| `beforeInsert` / `afterInsert` | Around record creation. |
61-
| `beforeUpdate` / `afterUpdate` | Around record update. |
62-
| `beforeDelete` / `afterDelete` | Around record deletion. |
61+
| `beforeUpdate` / `afterUpdate` | Around record update — single-id **and** bulk (`multi: true`). |
62+
| `beforeDelete` / `afterDelete` | Around record deletion — single-id **and** bulk (`multi: true`). |
6363

64-
Read and bulk variants (`beforeFind`/`afterFind`, `beforeCount`, `beforeAggregate`, `beforeUpdateMany`, `beforeDeleteMany`, …) also exist.
64+
Reads fire `beforeFind` / `afterFind`, for **both** `find` and `findOne` (one read event covers every read shape). There are no per-method (`findOne`/`count`/`aggregate`) or `*Many` events: read authorization and row filtering are handled by RLS/permission rules, field masking by field-level metadata, and bulk writes by the same `before*`/`after*` write events (with the row-scoping predicate in `ctx.input.ast`).
6565

6666
### The HookContext
6767

content/docs/protocol/objectql/schema.mdx

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -594,12 +594,15 @@ const customerHook: Hook = {
594594
export default customerHook;
595595
```
596596

597-
**Event Types** (camelCase):
597+
**Event Types** (camelCase) — 8 events:
598598
- `beforeInsert` / `afterInsert`
599-
- `beforeUpdate` / `afterUpdate`
600-
- `beforeDelete` / `afterDelete`
601-
- Query-side: `beforeFind` / `afterFind`, `beforeCount` / `afterCount`, and the
602-
bulk variants `beforeUpdateMany` / `beforeDeleteMany`, etc.
599+
- `beforeUpdate` / `afterUpdate` — fire for single-id **and** bulk (`multi: true`) updates
600+
- `beforeDelete` / `afterDelete` — fire for single-id **and** bulk (`multi: true`) deletes
601+
- `beforeFind` / `afterFind` — fire for **both** `find` and `findOne`
602+
603+
There are no per-method (`findOne` / `count` / `aggregate`) or `*Many` events: read
604+
authorization and row filtering are RLS/permission-rule concerns, field masking is
605+
field-level metadata, and bulk writes reuse the singular write events.
603606

604607
## Advanced Features
605608

content/docs/references/ai/agent.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,6 @@ const result = AIKnowledge.parse(data);
8383
| **active** | `boolean` || |
8484
| **access** | `string[]` | optional | Who can chat with this agent |
8585
| **permissions** | `string[]` | optional | Required permission-set capabilities |
86-
| **tenantId** | `string` | optional | Tenant/Organization ID |
8786
| **visibility** | `Enum<'global' \| 'organization' \| 'private'>` || [EXPERIMENTAL — NOT ENFORCED, #1901] Intended listing scope. No runtime consumer yet; use access/permissions for real gating. |
8887
| **planning** | `{ maxIterations: integer }` | optional | Autonomous reasoning and planning configuration |
8988
| **memory** | `{ longTerm?: object; reflectionInterval?: integer }` | optional | Agent memory management |

0 commit comments

Comments
 (0)