Skip to content

Commit 68da3cd

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-4610-notification-dual-source
2 parents f8ecd72 + e533b0b commit 68da3cd

30 files changed

Lines changed: 550 additions & 381 deletions
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
ADR-0118(非用户 actor 的平台契约)— docs-only,不发布任何包。执行项 #4556 / #2991 / #4560 / #3166 的实施 PR 各自携带真正的 changeset。
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
---
2+
"@objectstack/spec": major
3+
"@objectstack/example-crm": patch
4+
---
5+
6+
feat(spec)!: retire `datasource.capabilities` — eleven flags nothing read, one of them a safety claim (#4583)
7+
8+
`DatasourceCapabilities` declared eleven booleans — `transactions`, seven `query*`
9+
flags, `joins`, `fullTextSearch`, `readOnly`, `dynamicSchema` — all strict-guarded,
10+
all read by nothing. Pushdown is decided by the runtime driver's own `supports.*`
11+
object, a different mechanism entirely, so a datasource declaring
12+
`queryAggregations: false` never once changed which engine path ran. The block is
13+
removed rather than bridged: there was nothing on the other side to connect it to.
14+
15+
**`readOnly` is why this is not tidy-up.** It reads as a safety property and was
16+
authored as one — the shipped CRM example labelled a datasource "CRM Analytics Read
17+
Replica" on the strength of it, while the datasource accepted writes exactly like the
18+
primary. The key had already been MOVED twice toward somewhere it might be enforced,
19+
out of `config` in #4410 and into `capabilities` in #4465, and was inert at every
20+
address. This removes it instead of moving it a third time.
21+
22+
**Removing it does not hand you a working replacement, and the rejection says so.**
23+
The one enforced datasource-wide write gate is `external.allowWrites: false`, and it
24+
applies only to a FEDERATED datasource — `assertWriteAllowed` returns early for a
25+
`managed` (or unset-`schemaMode`) datasource, so that key would be equally inert for a
26+
local database. **A managed datasource has no read-only gate at all**; that gap is
27+
#4584, deliberately not invented here. Until it is answered, enforce read-only where
28+
it is real: grant the connection SELECT-only at the database.
29+
30+
FROM → TO:
31+
32+
```ts
33+
// before — parsed cleanly, changed nothing
34+
defineDatasource({
35+
name: 'analytics', driver: 'sqlite', config: { filename: ':memory:' },
36+
capabilities: { readOnly: true, queryAggregations: true },
37+
})
38+
39+
// after — delete the block; for a FEDERATED datasource the enforced gate is:
40+
defineDatasource({
41+
name: 'warehouse', driver: 'postgres', config: { … },
42+
schemaMode: 'external',
43+
external: { allowWrites: false },
44+
})
45+
```
46+
47+
`os migrate meta --from 16` rewrites it automatically (ADR-0087 conversion
48+
`datasource-capabilities-removed`). Both `DatasourceSchema` and
49+
`DriverDefinitionSchema` are `.strict()`, so a leftover key is a loud rejection
50+
carrying the prescription — never a silent strip.
51+
52+
Also fixed: `READ_ONLY_BELONGS_ON_DATASOURCE`, the prescription every SQL driver
53+
shares for a `readOnly` written inside `config`, was still sending authors *to* the
54+
removed key. It now names the enforced gate and states plainly where that gate does
55+
not apply — a prescription that lands on an inert key manufactures exactly the belief
56+
it was meant to correct.
57+
58+
The `datasource` liveness ledger drops from 20 dead properties to 9 (remaining:
59+
`healthCheck` ×3, `retryPolicy` ×4, `external` ×2 — batches B/C/D of #4583).
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(docs): the schema-extension FAQ rotted in reverse — `.extend()` works on `FieldSchema` again since protocol 17
6+
7+
The #3890 fix taught that `FieldSchema` / `ObjectSchema` / `ActionSchema` are all
8+
`ZodPipe`s and that `FieldSchema.extend` throws. True when written; protocol 17
9+
(#3855) then retired the deprecated aliases whose lowering was the whole reason
10+
for the field/object transforms, the pipes collapsed to plain `ZodObject`s, and
11+
both prose claims inverted within days: `.extend` works, and the recommended
12+
`FieldSchema.in` is now `undefined` — following the FAQ was once again the only
13+
way to hit an error. Only `ActionSchema` (whose `requiresFeature``visible`
14+
lowering is still live) remains a pipe.
15+
16+
The example gate never noticed because the checked block used only `.parse()`
17+
deliberately shape-agnostic after CI rejected the first #3890 attempt. That made
18+
the code durable and left the PROSE as the only load-bearing surface, which is
19+
where the rot settled.
20+
21+
So the rewrite moves the claim into the checked block: it now calls
22+
`FieldSchema.extend({ … })` directly, so if the schema ever grows a transform
23+
again the gate goes red instead of the prose going quietly wrong. Composition
24+
stays as the shape-agnostic default, `ActionSchema` is documented as the pipe
25+
case with the `.in.extend` caveat, and the FAQ teaches the one-line probe
26+
(`typeof SomeSchema.extend === 'function'`) instead of a table of shapes that
27+
history says will not stay true.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
---
3+
4+
Internal agent tooling only (`.claude/` pm-dispatch backlog-sweep duty + stale-premise checks + process discipline) — releases nothing.

.claude/agents/os-dev.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ build/test runs OOM it.** Binding rules:
4949
4. **Clean up when done**: after the PR is up, remove your worktree
5050
(`git worktree remove <path> --force`) — leftover `node_modules` trees
5151
exhaust the container's disk, which fails as confusingly as OOM.
52+
5. **Never kill by process name.** `pkill -f vitest` (or any name-matched
53+
kill) can take down a parallel agent's run — AGENTS.md's server rule,
54+
applied to every process. Record the PID of what you start and operate
55+
on that PID only (`kill $PID`, liveness via `kill -0 $PID` — a
56+
`pgrep -f` pattern can match your own watcher and never terminate).
5257

5358
Definition of done, in order:
5459

.claude/skills/pm-dispatch/SKILL.md

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,45 @@ shared files, and the merge queue is one lane regardless. Scaling order:
114114
2. When one PM genuinely can't keep up: a second session takes a **whole
115115
repo** as its shard (`/pm-dispatch repo:objectstack-ai/objectui`) —
116116
file universes are disjoint by construction. A sharded PM states its
117-
shard in every claim comment and **never claims outside it**; cross-repo
118-
parent/sub-issue chains stay with the main-backlog PM.
117+
shard in every claim comment and **never claims outside it**.
119118
3. Multiple PMs on the SAME queue: prohibited — all cost, no throughput.
120119

120+
**Shard ownership is registered, never assumed.** A registry issue in the
121+
main backlog (`[PM] 分片分工登记表`) records which session owns which
122+
shard; a PM taking over a shard comments there as its FIRST action, and
123+
comments again when handing off. An unowned shard may be **caretaken** by
124+
the main-backlog PM (triage + dispatch), but the moment a shard is
125+
registered to another session, the caretaker stops dispatching into it —
126+
in-flight claimed tasks finish under whoever claimed them (the claim
127+
protocol makes the handoff collision-free), and everything else belongs to
128+
the new owner. State the mode in claim comments (「cloud 分片,主 PM 代管」
129+
vs a registered shard PM's own tag) so the registry and the claims never
130+
disagree silently.
131+
132+
**Cross-shard transfer protocol — work crosses shard lines, PMs never
133+
do.** When a sharded PM's task (or a sub-task of its parent issue) needs a
134+
change in another shard's repo:
135+
136+
- **Transfer via the target queue**: file the piece as an issue in the
137+
target repo with `pm:queue` and a source line (`Part of
138+
<owner/repo>#<n>`). The target shard's PM picks it up through its own
139+
backlog sweep — the queue label IS the inter-PM channel; PMs never need
140+
to talk directly, and never dispatch into a repo whose in-flight batch
141+
they cannot see (that is the same collision the same-queue ban exists
142+
for).
143+
- **Dependencies via `Blocked-by:`** on the waiting side; the waiting PM's
144+
batch selection skips it until the upstream merges.
145+
- **Follow-up chores belong to the consuming shard**: when the upstream
146+
change lands (say spec gained a key), the dependent-repo adaptation issue
147+
is filed by the PM of the repo that consumes it — it knows its surfaces.
148+
- **Shared contract surfaces have one owner**: anything touching
149+
`packages/spec` transfers to the main-backlog (objectstack) PM
150+
regardless of who needs it — only that PM sees the repo's in-flight
151+
batch and generated-baseline collisions.
152+
- Cross-repo parent/sub-issue chains as a whole stay coordinated by the
153+
main-backlog PM; sharded PMs coordinate only chains fully inside their
154+
shard.
155+
121156
**5. One board, no second tracker.** The pm labels above are the state
122157
machine; an org-level GitHub Project pulling issues/PRs from all three repos
123158
gives the maintainer a single view (filter by `repo:*` and `pm:*`). The PM
@@ -126,13 +161,43 @@ the loop resumable and the board honest.
126161

127162
## The round loop
128163

164+
### 0. Backlog sweep — classification is a standing duty, not a request
165+
166+
The maintainer does not pre-sort the backlog. On every round (and every
167+
idle check-in), sweep issues that carry no `pm:*` / `needs-user-decision`
168+
label and classify each:
169+
170+
- **Auto-queue (`pm:queue`)**: a concrete defect with a named location or
171+
repro; a scoped tooling/gate fix; a restore-invariant finding; a
172+
test-only pin. Nothing to ask — label it and it becomes dispatchable.
173+
- **Maintainer confirm (`needs-user-decision`)**: design cards, feature/
174+
contract-shape proposals, multi-week programs needing appetite and
175+
sequencing, anything touching stored-data migration shape or removing a
176+
shipped capability. The label alone is the inbox entry; the deep two-axis
177+
analysis is written when the card is actually taken up.
178+
- **Repair first**: a body truncated by GitHub's sanitizer (bare `<x>`
179+
swallows the rest at rest) cannot be dispatched — comment the repair
180+
instruction and move on.
181+
129182
### 1. Fetch candidates
130183

131184
List open issues matching the filter, excluding anything assigned or labeled
132185
`needs-user-decision`. **Open sub-issues of a matching parent are candidates
133186
too** — they inherit the parent's queue membership and need no label of their
134-
own. Read each candidate's full body — triage, batch selection (steps 2–3)
135-
and the dispatch prompt all need it.
187+
own. Read each candidate's full body **and its comments** — a comment may
188+
record that half the work already shipped (#4075's step 1 had been merged
189+
for three days; the claim went out without reading the comment that said
190+
so). Triage, batch selection (steps 2–3) and the dispatch prompt all need
191+
the full picture.
192+
193+
**Stale-premise check before every dispatch.** Issues describe the repo as
194+
of their filing date; main moves ~18 merges a day. Before dispatching,
195+
check the named files/subsystem against recent main history (`git log
196+
--oneline -20 -- <paths>`, or search merged PRs referencing the issue's
197+
keywords). Three same-day cases: #4525 (spec key landed 3 days before
198+
filing), #4379 (fix merged via #4459 with the exact proposed sketch),
199+
#4075 (step 1 shipped via objectui#3032). A dispatch that starts with "is
200+
this still true?" costs minutes; one that doesn't costs an agent-run.
136201

137202
### 2. Triage — routing is the PM's job, never the maintainer's
138203

content/docs/deployment/troubleshooting.mdx

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -364,34 +364,44 @@ Yes. All schemas work with plain JavaScript. You lose compile-time type checking
364364

365365
### How do I extend a built-in schema?
366366

367-
**Not with `.extend()` on the schema itself.** Several built-in schemas —
368-
`FieldSchema`, `ObjectSchema`, `ActionSchema` — carry a `.transform()` that lowers
369-
author-facing sugar at parse time, which makes the exported value a **`ZodPipe`,
370-
not a `ZodObject`**. `FieldSchema.extend` is `undefined`, so calling it throws
371-
`is not a function`.
372-
373-
Compose instead: parse with the built-in schema, and validate your additions
374-
alongside it. This keeps the transform running, which `.extend()` would discard.
367+
It depends on whether the schema you are extending carries a parse-time
368+
`.transform()`. A plain schema (`FieldSchema`, `ObjectSchema` since protocol 17
369+
retired their alias-lowering transforms, #3855) is a `ZodObject` and takes
370+
`.extend()` directly. The composition form works in **either** case, and is the
371+
safe default when you don't want to track which is which:
375372

376373
{/* os:check */}
377374
```typescript
378375
import { z } from 'zod';
379376
import { FieldSchema } from '@objectstack/spec/data';
380377

381-
const CustomProps = z.object({ customProperty: z.string().optional() });
378+
// Plain object schema: .extend() works directly. (This line is what pins the
379+
// claim — if FieldSchema ever grows a transform again and becomes a ZodPipe,
380+
// .extend vanishes and this example fails CI instead of the prose going stale.)
381+
const CustomFieldSchema = FieldSchema.extend({
382+
customProperty: z.string().optional(),
383+
});
384+
CustomFieldSchema.parse({ name: 'code', type: 'text', customProperty: 'x' });
382385

386+
// Composition: parse with the built-in schema, validate additions alongside.
387+
// Shape-agnostic — works for plain schemas AND transform-carrying pipes.
388+
const CustomProps = z.object({ customProperty: z.string().optional() });
383389
function parseCustomField(input: unknown) {
384390
return { ...FieldSchema.parse(input), ...CustomProps.parse(input) };
385391
}
386-
387392
parseCustomField({ name: 'code', type: 'text', customProperty: 'x' });
388393
```
389394

390-
If you genuinely need one merged schema object, `FieldSchema.in` is the
391-
`ZodObject` the pipe wraps, so `FieldSchema.in.extend({ … })` builds one — but
392-
the result **skips the transform**, so author-facing sugar the pipe would have
393-
lowered stays raw. Prefer the composition above unless you specifically want the
394-
untransformed shape.
395+
A schema that still lowers author-facing sugar at parse time — `ActionSchema`
396+
(its `requiresFeature``visible` lowering) is one, as of protocol 17 — is a
397+
**`ZodPipe`**, where `.extend` does not exist: prefer the composition form.
398+
Reaching the inner object via `.in.extend({ … })` builds a merged schema but
399+
**skips the transform**, so the sugar the pipe would have lowered stays raw.
400+
401+
A quick check when unsure: `typeof SomeSchema.extend === 'function'` — a pipe
402+
reports `undefined`. This page once asserted the shapes the other way around;
403+
the schemas moved under it within days (#3890#3855), which is why the
404+
load-bearing claim above now lives in a CI-checked block rather than prose.
395405

396406
### Where are the JSON Schemas for IDE autocomplete?
397407

content/docs/getting-started/quick-reference.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ Core business logic and data modeling schemas.
2323
| **[Query](/docs/references/data/query)** | `query.zod.ts` | Query, QueryAST | Query AST with joins, aggregations |
2424
| **[Filter](/docs/references/data/filter)** | `filter.zod.ts` | QueryFilter, FilterCondition | Advanced filtering operators |
2525
| **[Validation](/docs/references/data/validation)** | `validation.zod.ts` | ValidationRule | Business validation rules |
26-
| **[Datasource](/docs/references/data/datasource)** | `datasource.zod.ts` | Datasource, DatasourceCapabilities | Database connection configs |
26+
| **[Datasource](/docs/references/data/datasource)** | `datasource.zod.ts` | Datasource, DriverDefinition | Database connection configs |
2727
| **[Analytics](/docs/references/data/analytics)** | `analytics.zod.ts` | Analytics | Data analytics and aggregation |
2828
| **[Mapping](/docs/references/data/mapping)** | `mapping.zod.ts` | FieldMapping | Field transformation mappings |
2929
| **[Hook](/docs/references/data/hook)** | `hook.zod.ts` | Hook, HookEvent | Lifecycle event hooks |

content/docs/references/data/datasource.mdx

Lines changed: 1 addition & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Can be a built-in driver or a plugin-contributed driver (e.g., "com.vendor.snowf
1616
## TypeScript Usage
1717

1818
```typescript
19-
import { DatasourceSchema, DatasourceCapabilities, DriverDefinitionSchema, DriverType, ExternalDatasourceSettingsSchema } from '@objectstack/spec/data';
19+
import { DatasourceSchema, DriverDefinitionSchema, DriverType, ExternalDatasourceSettingsSchema } from '@objectstack/spec/data';
2020
import type { Datasource, DriverDefinition, ExternalDatasourceSettings } from '@objectstack/spec/data';
2121

2222
// Validate data
@@ -36,7 +36,6 @@ const result = DatasourceSchema.parse(data);
3636
| **driver** | `string` || Underlying driver type |
3737
| **config** | `Record<string, any>` || Driver specific configuration |
3838
| **pool** | `{ min: number; max: number; idleTimeoutMillis: number; connectionTimeoutMillis: number }` | optional | Connection pool settings |
39-
| **capabilities** | `{ transactions: boolean; queryFilters: boolean; queryAggregations: boolean; querySorting: boolean; … }` | optional | Capability overrides |
4039
| **healthCheck** | `{ enabled: boolean; intervalMs: number; timeoutMs: number }` | optional | Datasource health check configuration |
4140
| **ssl** | `{ enabled: boolean; rejectUnauthorized: boolean; ca?: string; cert?: string; … }` | optional | SSL/TLS configuration for secure database connections |
4241
| **retryPolicy** | `{ maxRetries: number; baseDelayMs: number; maxDelayMs: number; backoffMultiplier: number }` | optional | Connection retry policy for transient failures |
@@ -55,27 +54,6 @@ const result = DatasourceSchema.parse(data);
5554
| **_lockDocsUrl** | `string` | optional | Optional documentation link surfaced next to _lockReason. |
5655

5756

58-
---
59-
60-
## DatasourceCapabilities
61-
62-
### Properties
63-
64-
| Property | Type | Required | Description |
65-
| :--- | :--- | :--- | :--- |
66-
| **transactions** | `boolean` || |
67-
| **queryFilters** | `boolean` || |
68-
| **queryAggregations** | `boolean` || |
69-
| **querySorting** | `boolean` || |
70-
| **queryPagination** | `boolean` || |
71-
| **queryWindowFunctions** | `boolean` || |
72-
| **querySubqueries** | `boolean` || |
73-
| **joins** | `boolean` || |
74-
| **fullTextSearch** | `boolean` || |
75-
| **readOnly** | `boolean` || |
76-
| **dynamicSchema** | `boolean` || |
77-
78-
7957
---
8058

8159
## DriverDefinition
@@ -89,7 +67,6 @@ const result = DatasourceSchema.parse(data);
8967
| **description** | `string` | optional | |
9068
| **icon** | `string` | optional | |
9169
| **configSchema** | `Record<string, any>` || JSON Schema for connection configuration |
92-
| **capabilities** | `{ transactions: boolean; queryFilters: boolean; queryAggregations: boolean; querySorting: boolean; … }` | optional | |
9370

9471

9572
---

0 commit comments

Comments
 (0)