Skip to content

Commit f8a8064

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/issue-22160-dev-noise-budget
2 parents 2c50c26 + 7b926f7 commit f8a8064

16 files changed

Lines changed: 780 additions & 139 deletions
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
---
2+
"@objectstack/metadata-protocol": minor
3+
"@objectstack/rest": patch
4+
"@objectstack/spec": patch
5+
---
6+
7+
fix(meta): a second unpinned draft save of a package-owned item is no longer refused 409, the item read serves that draft's `version`, and a package-less draft save keeps a package-owned datasource's stored credential
8+
9+
Clause-②: yes (widening)
10+
11+
- **The second draft save is accepted.** Save an item into a package (`PUT /api/v1/meta/:type/:name?package=<id>`), then save a draft of it without `?package=` twice. The repository stores that draft in the item's package (the draft inheritance that keeps a pending change in the package it edits). The save door took its expected parent from the package-unbound draft row instead. It found none, so the second save, which sent no `If-Match`, was refused `409 METADATA_CONFLICT` ("Expected parent null but current is hmac-sha256:…"). ADR-0008 makes an unpinned save last-writer-wins, and it is accepted again.
12+
- **One head row per write address.** `SysMetadataRepository.headAt(ref, { state, packageId })` answers `{ head, packageId }`: the row a `put` at that address upserts (`null` for a create), and the package binding that put writes under. Both come from the same resolution `put`'s optimistic lock runs. That resolution includes the draft inheritance and the adoption of a draft stored without a package. The save door's expected parent and the item read's `version` both come from it, so the token `GET /meta/:type/:name?state=draft` serves is the token a draft save at the same address accepts. Before this, that read served `version: null` while the inherited draft existed.
13+
- **`?package=all` on the item read.** `GET /meta/:type/:name?package=all` now reads `all` as the save and publish doors read it: as naming no package. Before, the read forwarded the literal `all`. Its `version` was resolved at a package no save writes, so it served `null` beside a row the save then judged. Its `?state=draft` read answered `404 NO_DRAFT` for a draft stored in the item's package. The read now serves the item as a read with no `?package=` serves it, and `?package=all` still takes the uncached arm, so it keeps serving `version`.
14+
- **The credential carry-forward reads the same row.** A save of a type whose read withholds a credential (`datasource`; `flow` with the automation plugin) carries the stored credential back into a body that returns as it was served. It compared that body against the row at the key the caller named. A package-less draft save of a package-owned item therefore found neither the inherited draft nor the package's active row, and compared against the code layer. The stored credential was persisted away on the first such save, measured on the real route with a datasource whose `config.url` holds one. Both reads now go through `headAt`: the draft the save overwrites, or with no draft yet the active row in the package the draft is stamped into. An item with no package binding is unchanged.
15+
- **Descriptions only, in `@objectstack/spec`.** `SaveMetaItemRequestSchema.packageId` now states the draft inheritance, where it said "absent = env-local overlay". The `POST /meta/:type/:name/publish` route description spells the conflict code `METADATA_CONFLICT`, as the wire sends it.
16+
- A stale `If-Match` is still refused `409`, and `If-None-Match: *` is still refused while a row exists. An item with no package binding is unchanged. No spec key, request field or response field is added or removed.
17+
- The only change to the public surface is one added method on the exported `SysMetadataRepository`: `headAt`. No existing signature, schema or accept set changes.

‎content/docs/getting-started/build-with-claude-code.mdx‎

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@ The thing you end up with isn't just an app — it's an app that AI can *operate
1414
because the same metadata generates an MCP server.
1515

1616
<Callout type="info">
17-
Want to understand the pieces the agent writes before you review them?
18-
[Anatomy of an ObjectStack App](/docs/getting-started/quick-start) is a tour of
19-
the metadata — objects, actions, views — and how to read it. This page is the
20-
workflow; that one is the map.
17+
**The agent path** from [Start here](/docs/getting-started): an agent writes the
18+
app, and you verify it. Want to write it by hand and understand every file
19+
instead → [Your First Project](/docs/getting-started/your-first-project). To
20+
understand the pieces the agent writes before you review them, keep
21+
[Anatomy of an ObjectStack App](/docs/getting-started/quick-start) beside this
22+
page: this page is the workflow; that one is the map.
2123
</Callout>
2224

2325
## The build loop
@@ -155,8 +157,10 @@ export const ResolveTicketAction = defineAction({
155157
label: 'Resolve',
156158
objectName: 'support_desk_ticket',
157159
icon: 'check-circle',
158-
type: 'script',
159-
target: 'resolveTicket',
160+
// What the click does: write `status: 'resolved'` to this ticket. No handler
161+
// to write or register; the platform performs the update.
162+
operation: 'update',
163+
patch: { status: 'resolved' },
160164
locations: ['record_header', 'list_item'],
161165
// Only offer "Resolve" on tickets that aren't already resolved or closed.
162166
// Predicates are CEL, record-scoped — `record.status`, never bare `status`.
@@ -223,6 +227,17 @@ export const SupportApp = defineApp({
223227
});
224228
```
225229

230+
The **Resolve** action is a *declarative field write*: `operation: 'update'` with
231+
`patch: { status: 'resolved' }` sets the ticket's status when it is clicked.
232+
There is no handler to write or register, and the update runs as the signed-in
233+
user, so their permissions and the object's validations apply exactly as they
234+
would to an edit. An action that runs code of its own names a handler in
235+
`target` instead, and then something must register that handler: `os validate`
236+
passes an unregistered one, the `os dev` boot log warns
237+
`[action-governance] declared script actions with NO handler`, and the button
238+
fails when clicked (the
239+
[two paths](/docs/ui/actions#give-it-server-behavior--two-paths) a handler takes).
240+
226241
The agent also **exports each new file from its directory's barrel** — the object
227242
from `src/objects/index.ts`, and the action, view and app from
228243
`src/actions/index.ts`, `src/views/index.ts` and `src/apps/index.ts`. The
@@ -320,9 +335,12 @@ as the dev admin the server seeds on an empty database
320335
(`admin@objectos.ai` / `admin123`). Then drive the app like a user:
321336

322337
- Create a ticket. Confirm the fields, labels, and picklist colors match intent.
323-
- Check the **Resolve** action shows on an open ticket — and **disappears** once
324-
the ticket is resolved (that's the `record.status` predicate doing its job).
325338
- Open the **Support → Open** nav item; confirm the filter shows only open tickets.
339+
- Open your ticket and click **Resolve** in its header. You should see the
340+
*Ticket resolved.* toast, the status change to **Resolved**, and the
341+
**Resolve** button disappear — the `patch` wrote the status, and the
342+
`record.status` predicate now hides the action. Back in **Support → Open**,
343+
the ticket is no longer listed.
326344

327345
{/* screenshot placeholder: /_console/ ticket list with the Support nav group */}
328346
{/* screenshot placeholder: ticket record with the Resolve action in the header */}

‎content/docs/getting-started/how-ai-development-works.mdx‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,15 @@ description: "How building with ObjectStack splits the work: AI writes the typed
77
ObjectStack is built for a specific way of working: **an AI agent writes the
88
application; a human verifies it in a visual UI; and a stack of guardrails keeps
99
the AI from shipping mistakes.** This page explains why that division of labor is
10-
both *fast* and *safe*. For the hands-on walkthrough, see
11-
[Build with Claude Code](/docs/getting-started/build-with-claude-code).
10+
both *fast* and *safe*.
11+
12+
<Callout type="info">
13+
**The why, not a starting point.** To build, take a path from
14+
[Start here](/docs/getting-started): want an agent to write the app →
15+
[Build with Claude Code](/docs/getting-started/build-with-claude-code); want to
16+
write it by hand and understand every file →
17+
[Your First Project](/docs/getting-started/your-first-project).
18+
</Callout>
1219

1320
## The division of labor
1421

‎content/docs/getting-started/index.mdx‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,13 @@ description: ObjectStack is the AI-native business backend for structured, audit
66

77
import { Database, Layout, Cog } from 'lucide-react';
88

9+
<Callout type="info">
10+
**Start here:** want an agent to write the app →
11+
[Build with Claude Code](/docs/getting-started/build-with-claude-code); want to
12+
write it by hand and understand every file →
13+
[Your First Project](/docs/getting-started/your-first-project).
14+
</Callout>
15+
916
**ObjectStack** is not just a framework; it is an **AI-native business backend protocol** for building enterprise software. It decouples the *Business Intent* (defined as typed metadata) from the *Technical Execution* (handled by the Kernel), so APIs, UI metadata, workflows, and agent tools can all derive from the same source of truth.
1017

1118
## How you'll build

‎content/docs/getting-started/quick-start.mdx‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ page is a tour of the pieces so you can review an agent's work with confidence
1616
and catch the things the [validation gate](/docs/deployment/validating-metadata)
1717
can't.
1818

19-
<Callout type="tip">
20-
Want to *build* something, not just read about the pieces? Follow
21-
[Build with Claude Code](/docs/getting-started/build-with-claude-code) — it runs
22-
the whole loop (describe → agent authors → validate → verify) on a live app. New
23-
here? The [Example Apps](/docs/getting-started/examples) are working code you can
24-
run today.
19+
<Callout type="info">
20+
**The map, not a starting point.** This page is for reading metadata, whether an
21+
agent wrote it or you are about to. To build, take a path from
22+
[Start here](/docs/getting-started): want an agent to write the app →
23+
[Build with Claude Code](/docs/getting-started/build-with-claude-code); want to
24+
write it by hand and understand every file →
25+
[Your First Project](/docs/getting-started/your-first-project). Working code to
26+
run today: the [Example Apps](/docs/getting-started/examples).
2527
</Callout>
2628

2729
## What you build
@@ -112,7 +114,12 @@ reviewing an agent's work, these are the two things most worth a close read:
112114
`has(record.status) && record.status != 'sent'` — because a list row carries
113115
only the columns that view projects, and reading an absent one faults and
114116
hides the button. See [Actions](/docs/ui/actions) for when the guard needs
115-
`&& record.x != null` on top.
117+
`&& record.x != null` on top. Then check what a click **does**: a declarative
118+
`operation: 'update'` with a `patch` (the tutorial's Resolve action), an
119+
inline `body`, or a `target` naming a handler that something registers. A
120+
`script` action whose `target` nothing registers passes `os validate` and
121+
fails only when clicked; the `os dev` boot log names it under
122+
`[action-governance] declared script actions with NO handler`.
116123
- **Views & Apps** (`defineView`, `App.create`) — the list/form lenses and the
117124
navigation. Reading these tells you what the user will actually see and click.
118125

@@ -240,9 +247,9 @@ npx os dev --ui # boot the app, then open http://localhost:3000/_console/
240247
```
241248

242249
`os validate` proves the metadata is *well-formed*; the **Console** at `/_console/`
243-
proves it does what you *meant* — create a record, watch an action show/hide,
244-
confirm the nav and filters. That visual check is the human's half of every build
245-
loop. See [Validating Metadata](/docs/deployment/validating-metadata) for what
250+
proves it does what you *meant* — create a record, click an action and check
251+
what it changed, watch an action show/hide, confirm the nav and filters. That
252+
visual check is the human's half of every build loop. See [Validating Metadata](/docs/deployment/validating-metadata) for what
246253
the gate catches, and [Build with Claude Code](/docs/getting-started/build-with-claude-code)
247254
for the full describe → author → validate → verify cycle.
248255

‎content/docs/getting-started/your-first-project.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ minutes you will scaffold a project, run it, add a field by hand, call the
1010
generated REST API, and compile the artifact you would deploy.
1111

1212
<Callout type="info">
13-
**Two ways to build.** ObjectStack is designed AI-first: normally
14-
[an agent authors the metadata and you verify it](/docs/getting-started/build-with-claude-code).
15-
This page walks the same ground *manually* so you understand every file the
16-
agent would touch — useful for evaluating the platform, wiring CI, or working
17-
without an agent. The two paths produce identical projects.
13+
**The by-hand path** from [Start here](/docs/getting-started): you write every
14+
file yourself, so you understand each one the agent would touch — useful for
15+
evaluating the platform, wiring CI, or working without an agent. Want an agent
16+
to write the app instead → [Build with Claude Code](/docs/getting-started/build-with-claude-code).
17+
The two paths produce identical projects.
1818
</Callout>
1919

2020
## Prerequisites

‎content/docs/references/api/protocol.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2601,7 +2601,7 @@ Installed package with runtime lifecycle state
26012601
| **actor** | `string` | optional | Identity recorded on the write's history event (`recorded_by`, a lookup into `sys_user`) and audit row. On the REST door this is the request's authenticated identity (one producer) — never a caller-supplied header. Absent, the event is recorded actor-less (null), deliberately not attributed to "system". |
26022602
| **force** | `boolean` | optional | Destructive-change acknowledgement (`?force=true` on the REST door): skips the safety diff that refuses an `object` save whose body drops fields or narrows types the stored item still carries (409 with the findings otherwise). Only `object` saves reach that diff, so the flag is inert for every other type. Absent = the guard runs. |
26032603
| **mode** | `Enum<'draft' \| 'publish'>` | optional | Per-item lifecycle (ADR-0005 drafts): `draft` stages the body as a pending draft overlay (`?mode=draft` on the REST door; the publish door promotes it later); `publish` or ABSENT writes straight to the live `active` row — the legacy default, kept so callers that predate the draft/publish split keep working. Any value other than `draft` is read as `publish`. |
2604-
| **packageId** | `string \| null` | optional | ADR-0048 — the software package to bind the saved row to (`sys_metadata.package_id`; `?package=<id>` on the REST door, sent only when it names a real package). Set when authoring inside a Studio package workspace; a named read-only base package is refused. On create the row is stamped with this id; on update an existing binding is preserved, never silently re-bound. Absent = env-local overlay (no package stamp); it also scopes which row the unpinned parent-version resolution reads. |
2604+
| **packageId** | `string \| null` | optional | ADR-0048 — the software package to bind the saved row to (`sys_metadata.package_id`; `?package=<id>` on the REST door, where `all` and an empty value name no package). Set when authoring inside a Studio package workspace; a named read-only base package is refused. On create the row is stamped with this id; on update an existing binding is preserved, never silently re-bound. Absent or `null` names no package: an `active` save writes the env-local overlay (no package stamp), and a `draft` save writes the draft of the package the item's active row is bound to when it is package-bound (a pending change stays in the package it edits), else the env-local draft. That row is also the one the parent-version resolution reads: an unpinned save takes its version as the parent (last-write-wins), a pinned one is judged against it, and the item read at the same scope serves it as `version`. |
26052605
| **writeFace** | `Enum<'package-duplicate' \| 'meta-envelope' \| 'meta-dispatch' \| 'external-import'>` | optional | Which write door a refusal is being rendered FOR — stated by the SERVER, never by a remote caller: every door builds this request field by field and never spreads a request body into it, so there is no path for a client to smuggle a face in, and a face arriving in a wire body is simply never read. Two refusals branch on it, for different questions: the 409 destructive-change remedy names the acknowledgement mechanism that actually exists on the refusing door (`?force=true` on the REST doors; the dispatcher, the duplicate-package door and the external-table import have none), and the 422 invalid-metadata message adapts to whether a structured `issues[]` channel reaches the consumer beside it. Absent = the conservative default wording. |
26062606

26072607

‎docs/NORTH-STAR.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@
114114
1. 安全与数据完整性永远最高,不等路。
115115
2. 路断了、或清单上的能力断了 ⇒ P0/P1;能跑但出错 ⇒ P2;不在路上、不在清单上 ⇒ p3 或不做。定级读「路上的功能点」:改那张表即改优先级,⛔ 不逐卡改档。
116116
3. 本车道队列还有开放的 P0/P1 时,该车道不派 p2/p3 的工具卡、契约卫生卡;解锁产品 P0/P1 的仪器卡沿链继承其优先级。
117-
4. 因为写的是 AI、用的也是 AI:本体既要 AI 能写——错的必须被响亮拒绝并给处方,永不静默落库;也要人与 Agent 能用——声明了的对象、动作、agent / tool / skill 元数据在运行时兑现,MCP 面暴露的就是应用真能做的,Agent 越权时被权限挡住并留痕。写给 AI 的文档与 skills 说错一句,等于产品缺陷。
117+
4. 因为写的是 AI、用的也是 AI:本体既要 AI 能写——错的必须被响亮拒绝并给处方,永不静默落库;也要人与 Agent 能用——声明了的对象、动作、agent / tool / skill 元数据在运行时兑现,MCP 面暴露的就是应用真能做的,Agent 越权时被权限挡住并留痕。人用的那一半也有标准:作者不写一行界面代码,得到的界面要像设计师经手过——同一套设计系统、同一种版式;看着不对与渲染不出同样是缺陷,在「看到」那一步就被拒。写给 AI 的文档与 skills 说错一句,等于产品缺陷。
118118

119119
**阶段姿态**:速度优先于兼容。用的人少:退役立即生效,无过渡窗口、无别名双拼;兼容层、迁移窗口一类的工作默认 p3;声明了但不兑现的键按发布批量退役,不一键一卡。
120120

0 commit comments

Comments
 (0)