From 13a6486849ccfef884752a26d2ca284b644eee2e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:13:08 +0000 Subject: [PATCH 1/5] =?UTF-8?q?content(blog):=20views,=20dashboards=20and?= =?UTF-8?q?=20apps=20are=20projections=20of=20the=20ontology=20=E2=80=94?= =?UTF-8?q?=20the=20naming=20test,=20measured=20on=20app-todo=20(slot=2020?= =?UTF-8?q?26-11-10)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit One new post directory: en and zh-Hans authored, zh-Hant generated, two SVG pairs. The proof artifact is a one-field diff on examples/app-todo at objectstack 43567a8f, run on the published 18.0.0-next.2 packages and read before and after on the table, REST, metadata, MCP, Console, views, translation and dataset surfaces. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017GeNDQH7E5S2rX8ajVAPxv --- .../cover-en.svg | 66 +++++++++ .../cover.svg | 66 +++++++++ .../index.mdx | 124 +++++++++++++++++ .../index.zh-Hans.mdx | 124 +++++++++++++++++ .../index.zh-Hant.mdx | 125 ++++++++++++++++++ .../naming-test-en.svg | 124 +++++++++++++++++ .../naming-test.svg | 124 +++++++++++++++++ 7 files changed, 753 insertions(+) create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg new file mode 100644 index 00000000..b05352d4 --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + WHAT CHANGES FOR REVIEW + One field. + Every surface. + Add a field to the definition and the table, the API, + the MCP tools and the Console follow by themselves. + The views that name their columns do not. + + + + + budget: Field.currency({ label: 'Budget' }) + + + + + The naming test: names the field, or only the object? + + + + + FOLLOWED, NO EDIT · DERIVED + SQLite table todo_task + 27 → 28 columns + GET /api/v1/data/todo_task + 27 → 28 keys + POST with budget: 1200 + 400 → 201 + MCP describe_object + 19 → 20 fields + Console create form + 16 → 17 inputs + + + + + UNCHANGED · PINNED TO FIELD NAMES + All Tasks list view + 6 columns, as authored + zh-CN translation + budget → "Budget" + Dashboard dataset task_metrics + 3 measures, by name + + + Measured on ObjectStack 18.0.0-next.2, examples/app-todo at commit 43567a8f: one five-line diff, eleven readings before and after. + diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg new file mode 100644 index 00000000..1a2dc129 --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + 这对审阅意味着什么 + 改一个字段, + 动到每个面。 + 给定义加一个字段,数据表、API、MCP 工具 + 和控制台自己跟上。 + 写明了列名的视图不会。 + + + + + budget: Field.currency({ label: 'Budget' }) + + + + + 命名测试:写明的是字段,还是只有对象? + + + + + 未经编辑就跟上 · 推导 + SQLite 表 todo_task + 27 → 28 列 + GET /api/v1/data/todo_task + 27 → 28 个键 + POST 带 budget: 1200 + 400 → 201 + MCP describe_object + 19 → 20 个字段 + 控制台新建表单 + 16 → 17 个输入项 + + + + + 没有变化 · 钉在字段名上 + All Tasks 列表视图 + 6 列,原样 + zh-CN 翻译 + budget → "Budget" + 仪表盘数据集 task_metrics + 3 个度量,按名字 + + + 在 ObjectStack 18.0.0-next.2 上测量,examples/app-todo 取自提交 43567a8f:一份五行 diff,前后各十一个读数。 + diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx new file mode 100644 index 00000000..638e7ac6 --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx @@ -0,0 +1,124 @@ +--- +title: "Views, Dashboards and Apps Are Projections of the Ontology: What That Changes for Review" +description: "Add one field to a bundled ObjectStack app: the table, REST API, MCP describe call and Console form follow by themselves; the pinned views, dataset and translation do not. The naming test says which is which." +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontology + - Executable business ontology + - Reviewable diff + - Metadata review +--- + +This is the whole diff. Five lines, one field, added to the `todo_task` object of the Todo example that ships in the ObjectStack repository: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +Booted on the ObjectStack runtime, those five lines changed the database table, the REST read and write, the metadata endpoint, the MCP `describe_object` and `query_records` answers and the Console's create form — seven surfaces, none of which was edited. They did not change the three list views, the dashboard's dataset or the Chinese translation, because each of those names fields by name. The first group is derived from the definition; the second is pinned to it. **The naming test** sorts any projection into one of the two before you open it: *if it names the field, you review it; if it names only the object, the runtime already did.* The rest of this post is the measurement behind that sentence, taken on ObjectStack `18.0.0-next.2` with the example's source at commit `43567a8f`, and what it changes about reading a diff. + +## Where the definition of a projection lives + +Three places already define what a projection is, and this post does not restate them. [The definition post](/en/blog/what-does-the-ontology-is-the-software-mean/) names the four projections and gives the removal test for telling core from projection. [The ontology page](/en/ontology/) states the rule in one line: change the core and the projections follow; remove a projection and the business it showed is unchanged. The [glossary entry](/en/glossary/ontology-projection/) adds the consequence for a reviewer: a diff that touches only projections changes presentation, and a diff that touches the core changes authority. + +What none of them says is *how* a projection follows. "Follow" covers two different mechanisms, and a reviewer needs to know which one is in front of them. That is the question the run below answers. + +## The run: one field, eleven readings + +The setup is the bundled Todo example (`examples/app-todo`), copied out of the repository at commit `43567a8f` and run on the published `18.0.0-next.2` packages with `os dev`. The database is the default SQLite file; the API key was minted over REST from the seeded dev admin. Every reading was taken twice: once on the committed definition, once after the five-line diff above and a restart with `--compile`. "Before" and "after" are the two answers. + +| Surface | How it was read | Before | After | Follows? | +| :--- | :--- | :--- | :--- | :--- | +| Database table `todo_task` | `PRAGMA table_info` on the SQLite file | 27 columns | 28; `budget float` added to the existing table | Derived | +| REST read | `GET /api/v1/data/todo_task` | 27 keys per record | 28 keys | Derived | +| REST write | `POST /api/v1/data/todo_task` with `budget: 1200` | `400` — `Unknown field 'budget' on object 'todo_task'`, code `INVALID_FIELD` | `201`, record created | Derived | +| Metadata endpoint | `GET /api/v1/meta/object/todo_task` | 27 fields | 28; `{ label: 'Budget', type: 'currency', min: 0, … }` | Derived | +| MCP `describe_object` | `tools/call` on `/api/v1/mcp` | 19 fields | 20 | Derived | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | Derived | +| Console create form | `/_console/apps/todo_app/todo_task/new` | 16 inputs | 17; *Budget* between *Actual Hours* and *Notes*, in declaration order | Derived | +| Console record detail | the new record's page | — | `BUDGET 1,200.00` in *Details* | Derived | +| Console list view *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 columns | 6 columns, unchanged | Pinned | +| Translation, `zh-CN` | `GET /api/v1/meta/object/todo_task` with `Accept-Language: zh-CN` | `subject` → 主题 | `budget` → `Budget` (English falls through) | Pinned | +| Dashboard dataset `task_metrics` | the dataset definition | 6 dimensions, 3 measures, each naming a field | unchanged; nothing can chart `budget` until a measure names it | Pinned | + +Two readings did not move and are not projections: the OpenAPI document still lists 14 paths for `todo_task` with no per-field schema, and the MCP `tools/list` still answers the same 12 tools (`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`). Both describe objects, not fields; the field shows up inside their answers, not in their shape. + +![The naming test: one five-line diff in the middle; on the left the surfaces that name only the object and followed it without an edit, with their before and after readings; on the right the projections that name the field and stayed exactly as authored; at the foot, the build gate's own warning that the new field has no consumer](./naming-test-en.svg) + +One more reading came for free. `os dev --compile` runs the author-time rules before it writes the artifact, and this is what the gate printed about the new field: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget +``` + +`os explain field-no-consumers` defines a consumer as "a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it", and separates it from a carrier, which "names a field without reading it", such as "a translation label, a seed value, an import-mapping target, a permission grant". The gate's list is the right-hand column of the table. It is the runtime telling you, before boot, which projections are pinned to a field and that none of them has picked this one up yet. + +## The naming test + +Why do seven surfaces follow and four stay put? Not because some are more important. Because of what each one names. + +The table, the REST routes, the metadata endpoint and the MCP tools are generated from the object. The SQL driver's schema sync walks `obj.fields` and creates a column for every entry; the data routes are registered per object and read whatever fields the object has; `describe_object` maps `Object.entries(def.fields)` into its answer; the Console, finding no form view declared for `todo_task`, lays out the object's own fields. None of these holds a list of field names. They hold the object's name and ask the definition for the rest on every call, which is why the field reached all of them with no edit and why it reached them in declaration order. + +The list views, the dataset and the translation bundle are authored metadata too, and each one spells out field names: `columns: [{ field: 'subject' }, { field: 'status' }, …]`, `measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`, `fields: { subject: { label: '主题' }, … }`. A projection that names the field is making a choice about it — this column, this label, this aggregate — and the runtime honours the choice exactly. It does not add what you did not ask for. + +So the test, stated for a reviewer: + +> **The naming test.** For each projection the diff could reach, ask whether it names the field or only the object. If it names only the object, it is derived: the diff you are reading is the whole change, and the runtime has already propagated it. If it names the field, it is pinned: it did not move, and whether it should have is your call. + +It composes with the removal test from the definition post. The removal test sorts a definition into core or projection. The naming test sorts the projections a core change can reach into derived and pinned. Between them a reviewer knows, from the diff alone, what the runtime did and what is still waiting for a decision. + +## What this changes about reading the diff + +The practical consequence is an order of work, and it is shorter than the one most teams follow. + +**First, the definition line is the review for every derived surface.** There is no table migration to read alongside it, no serializer, no controller, no tool manifest. In the run above, the `budget` column exists because the field does; the REST write accepts `budget` because the field does; the agent's `describe_object` shows it because the field does. If the field is right, those are right, and if the field is wrong, they are wrong in exactly the way the field is. Reviewing the five lines reviewed seven surfaces, which is the sentence the title promised and the sentence the glossary entry means by "a diff that touches the core changes authority". + +**Second, search the pinned projections for the field's name — and expect to find nothing.** An added field will not appear in any view, dataset or translation, because none of them has been edited. That is not an omission the runtime hides; it is the warning the gate printed. The reviewer's question is the one the gate's `fix:` line poses: should this field have a consumer, and which one? For `budget`, probably a column on *All Tasks* and a `zh-CN` label; probably not a dashboard measure yet. Those are three small, deliberate edits to three named files, and each is as reviewable as the first five lines. + +**Third, treat a removal as a different kind of change.** Everything above is about an added field. The driver's schema sync is additive only: it creates missing tables and adds missing columns, and it never alters or drops existing ones, so a removed or renamed field leaves its column behind as drift that `os migrate` reports and a human reconciles. The derived surfaces follow a removal instantly — the REST record loses the key, `describe_object` loses the field — while the table does not. And every pinned projection that still names the removed field is now wrong, which the same gate will tell you about as a dangling binding. The naming test still applies; the list of things to look at is simply longer, and it starts with the migration. + +This ordering is also the missing half of the [four-pass review checklist](/en/blog/how-to-review-ai-generated-app-changes/), whose fourth pass is "fields and views, last". The naming test says which views: the ones that name the field. The rest were reviewed when the field was. + +## In defence of pinning + +It would be easy to read the right-hand column of the table as a limitation, and the honest reading is the opposite. + +A grid that added every new column automatically would reorder a screen every time a developer added a field for a flow to write into. A dashboard that charted every numeric field would chart `recurrence_interval`. A translation that invented labels would be a translation nobody wrote. Pinned projections are pinned because a human made a presentation decision, and a runtime that silently overrode it would be the thing this whole format exists to prevent: behaviour that nobody declared. The Console's own fallback — lay out every field when no form view is declared — is the exception that proves it: it applies only where nobody has decided anything yet. + +What the format gives the reviewer is not that pinned projections follow, but that they are *findable*. They are metadata in the same repository, they name the field, and the gate enumerates them. The alternative, in a hand-written codebase, is a column list in a React component, a serializer in another directory, a label in a JSON file and a chart config in a third system, with nothing that knows they are about the same field. + +## What this does not cover + +A measurement on one example is a measurement on one example. Four things stay outside it. + +**Hand-tuned views stay hand-tuned.** Every list view in the Todo example declares its columns, and so does every view in the CRM example. A new field reaches a pinned grid only when someone adds it, and that someone is often the agent that added the field, in the same PR. The naming test tells you to look there; it does not do the looking. + +**Translations must be written.** The runtime falls back to the declared label, so a missing `zh-CN` entry renders `Budget` rather than nothing. That is the right default and still a gap a Chinese user sees. A translation is a pinned projection in the strictest sense: every string in it is a decision. + +**External consumers are outside the stack.** The gate says so itself: "a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view". A dashboard in another tool that selects `estimated_hours` by name is pinned to that field and nothing in the repository knows it. The naming test applies to it; the enumeration does not. + +**Semantic changes are not structural changes.** Adding a field is the easy case, which is why it was chosen for a clean before-and-after. Change the *meaning* of a field instead — add a `cancelled` option to `status`, say — and the derived surfaces follow just as faithfully, but the Todo example's *Overdue Tasks* view and its *Overdue Tasks* metric both encode overdue as `status != completed`, so a cancelled task with a past due date would count as overdue on both. That is a reading of the view and dataset definitions, not a measurement, and it is the kind of consequence the naming test points you at — both projections name `status` — without deciding for you. + +## Point your agent at it + +The run above is reproducible from the repository and takes about a minute once the packages are installed, which means it is also something your agent can do for you. Point a coding agent at an ObjectStack project — the scaffolder writes an `AGENTS.md` and installs the skills bundle, the running app serves MCP at `/api/v1/mcp`, and [how to configure it](/en/blog/how-to-point-your-agent-at-objectstack/) covers the rule file, the skills and the tool server — and when it opens a change to a field, ask it for the naming-test table of its own diff: which surfaces followed, which projections name the field, and what it proposes to do about each. The gate's `field-no-consumers` line is the first row of that table, and the agent can read it before you do. + +Everything in this post ran on the open-source stack. "Want the same loop hosted, in the browser, nothing to install? That's ObjectOS, the commercial runtime environment built on ObjectStack." ObjectOS runs hosted in the browser with nothing to install (ObjectOS Cloud) or self-managed on your own infrastructure (ObjectOS Enterprise), and on either edition you can export your ontology and run it on the open-source ObjectStack runtime. The naming test does not change with the host; the definition it reads is the same file. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx new file mode 100644 index 00000000..20f345b7 --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx @@ -0,0 +1,124 @@ +--- +title: 视图、仪表盘和应用是本体的投影:这对审阅意味着什么 +description: 给 ObjectStack 自带示例加一个字段并逐面实测:数据表、REST、MCP 与控制台表单自己跟上,钉死字段名的视图、数据集和翻译没动。命名测试告诉审阅者哪个是哪个。 +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover.svg +tags: + - 本体 + - 可执行业务本体 + - 可审阅的 diff + - 元数据审阅 +--- + +下面就是整份 diff。五行,一个字段,加在 ObjectStack 仓库自带的 Todo 示例的 `todo_task` 对象上: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +在 ObjectStack 运行时上启动之后,这五行改动了数据库表、REST 的读与写、元数据接口、MCP 的 `describe_object` 与 `query_records` 应答,以及控制台的新建表单——七个面,没有一个被编辑过。它们没有改动三个列表视图、仪表盘的数据集和中文翻译,因为这三样东西都逐个写明了字段名。前一组是从定义推导出来的,后一组是钉在定义上的。**命名测试**在你打开任何一个投影之前就能把它归到两组之一:它写明了这个字段,你就审它;它只写明了对象,运行时已经替你审过了。本文余下的部分,是这句话背后的测量——在 ObjectStack `18.0.0-next.2` 上、示例源码取自提交 `43567a8f`——以及它对读一份 diff 这件事改变了什么。 + +## 投影的定义住在哪里 + +投影是什么,已经有三处定义,本文不重复。[定义那篇](/zh-Hans/blog/what-does-the-ontology-is-the-software-mean/)点名了四种投影,并给出区分核心与投影的删除测试。本体页(`/zh-Hans/ontology/`,「投影」一节)用一句话说明规则:改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。[术语表条目](/zh-Hans/glossary/ontology-projection/)补上了对审阅者的推论:只碰投影的 diff 改的是呈现,碰到核心的 diff 改的是权限与事实。 + +它们都没说的是,投影是*怎么*跟着变的。「跟着变」底下藏着两种不同的机制,审阅者得知道眼前的是哪一种。下面这次运行回答的就是这个问题。 + +## 这次运行:一个字段,十一个读数 + +对象是自带的 Todo 示例(`examples/app-todo`),从仓库提交 `43567a8f` 复制出来,用已发布的 `18.0.0-next.2` 包以 `os dev` 启动。数据库是默认的 SQLite 文件;API key 用种子化的开发管理员通过 REST 签发。每个读数取两次:一次在提交原样的定义上,一次在上面那五行 diff 之后、带 `--compile` 重启之后。「之前」和「之后」就是这两次的答案。 + +| 面 | 怎么读的 | 之前 | 之后 | 跟着变? | +| :--- | :--- | :--- | :--- | :--- | +| 数据库表 `todo_task` | 对 SQLite 文件执行 `PRAGMA table_info` | 27 列 | 28 列;`budget float` 加到了已有的表上 | 推导 | +| REST 读 | `GET /api/v1/data/todo_task` | 每条记录 27 个键 | 28 个键 | 推导 | +| REST 写 | `POST /api/v1/data/todo_task`,带 `budget: 1200` | `400`——`Unknown field 'budget' on object 'todo_task'`,错误码 `INVALID_FIELD` | `201`,记录已创建 | 推导 | +| 元数据接口 | `GET /api/v1/meta/object/todo_task` | 27 个字段 | 28 个;`{ label: 'Budget', type: 'currency', min: 0, … }` | 推导 | +| MCP `describe_object` | 对 `/api/v1/mcp` 发 `tools/call` | 19 个字段 | 20 个 | 推导 | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | 推导 | +| 控制台新建表单 | `/_console/apps/todo_app/todo_task/new` | 16 个输入项 | 17 个;*Budget* 按声明顺序出现在 *Actual Hours* 和 *Notes* 之间 | 推导 | +| 控制台记录详情 | 新记录的页面 | — | *Details* 区显示 `BUDGET 1,200.00` | 推导 | +| 控制台列表视图 *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 列 | 6 列,没有变化 | 钉死 | +| 翻译,`zh-CN` | 带 `Accept-Language: zh-CN` 的 `GET /api/v1/meta/object/todo_task` | `subject` → 主题 | `budget` → `Budget`(回落到英文) | 钉死 | +| 仪表盘数据集 `task_metrics` | 数据集定义本身 | 6 个维度、3 个度量,每个都点名一个字段 | 没有变化;在某个度量点名 `budget` 之前,没有任何图表能画它 | 钉死 | + +另有两个读数没有变化,而它们也不是投影:OpenAPI 文档仍然为 `todo_task` 列出 14 条路径,没有逐字段的 schema;MCP 的 `tools/list` 仍然应答同样的 12 个工具(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)。两者描述的都是对象,不是字段;字段出现在它们的应答里,而不是它们的形状里。 + +![命名测试:中间是那份五行 diff;左边是只写明对象、未经编辑就跟上了定义的面,附前后读数;右边是写明了字段名、原样未动的投影;底部是构建门禁自己打出的警告——新字段没有任何消费者](./naming-test.svg) + +还有一个读数是白送的。`os dev --compile` 在写出工件之前会跑一遍编写期规则,下面是门禁对这个新字段打印的原话: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget +``` + +`os explain field-no-consumers` 把「消费者」定义为「a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it」,并把它和「载体」区分开——载体「names a field without reading it」,例如「a translation label, a seed value, an import-mapping target, a permission grant」。门禁的这份清单,正是上表的右半栏。这是运行时在启动之前就告诉你:哪些投影是钉在字段上的,而其中还没有任何一个接住这个新字段。 + +## 命名测试 + +为什么七个面跟着变、四个面原地不动?不是因为有的更重要。是因为它们各自写明了什么。 + +数据表、REST 路由、元数据接口和 MCP 工具都是从对象生成的。SQL 驱动的结构同步遍历 `obj.fields`,为每一项建一列;数据路由按对象注册,读对象当下有的全部字段;`describe_object` 把 `Object.entries(def.fields)` 映射成它的应答;控制台发现 `todo_task` 没有声明表单视图,就按对象自己的字段排版。这些地方没有一处持有一份字段名清单。它们持有的是对象名,每次调用时向定义要其余的一切——所以字段不经编辑就到达了它们全部,而且是按声明顺序到达的。 + +列表视图、数据集和翻译包同样是编写出来的元数据,而且每一个都逐字写明字段名:`columns: [{ field: 'subject' }, { field: 'status' }, …]`、`measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`、`fields: { subject: { label: '主题' }, … }`。写明了字段的投影是在对这个字段做一个选择——这一列、这个标签、这个聚合——运行时会原封不动地兑现这个选择。你没要的,它不会替你加。 + +于是,把测试说给审阅者听: + +> **命名测试。** 对这份 diff 可能波及的每一个投影,问一句:它写明的是这个字段,还是只有对象?只写明对象的,是推导出来的:你正在读的 diff 就是全部改动,运行时已经把它传播到位。写明了字段的,是钉死的:它没有动,该不该动,由你判断。 + +它和定义那篇的删除测试是配套的。删除测试把一份定义归为核心或投影;命名测试把一次核心改动能波及的投影归为推导或钉死。两条合在一起,审阅者光凭 diff 就知道运行时做了什么、还有什么在等一个决定。 + +## 这对读 diff 改变了什么 + +实际后果是一套工作顺序,而且比多数团队现在遵循的那套更短。 + +**第一,定义那几行,就是对所有推导面的审阅。** 没有要陪着读的建表迁移,没有序列化器,没有控制器,没有工具清单。在上面的运行里,`budget` 列存在是因为字段存在;REST 写入接受 `budget` 是因为字段存在;Agent 的 `describe_object` 显示它是因为字段存在。字段对,它们就对;字段错,它们就以和字段一模一样的方式错。审五行审掉了七个面——这正是标题承诺的那句话,也正是术语表条目所谓「碰到核心的 diff 改的是权限与事实」的意思。 + +**第二,在钉死的投影里搜这个字段的名字——并且预期什么都搜不到。** 新加的字段不会出现在任何视图、数据集或翻译里,因为它们一个都没被编辑过。这不是运行时藏起来的遗漏,而是门禁打印出来的那条警告。审阅者要问的,正是门禁 `fix:` 那一行提出的问题:这个字段该不该有一个消费者,是哪一个?对 `budget` 来说,大概率是 *All Tasks* 上的一列和一个 `zh-CN` 标签;大概率暂时不是一个仪表盘度量。那是对三个具名文件的三处小而有意的编辑,每一处都和最初那五行一样可审。 + +**第三,把删除当成另一种改动。** 上面说的全是加字段。驱动的结构同步只做加法:它建缺失的表、加缺失的列,从不修改或删除已有的列,所以一个被删掉或改名的字段会把它的列留在原地,成为 `os migrate` 报告出来、由人来对账的漂移。推导面会立刻跟上删除——REST 记录丢掉那个键,`describe_object` 丢掉那个字段——数据表却不会。而每一个仍然写明被删字段的钉死投影现在都错了,同一道门禁会把它当作悬空绑定报出来。命名测试照样适用;只是要看的清单更长,而且从迁移开始。 + +这套顺序也补上了[四遍审阅清单](/en/blog/how-to-review-ai-generated-app-changes/)缺的那一半——它的第四遍是「字段和视图,放最后」。命名测试说的是:哪些视图。写明了这个字段的那些。其余的,在审字段的时候已经审过了。 + +## 替钉死说句话 + +把上表的右半栏读成一种局限很容易,而诚实的读法恰恰相反。 + +一张会自动把所有新列加进来的网格,会在每次开发者为了让某条流程有地方写而加个字段时,把整个页面重排一遍。一张会把所有数值字段都画出来的仪表盘,会把 `recurrence_interval` 画出来。一份会自己发明标签的翻译,是一份没人写过的翻译。钉死的投影之所以钉死,是因为有人做过一个呈现上的决定;一个悄悄推翻这个决定的运行时,正是这整套格式要防止的那件事——没有任何人声明过的行为。控制台自己的回退——没声明表单视图时就把所有字段排出来——恰好是证明这条规则的例外:它只在还没有任何人做过决定的地方生效。 + +这套格式给审阅者的,不是钉死的投影会跟着变,而是它们*找得到*。它们是同一个仓库里的元数据,它们写明了字段名,门禁能把它们枚举出来。另一种情况——一份手写的代码库——是某个 React 组件里的一份列清单、另一个目录里的一个序列化器、一个 JSON 文件里的一个标签、第三套系统里的一份图表配置,而没有任何东西知道它们说的是同一个字段。 + +## 本文不覆盖的 + +在一个示例上的测量,就是在一个示例上的测量。有四件事留在它之外。 + +**手工调过的视图仍然是手工的。** Todo 示例里每个列表视图都声明了列,CRM 示例里每个视图也是。新字段到达一张钉死的网格,只能靠有人把它加进去——而这个「有人」常常就是加这个字段的那个 Agent,在同一个 PR 里。命名测试告诉你往那里看;它不替你看。 + +**翻译必须有人写。** 运行时会回落到声明的标签,所以缺一条 `zh-CN` 会渲染成 `Budget`,而不是空白。这是对的默认值,但仍然是中文用户看得见的缺口。翻译是最严格意义上的钉死投影:它里面的每一个字符串都是一个决定。 + +**外部消费者在栈之外。** 门禁自己就这么说:「a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view」。另一个工具里按名字选取 `estimated_hours` 的仪表盘,钉在那个字段上,而仓库里没有任何东西知道它。命名测试对它适用;枚举对它不适用。 + +**语义改动不是结构改动。** 加字段是最简单的情形,所以才选它来做一次干净的前后对照。换成改一个字段的*含义*——比如给 `status` 加一个 `cancelled` 选项——推导面会同样忠实地跟上,但 Todo 示例的 *Overdue Tasks* 视图和它的 *Overdue Tasks* 指标都把「逾期」编码成 `status != completed`,于是一条截止日期已过的已取消任务,在两处都会被算作逾期。这是对视图和数据集定义的一次阅读,不是一次测量,而它正是命名测试会把你指过去——两个投影都写明了 `status`——却不替你拍板的那类后果。 + +## 把你的 Agent 指向它 + +上面这次运行可以从仓库复现,包装好之后大约一分钟,这意味着它也是你的 Agent 可以替你做的事。把一个编码 Agent 指向一个 ObjectStack 项目——脚手架会写出 `AGENTS.md` 并安装技能包,运行中的应用在 `/api/v1/mcp` 提供 MCP,[如何配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆盖了规则文件、技能和工具服务器——当它开出一个改字段的变更时,向它要它自己这份 diff 的命名测试表:哪些面跟上了,哪些投影写明了这个字段,它打算对每一个怎么办。门禁那行 `field-no-consumers` 是这张表的第一行,而 Agent 可以比你先读到它。 + +本文的一切都跑在开源栈上。「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise);无论哪个版本,你都可以导出本体,拿到开源的 ObjectStack 运行时上运行。命名测试不随宿主而变;它读的那份定义,是同一个文件。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx new file mode 100644 index 00000000..80eb92ef --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx @@ -0,0 +1,125 @@ +--- +# @generated zh-Hant from zh-Hans (s2twp) — edit the zh-Hans file; delete this line to hand-maintain. +title: 檢視、儀表盤和應用是本體的投影:這對審閱意味著什麼 +description: 給 ObjectStack 自帶示例加一個欄位並逐面實測:資料表、REST、MCP 與控制台表單自己跟上,釘死欄位名的檢視、資料集和翻譯沒動。命名測試告訴審閱者哪個是哪個。 +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover.svg +tags: + - 本體 + - 可執行業務本體 + - 可審閱的 diff + - 後設資料審閱 +--- + +下面就是整份 diff。五行,一個欄位,加在 ObjectStack 倉庫自帶的 Todo 示例的 `todo_task` 物件上: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +在 ObjectStack 執行時上啟動之後,這五行改動了資料庫表、REST 的讀與寫、後設資料介面、MCP 的 `describe_object` 與 `query_records` 應答,以及控制台的新建表單——七個面,沒有一個被編輯過。它們沒有改動三個列表檢視、儀表盤的資料集和中文翻譯,因為這三樣東西都逐個寫明瞭欄位名。前一組是從定義推匯出來的,後一組是釘在定義上的。**命名測試**在你開啟任何一個投影之前就能把它歸到兩組之一:它寫明瞭這個欄位,你就審它;它只寫明瞭物件,執行時已經替你審過了。本文餘下的部分,是這句話背後的測量——在 ObjectStack `18.0.0-next.2` 上、示例原始碼取自提交 `43567a8f`——以及它對讀一份 diff 這件事改變了什麼。 + +## 投影的定義住在哪裡 + +投影是什麼,已經有三處定義,本文不重複。[定義那篇](/zh-Hant/blog/what-does-the-ontology-is-the-software-mean/)點名了四種投影,並給出區分核心與投影的刪除測試。本體頁(`/zh-Hans/ontology/`,「投影」一節)用一句話說明規則:改動核心,投影跟著變;刪掉一個投影,它所展示的業務不變。[術語表條目](/zh-Hant/glossary/ontology-projection/)補上了對審閱者的推論:只碰投影的 diff 改的是呈現,碰到核心的 diff 改的是權限與事實。 + +它們都沒說的是,投影是*怎麼*跟著變的。「跟著變」底下藏著兩種不同的機制,審閱者得知道眼前的是哪一種。下面這次執行回答的就是這個問題。 + +## 這次執行:一個欄位,十一個讀數 + +物件是自帶的 Todo 示例(`examples/app-todo`),從倉庫提交 `43567a8f` 複製出來,用已釋出的 `18.0.0-next.2` 包以 `os dev` 啟動。資料庫是預設的 SQLite 檔案;API key 用種子化的開發管理員通過 REST 簽發。每個讀數取兩次:一次在提交原樣的定義上,一次在上面那五行 diff 之後、帶 `--compile` 重啟之後。「之前」和「之後」就是這兩次的答案。 + +| 面 | 怎麼讀的 | 之前 | 之後 | 跟著變? | +| :--- | :--- | :--- | :--- | :--- | +| 資料庫表 `todo_task` | 對 SQLite 檔案執行 `PRAGMA table_info` | 27 列 | 28 列;`budget float` 加到了已有的表上 | 推導 | +| REST 讀 | `GET /api/v1/data/todo_task` | 每條記錄 27 個鍵 | 28 個鍵 | 推導 | +| REST 寫 | `POST /api/v1/data/todo_task`,帶 `budget: 1200` | `400`——`Unknown field 'budget' on object 'todo_task'`,錯誤碼 `INVALID_FIELD` | `201`,記錄已建立 | 推導 | +| 後設資料介面 | `GET /api/v1/meta/object/todo_task` | 27 個欄位 | 28 個;`{ label: 'Budget', type: 'currency', min: 0, … }` | 推導 | +| MCP `describe_object` | 對 `/api/v1/mcp` 發 `tools/call` | 19 個欄位 | 20 個 | 推導 | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | 推導 | +| 控制台新建表單 | `/_console/apps/todo_app/todo_task/new` | 16 個輸入項 | 17 個;*Budget* 按宣告順序出現在 *Actual Hours* 和 *Notes* 之間 | 推導 | +| 控制台記錄詳情 | 新記錄的頁面 | — | *Details* 區顯示 `BUDGET 1,200.00` | 推導 | +| 控制台列表檢視 *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 列 | 6 列,沒有變化 | 釘死 | +| 翻譯,`zh-CN` | 帶 `Accept-Language: zh-CN` 的 `GET /api/v1/meta/object/todo_task` | `subject` → 主題 | `budget` → `Budget`(回落到英文) | 釘死 | +| 儀表盤資料集 `task_metrics` | 資料集定義本身 | 6 個維度、3 個度量,每個都點名一個欄位 | 沒有變化;在某個度量點名 `budget` 之前,沒有任何圖表能畫它 | 釘死 | + +另有兩個讀數沒有變化,而它們也不是投影:OpenAPI 文件仍然為 `todo_task` 列出 14 條路徑,沒有逐欄位的 schema;MCP 的 `tools/list` 仍然應答同樣的 12 個工具(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)。兩者描述的都是物件,不是欄位;欄位出現在它們的應答裡,而不是它們的形狀裡。 + +![命名測試:中間是那份五行 diff;左邊是隻寫明物件、未經編輯就跟上了定義的面,附前後讀數;右邊是寫明瞭欄位名、原樣未動的投影;底部是構建門禁自己打出的警告——新欄位沒有任何消費者](./naming-test.svg) + +還有一個讀數是白送的。`os dev --compile` 在寫出工件之前會跑一遍編寫期規則,下面是門禁對這個新欄位列印的原話: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget +``` + +`os explain field-no-consumers` 把「消費者」定義為「a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it」,並把它和「載體」區分開——載體「names a field without reading it」,例如「a translation label, a seed value, an import-mapping target, a permission grant」。門禁的這份清單,正是上表的右半欄。這是執行時在啟動之前就告訴你:哪些投影是釘在欄位上的,而其中還沒有任何一個接住這個新欄位。 + +## 命名測試 + +為什麼七個面跟著變、四個面原地不動?不是因為有的更重要。是因為它們各自寫明瞭什麼。 + +資料表、REST 路由、後設資料介面和 MCP 工具都是從物件生成的。SQL 驅動的結構同步遍歷 `obj.fields`,為每一項建一列;資料路由按物件註冊,讀物件當下有的全部欄位;`describe_object` 把 `Object.entries(def.fields)` 對映成它的應答;控制台發現 `todo_task` 沒有宣告表單檢視,就按物件自己的欄位排版。這些地方沒有一處持有一份欄位名清單。它們持有的是物件名,每次呼叫時向定義要其餘的一切——所以欄位不經編輯就到達了它們全部,而且是按宣告順序到達的。 + +列表檢視、資料集和翻譯包同樣是編寫出來的後設資料,而且每一個都逐字寫明欄位名:`columns: [{ field: 'subject' }, { field: 'status' }, …]`、`measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`、`fields: { subject: { label: '主題' }, … }`。寫明瞭欄位的投影是在對這個欄位做一個選擇——這一列、這個標籤、這個聚合——執行時會原封不動地兌現這個選擇。你沒要的,它不會替你加。 + +於是,把測試說給審閱者聽: + +> **命名測試。** 對這份 diff 可能波及的每一個投影,問一句:它寫明的是這個欄位,還是隻有物件?只寫明物件的,是推匯出來的:你正在讀的 diff 就是全部改動,執行時已經把它傳播到位。寫明瞭欄位的,是釘死的:它沒有動,該不該動,由你判斷。 + +它和定義那篇的刪除測試是配套的。刪除測試把一份定義歸為核心或投影;命名測試把一次核心改動能波及的投影歸為推導或釘死。兩條合在一起,審閱者光憑 diff 就知道執行時做了什麼、還有什麼在等一個決定。 + +## 這對讀 diff 改變了什麼 + +實際後果是一套工作順序,而且比多數團隊現在遵循的那套更短。 + +**第一,定義那幾行,就是對所有推導面的審閱。** 沒有要陪著讀的建表遷移,沒有序列化器,沒有控制器,沒有工具清單。在上面的執行裡,`budget` 列存在是因為欄位存在;REST 寫入接受 `budget` 是因為欄位存在;Agent 的 `describe_object` 顯示它是因為欄位存在。欄位對,它們就對;欄位錯,它們就以和欄位一模一樣的方式錯。審五行審掉了七個面——這正是標題承諾的那句話,也正是術語表條目所謂「碰到核心的 diff 改的是權限與事實」的意思。 + +**第二,在釘死的投影裡搜這個欄位的名字——並且預期什麼都搜不到。** 新加的欄位不會出現在任何檢視、資料集或翻譯裡,因為它們一個都沒被編輯過。這不是執行時藏起來的遺漏,而是門禁打印出來的那條警告。審閱者要問的,正是門禁 `fix:` 那一行提出的問題:這個欄位該不該有一個消費者,是哪一個?對 `budget` 來說,大機率是 *All Tasks* 上的一列和一個 `zh-CN` 標籤;大機率暫時不是一個儀表盤度量。那是對三個具名檔案的三處小而有意的編輯,每一處都和最初那五行一樣可審。 + +**第三,把刪除當成另一種改動。** 上面說的全是加欄位。驅動的結構同步只做加法:它建缺失的表、加缺失的列,從不修改或刪除已有的列,所以一個被刪掉或改名的欄位會把它的列留在原地,成為 `os migrate` 報告出來、由人來對賬的漂移。推導面會立刻跟上刪除——REST 記錄丟掉那個鍵,`describe_object` 丟掉那個欄位——資料表卻不會。而每一個仍然寫明被刪欄位的釘死投影現在都錯了,同一道門禁會把它當作懸空繫結報出來。命名測試照樣適用;只是要看的清單更長,而且從遷移開始。 + +這套順序也補上了[四遍審閱清單](/en/blog/how-to-review-ai-generated-app-changes/)缺的那一半——它的第四遍是「欄位和檢視,放最後」。命名測試說的是:哪些檢視。寫明瞭這個欄位的那些。其餘的,在審欄位的時候已經審過了。 + +## 替釘死說句話 + +把上表的右半欄讀成一種侷限很容易,而誠實的讀法恰恰相反。 + +一張會自動把所有新列加進來的網格,會在每次開發者為了讓某條流程有地方寫而加個欄位時,把整個頁面重排一遍。一張會把所有數值欄位都畫出來的儀表盤,會把 `recurrence_interval` 畫出來。一份會自己發明標籤的翻譯,是一份沒人寫過的翻譯。釘死的投影之所以釘死,是因為有人做過一個呈現上的決定;一個悄悄推翻這個決定的執行時,正是這整套格式要防止的那件事——沒有任何人宣告過的行為。控制台自己的回退——沒宣告表單檢視時就把所有欄位排出來——恰好是證明這條規則的例外:它只在還沒有任何人做過決定的地方生效。 + +這套格式給審閱者的,不是釘死的投影會跟著變,而是它們*找得到*。它們是同一個倉庫裡的後設資料,它們寫明瞭欄位名,門禁能把它們枚舉出來。另一種情況——一份手寫的程式碼庫——是某個 React 元件裡的一份列清單、另一個目錄裡的一個序列化器、一個 JSON 檔案裡的一個標籤、第三套系統裡的一份圖表配置,而沒有任何東西知道它們說的是同一個欄位。 + +## 本文不覆蓋的 + +在一個示例上的測量,就是在一個示例上的測量。有四件事留在它之外。 + +**手工調過的檢視仍然是手工的。** Todo 示例裡每個列表檢視都聲明瞭列,CRM 示例裡每個檢視也是。新欄位到達一張釘死的網格,只能靠有人把它加進去——而這個「有人」常常就是加這個欄位的那個 Agent,在同一個 PR 裡。命名測試告訴你往那裡看;它不替你看。 + +**翻譯必須有人寫。** 執行時會回落到宣告的標籤,所以缺一條 `zh-CN` 會渲染成 `Budget`,而不是空白。這是對的預設值,但仍然是中文使用者看得見的缺口。翻譯是最嚴格意義上的釘死投影:它裡面的每一個字串都是一個決定。 + +**外部消費者在棧之外。** 門禁自己就這麼說:「a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view」。另一個工具裡按名字選取 `estimated_hours` 的儀表盤,釘在那個欄位上,而倉庫裡沒有任何東西知道它。命名測試對它適用;列舉對它不適用。 + +**語義改動不是結構改動。** 加欄位是最簡單的情形,所以才選它來做一次乾淨的前後對照。換成改一個欄位的*含義*——比如給 `status` 加一個 `cancelled` 選項——推導面會同樣忠實地跟上,但 Todo 示例的 *Overdue Tasks* 檢視和它的 *Overdue Tasks* 指標都把「逾期」編碼成 `status != completed`,於是一條截止日期已過的已取消任務,在兩處都會被算作逾期。這是對檢視和資料集定義的一次閱讀,不是一次測量,而它正是命名測試會把你指過去——兩個投影都寫明瞭 `status`——卻不替你拍板的那類後果。 + +## 把你的 Agent 指向它 + +上面這次執行可以從倉庫復現,包裝好之後大約一分鐘,這意味著它也是你的 Agent 可以替你做的事。把一個編碼 Agent 指向一個 ObjectStack 專案——腳手架會寫出 `AGENTS.md` 並安裝技能包,執行中的應用在 `/api/v1/mcp` 提供 MCP,[如何配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆蓋了規則檔案、技能和工具伺服器——當它開出一個改欄位的變更時,向它要它自己這份 diff 的命名測試表:哪些面跟上了,哪些投影寫明瞭這個欄位,它打算對每一個怎麼辦。門禁那行 `field-no-consumers` 是這張表的第一行,而 Agent 可以比你先讀到它。 + +本文的一切都跑在開源棧上。「想把同一個迴圈託管起來、在瀏覽器裡用、什麼都不用安裝?那就是 ObjectOS——建立在 ObjectStack 之上的商業執行環境。」ObjectOS 既可託管在瀏覽器中、無需安裝(ObjectOS Cloud),也可自管部署在你自己的基礎設施上(ObjectOS Enterprise);無論哪個版本,你都可以匯出本體,拿到開源的 ObjectStack 執行時上執行。命名測試不隨宿主而變;它讀的那份定義,是同一個檔案。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg new file mode 100644 index 00000000..8066cb8c --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg @@ -0,0 +1,124 @@ + + + + + + + + + + The naming test: does the projection name the field, or only the object? + + + + + THE DIFF · src/objects/task.object.ts + + budget: Field.currency({ + + label: 'Budget', + + min: 0, + + }), + + + + + + + + + + NAMES ONLY THE OBJECT + Derived: it followed + Generated from the object on every + call. The diff is the whole review. + 7 surfaces · 0 edits + + + + + + NAMES THE FIELD + Pinned: it stayed + An authored choice per field. Still + as written; whether it should move + is the reviewer's call · 4 projections + + + + + + BEFORE → AFTER, MEASURED + + + SQLite table todo_task (PRAGMA table_info) + 27 → 28 columns + budget float, added to the existing table by the additive sync + + GET /api/v1/data/todo_task + 27 → 28 keys + + POST /api/v1/data/todo_task with budget: 1200 + 400 → 201 + before: Unknown field 'budget' on object 'todo_task' (INVALID_FIELD) + + GET /api/v1/meta/object/todo_task + 27 → 28 fields + + MCP describe_object + 19 → 20 fields + + MCP query_records, where budget ≥ 1000 + 1 record, 1200 + + Console create form (no form view declared) + 16 → 17 inputs + Budget placed between Actual Hours and Notes, in declaration order + + Unchanged by design, not projections: OpenAPI (14 paths), MCP tools/list (12 tools) + + + + + + AS AUTHORED, UNCHANGED + + + todo_task.default — All Tasks + 6 columns + columns: [{ field: 'subject' }, { field: 'status' }, …] + + todo_task.overdue · todo_task.due_today + 5 + 5 columns + + Dataset task_metrics (feeds the dashboard) + 3 measures + measures: [{ name: 'est_hours', field: 'estimated_hours' }, …] + + Translation bundle, zh-CN + 19 labels + budget → "Budget" (declared label falls through) + + Each one spells the field name, so each one is a + decision the runtime honours exactly. The reviewer + asks the gate's question: should this field have a + consumer, and which one? + + Removals differ: the sync never drops a column, so the table lags as drift + + + + + + THE GATE SAID SO BEFORE BOOT · os dev --compile, 49 author-time rules + ⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) — rule: field-no-consumers + + diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg new file mode 100644 index 00000000..c350e053 --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg @@ -0,0 +1,124 @@ + + + + + + + + + + 命名测试:这个投影写明的是字段,还是只有对象? + + + + + 这份 DIFF · src/objects/task.object.ts + + budget: Field.currency({ + + label: 'Budget', + + min: 0, + + }), + + + + + + + + + + 只写明对象 + 推导:它跟上了 + 每次调用都从对象生成。 + 这份 diff 就是全部审阅。 + 7 个面 · 0 处编辑 + + + + + + 写明了字段 + 钉死:它没动 + 逐字段的一次编写决定。 + 原样未动;该不该动, + 由审阅者判断 · 4 个投影 + + + + + + 之前 → 之后,实测 + + + SQLite 表 todo_task(PRAGMA table_info) + 27 → 28 列 + budget float,由只做加法的同步加到已有的表上 + + GET /api/v1/data/todo_task + 27 → 28 个键 + + POST /api/v1/data/todo_task,带 budget: 1200 + 400 → 201 + 之前:Unknown field 'budget' on object 'todo_task'(INVALID_FIELD) + + GET /api/v1/meta/object/todo_task + 27 → 28 个字段 + + MCP describe_object + 19 → 20 个字段 + + MCP query_records,where budget ≥ 1000 + 1 条记录,1200 + + 控制台新建表单(未声明表单视图) + 16 → 17 个输入项 + Budget 按声明顺序排在 Actual Hours 和 Notes 之间 + + 按设计不变、也不是投影:OpenAPI(14 条路径)、MCP tools/list(12 个工具) + + + + + + 原样,没有变化 + + + todo_task.default — All Tasks + 6 列 + columns: [{ field: 'subject' }, { field: 'status' }, …] + + todo_task.overdue · todo_task.due_today + 5 + 5 列 + + 数据集 task_metrics(供给仪表盘) + 3 个度量 + measures: [{ name: 'est_hours', field: 'estimated_hours' }, …] + + 翻译包,zh-CN + 19 个标签 + budget → "Budget"(回落到声明的标签) + + 每一个都写明了字段名,所以每一个都是 + 运行时原样兑现的一个决定。审阅者问的是 + 门禁的那个问题:这个字段该不该有消费者, + 是哪一个? + + 删除不同:同步从不删列,所以数据表会以漂移的形式滞后 + + + + + + 门禁在启动前就说了 · os dev --compile,49 条编写期规则 + ⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) — rule: field-no-consumers + + From db595628400872d0fab54562a5f8a6cb6ef27ce6 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:25:42 +0000 Subject: [PATCH 2/5] =?UTF-8?q?content(blog):=20the=20two=20covers=20state?= =?UTF-8?q?=20the=20measurement=20=E2=80=94=20one=20field,=20seven=20surfa?= =?UTF-8?q?ces=20follow;=20not=20every=20surface?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The share image travels alone (writing rule 7): the headline now matches the post's own reading, seven derived surfaces followed and four pinned projections did not. Layout unchanged; zh re-derived, not translated. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017GeNDQH7E5S2rX8ajVAPxv --- .../cover-en.svg | 4 ++-- .../cover.svg | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg index b05352d4..5089a887 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg @@ -20,8 +20,8 @@ WHAT CHANGES FOR REVIEW - One field. - Every surface. + One field, + seven surfaces. Add a field to the definition and the table, the API, the MCP tools and the Console follow by themselves. The views that name their columns do not. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg index 1a2dc129..959c6fa7 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg @@ -21,7 +21,7 @@ 这对审阅意味着什么 改一个字段, - 动到每个面。 + 七个面自己跟上。 给定义加一个字段,数据表、API、MCP 工具 和控制台自己跟上。 写明了列名的视图不会。 From 0b2ee7818273bb4ef2a59ee0d60e00923a9ca496 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 08:42:49 +0000 Subject: [PATCH 3/5] content(blog): zh-Hans links the /ontology/ page again, now that gen-zh-hant rewrites marketing-page links The inline-code mention was a detour around the generator gap (#265, landed as 3c5b6c6); the zh-Hans post now links /zh-Hans/ontology/ like the en post links /en/ontology/, and the generated zh-Hant file carries /zh-Hant/ontology/. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_017GeNDQH7E5S2rX8ajVAPxv --- .../index.zh-Hans.mdx | 2 +- .../index.zh-Hant.mdx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx index 20f345b7..64890875 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx @@ -35,7 +35,7 @@ tags: ## 投影的定义住在哪里 -投影是什么,已经有三处定义,本文不重复。[定义那篇](/zh-Hans/blog/what-does-the-ontology-is-the-software-mean/)点名了四种投影,并给出区分核心与投影的删除测试。本体页(`/zh-Hans/ontology/`,「投影」一节)用一句话说明规则:改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。[术语表条目](/zh-Hans/glossary/ontology-projection/)补上了对审阅者的推论:只碰投影的 diff 改的是呈现,碰到核心的 diff 改的是权限与事实。 +投影是什么,已经有三处定义,本文不重复。[定义那篇](/zh-Hans/blog/what-does-the-ontology-is-the-software-mean/)点名了四种投影,并给出区分核心与投影的删除测试。[本体页](/zh-Hans/ontology/)用一句话说明规则:改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。[术语表条目](/zh-Hans/glossary/ontology-projection/)补上了对审阅者的推论:只碰投影的 diff 改的是呈现,碰到核心的 diff 改的是权限与事实。 它们都没说的是,投影是*怎么*跟着变的。「跟着变」底下藏着两种不同的机制,审阅者得知道眼前的是哪一种。下面这次运行回答的就是这个问题。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx index 80eb92ef..5bfa952e 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx @@ -36,7 +36,7 @@ tags: ## 投影的定義住在哪裡 -投影是什麼,已經有三處定義,本文不重複。[定義那篇](/zh-Hant/blog/what-does-the-ontology-is-the-software-mean/)點名了四種投影,並給出區分核心與投影的刪除測試。本體頁(`/zh-Hans/ontology/`,「投影」一節)用一句話說明規則:改動核心,投影跟著變;刪掉一個投影,它所展示的業務不變。[術語表條目](/zh-Hant/glossary/ontology-projection/)補上了對審閱者的推論:只碰投影的 diff 改的是呈現,碰到核心的 diff 改的是權限與事實。 +投影是什麼,已經有三處定義,本文不重複。[定義那篇](/zh-Hant/blog/what-does-the-ontology-is-the-software-mean/)點名了四種投影,並給出區分核心與投影的刪除測試。[本體頁](/zh-Hant/ontology/)用一句話說明規則:改動核心,投影跟著變;刪掉一個投影,它所展示的業務不變。[術語表條目](/zh-Hant/glossary/ontology-projection/)補上了對審閱者的推論:只碰投影的 diff 改的是呈現,碰到核心的 diff 改的是權限與事實。 它們都沒說的是,投影是*怎麼*跟著變的。「跟著變」底下藏著兩種不同的機制,審閱者得知道眼前的是哪一種。下面這次執行回答的就是這個問題。 From c8eb1abc7bcc164477b2acba6bc08130f1a097ae Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 02:48:34 +0000 Subject: [PATCH 4/5] content(blog): re-measure the projections post on ObjectStack 18.3.0 and backfill five locales Re-ran the eleven readings on the published 18.3.0 packages (npm latest on 2026-11-06) with examples/app-todo at the @objectstack/cli@18.3.0 release commit 83d7e4ae. Relabelled prose, cover footers and naming-test SVGs to 18.3.0 and updated what drifted: 28 -> 29 columns/keys/metadata fields, MCP describe_object now 28 -> 29 (it serves system fields), OpenAPI 17 paths, 63 author-time rules, the gate's rule line, the Console URL and the Overview tab. The Overdue paragraph now quotes due_date's dueLike + settledWhen (task.object.ts:73-74) and notes the keys are display-only. The ObjectOS paragraph follows E1 (export is planned), and the post links The Ontology Is the Software. Adds ja, ko, de, es and fr translations; zh-Hant regenerated with pnpm gen:zh-hant. date is unchanged. Co-Authored-By: Claude --- .../cover-en.svg | 8 +- .../cover.svg | 8 +- .../index.de.mdx | 124 ++++++++++++++++++ .../index.es.mdx | 124 ++++++++++++++++++ .../index.fr.mdx | 124 ++++++++++++++++++ .../index.ja.mdx | 124 ++++++++++++++++++ .../index.ko.mdx | 124 ++++++++++++++++++ .../index.mdx | 28 ++-- .../index.zh-Hans.mdx | 28 ++-- .../index.zh-Hant.mdx | 46 +++---- .../naming-test-en.svg | 14 +- .../naming-test.svg | 14 +- 12 files changed, 693 insertions(+), 73 deletions(-) create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx create mode 100644 content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg index 5089a887..a8102098 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover-en.svg @@ -40,13 +40,13 @@ FOLLOWED, NO EDIT · DERIVED SQLite table todo_task - 27 → 28 columns + 28 → 29 columns GET /api/v1/data/todo_task - 27 → 28 keys + 28 → 29 keys POST with budget: 1200 400 → 201 MCP describe_object - 19 → 20 fields + 28 → 29 fields Console create form 16 → 17 inputs @@ -62,5 +62,5 @@ 3 measures, by name - Measured on ObjectStack 18.0.0-next.2, examples/app-todo at commit 43567a8f: one five-line diff, eleven readings before and after. + Measured on ObjectStack 18.3.0, examples/app-todo at its release commit 83d7e4ae: one five-line diff, eleven readings before and after. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg index 959c6fa7..2cded07c 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/cover.svg @@ -40,13 +40,13 @@ 未经编辑就跟上 · 推导 SQLite 表 todo_task - 27 → 28 列 + 28 → 29 列 GET /api/v1/data/todo_task - 27 → 28 个键 + 28 → 29 个键 POST 带 budget: 1200 400 → 201 MCP describe_object - 19 → 20 个字段 + 28 → 29 个字段 控制台新建表单 16 → 17 个输入项 @@ -62,5 +62,5 @@ 3 个度量,按名字 - 在 ObjectStack 18.0.0-next.2 上测量,examples/app-todo 取自提交 43567a8f:一份五行 diff,前后各十一个读数。 + 在 ObjectStack 18.3.0 上测量,examples/app-todo 取自其发布提交 83d7e4ae:一份五行 diff,前后各十一个读数。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx new file mode 100644 index 00000000..bc2aa2ab --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx @@ -0,0 +1,124 @@ +--- +title: "Views, Dashboards und Apps sind Projektionen der Ontologie: Was das für das Review ändert" +description: "Ein Feld mehr in einer mitgelieferten ObjectStack-App: Tabelle, REST-API, MCP-describe und Console-Formular folgen von selbst, die festgeschriebenen Views, das Dataset und die Übersetzung nicht. Der Namenstest sagt, was was ist." +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontologie + - Ausführbare Geschäftsontologie + - Reviewbarer Diff + - Metadaten-Review +--- + +Das ist der ganze Diff. Fünf Zeilen, ein Feld, hinzugefügt zum Objekt `todo_task` des Todo-Beispiels, das im ObjectStack-Repository mitgeliefert wird: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +Auf der ObjectStack Runtime gestartet, haben diese fünf Zeilen die Datenbanktabelle verändert, das Lesen und Schreiben über REST, den Metadaten-Endpunkt, die Antworten von MCP `describe_object` und `query_records` und das Anlegeformular der Console: sieben Oberflächen, von denen keine bearbeitet wurde. Nicht verändert haben sie die drei Listen-Views, das Dataset des Dashboards und die chinesische Übersetzung, denn jede davon nennt Felder beim Namen. Die erste Gruppe ist aus der Definition abgeleitet; die zweite ist an sie festgeschrieben. **Der Namenstest** ordnet jede Projektion einer der beiden zu, bevor Sie sie öffnen: *Nennt sie das Feld, reviewen Sie sie; nennt sie nur das Objekt, hat die Runtime es schon getan.* Der Rest dieses Beitrags ist die Messung hinter diesem Satz, durchgeführt auf ObjectStack `18.3.0`, dem `latest`-Release auf npm zum Zeitpunkt des Laufs, mit dem Quellcode des Beispiels am Commit `83d7e4ae` dieses Releases, und das, was sie am Lesen eines Diffs ändert. + +## Wo die Definition einer Projektion steht + +Drei Stellen definieren bereits, was eine Projektion ist, und dieser Beitrag wiederholt sie nicht; der Essay [Die Ontologie ist die Software](/de/blog/the-ontology-is-the-software/) zieht dieselbe Linie als erste der beiden Grenzen, die er für das Wort festhält. [Der Definitionsbeitrag](/de/blog/what-does-the-ontology-is-the-software-mean/) nennt die vier Projektionen und gibt den Entfernungstest an, der Kern von Projektion trennt. [Die Ontologie-Seite](/en/ontology/) formuliert die Regel in einer Zeile: Ändern Sie den Kern, und die Projektionen folgen; entfernen Sie eine Projektion, und das Geschäft, das sie zeigte, ist unverändert. Der [Glossareintrag](/en/glossary/ontology-projection/) ergänzt die Folge für einen Reviewer: Ein Diff, der nur Projektionen berührt, ändert die Darstellung, und ein Diff, der den Kern berührt, ändert die Autorität. + +Was keine davon sagt, ist, *wie* eine Projektion folgt. „Folgen“ umfasst zwei verschiedene Mechanismen, und ein Reviewer muss wissen, welcher vor ihm liegt. Diese Frage beantwortet der Lauf unten. + +## Der Lauf: ein Feld, elf Messwerte + +Grundlage ist das mitgelieferte Todo-Beispiel (`examples/app-todo`), aus dem Repository am Commit `83d7e4ae` (dem Release-Tag `@objectstack/cli@18.3.0`) herauskopiert und auf den veröffentlichten `18.3.0`-Paketen mit `os dev` gestartet. Die Datenbank ist eine lokale SQLite-Datei; der API-Schlüssel wurde über REST vom geseedeten Dev-Admin erzeugt. Jeder Messwert wurde zweimal erhoben: einmal auf der committeten Definition, einmal nach dem Fünf-Zeilen-Diff oben und einem Neustart mit `--compile`. „Vorher“ und „Nachher“ sind die beiden Antworten. + +| Oberfläche | Wie gelesen | Vorher | Nachher | Folgt? | +| :--- | :--- | :--- | :--- | :--- | +| Datenbanktabelle `todo_task` | `PRAGMA table_info` auf der SQLite-Datei | 28 Spalten | 29; `budget float` zur bestehenden Tabelle hinzugefügt | Abgeleitet | +| REST-Lesen | `GET /api/v1/data/todo_task` | 28 Schlüssel pro Datensatz | 29 Schlüssel | Abgeleitet | +| REST-Schreiben | `POST /api/v1/data/todo_task` mit `budget: 1200` | `400` — `Unknown field 'budget' on object 'todo_task'`, Code `INVALID_FIELD` | `201`, Datensatz angelegt | Abgeleitet | +| Metadaten-Endpunkt | `GET /api/v1/meta/object/todo_task` | 28 Felder | 29; `{ label: 'Budget', type: 'currency', min: 0, … }` | Abgeleitet | +| MCP `describe_object` | `tools/call` auf `/api/v1/mcp` | 28 Felder | 29 | Abgeleitet | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | Abgeleitet | +| Console-Anlegeformular | `/_console/apps/com.example.todo/todo_task/new` | 16 Eingaben | 17; *Budget* zwischen *Actual Hours* und *Notes*, in Deklarationsreihenfolge | Abgeleitet | +| Console-Datensatzdetail | die Seite des neuen Datensatzes | — | `BUDGET 1,200.00` im Tab *Overview* | Abgeleitet | +| Console-Listen-View *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 Spalten | 6 Spalten, unverändert | Festgeschrieben | +| Übersetzung, `zh-CN` | `GET /api/v1/meta/object/todo_task` mit `Accept-Language: zh-CN` | `subject` → 主题 | `budget` → `Budget` (fällt auf Englisch zurück) | Festgeschrieben | +| Dashboard-Dataset `task_metrics` | die Dataset-Definition | 6 Dimensionen, 3 Kennzahlen, jede nennt ein Feld | unverändert; nichts kann `budget` darstellen, bis eine Kennzahl es nennt | Festgeschrieben | + +Zwei Messwerte haben sich nicht bewegt und sind keine Projektionen: Das OpenAPI-Dokument listet für `todo_task` weiterhin 17 Pfade ohne feldweises Schema, und MCP `tools/list` antwortet weiterhin mit denselben 12 Tools (`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`). Beide beschreiben Objekte, nicht Felder; das Feld taucht in ihren Antworten auf, nicht in ihrer Form. + +![Der Namenstest: in der Mitte der Fünf-Zeilen-Diff; links die Oberflächen, die nur das Objekt nennen und ihm ohne Bearbeitung gefolgt sind, mit ihren Vorher-Nachher-Werten; rechts die Projektionen, die das Feld nennen und genau so geblieben sind, wie sie geschrieben wurden; unten die eigene Warnung des Build-Gates, dass das neue Feld keinen Konsumenten hat](./naming-test-en.svg) + +Ein weiterer Messwert kam gratis dazu. `os dev --compile` führt die Autorenzeit-Regeln aus, bevor es das Artefakt schreibt, und das hat das Gate über das neue Feld ausgegeben: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer +``` + +`os explain field-no-consumers` definiert einen Konsumenten als „a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it“ (eine View-Spalte, eine Inline-Grid-Spalte, ein Formularabschnitt, eine Seitenbindung, ein Flow-Knoten, ein Dataset- oder Cube-Member, ein Dashboard-Widget, eine Formel, eine Validierung, ein Hook oder eine Aktion, die es nennt); der Text in 18.3.0 zählt danach auch eine deklarierte Feldgruppe und einen Match-Schlüssel eines Seeds oder Import-Mappings mit. Er grenzt ihn von einem Träger ab, der „names a field without reading it“ (ein Feld nennt, ohne es zu lesen), etwa „a translation label, a seed value, an import-mapping target, a permission grant“ (ein Übersetzungslabel, ein Seed-Wert, ein Ziel eines Import-Mappings, eine Berechtigungsvergabe). Die Liste des Gates ist die rechte Spalte der Tabelle. Es ist die Runtime, die Ihnen vor dem Start sagt, welche Projektionen an ein Feld festgeschrieben sind und dass noch keine davon dieses aufgenommen hat. + +## Der Namenstest + +Warum folgen sieben Oberflächen und vier bleiben stehen? Nicht, weil manche wichtiger sind. Sondern wegen dessen, was jede nennt. + +Die Tabelle, die REST-Routen, der Metadaten-Endpunkt und die MCP-Tools werden aus dem Objekt erzeugt. Der Schema-Sync des SQL-Treibers durchläuft `obj.fields` und legt für jeden Eintrag eine Spalte an; die Datenrouten werden pro Objekt registriert und lesen die Felder, die das Objekt gerade hat; `describe_object` bildet `Object.entries(def.fields)` in seine Antwort ab; die Console findet für `todo_task` keine deklarierte Formular-View und legt die eigenen Felder des Objekts aus. Keine davon hält eine Liste von Feldnamen. Sie halten den Namen des Objekts und fragen die Definition bei jedem Aufruf nach dem Rest, weshalb das Feld sie alle ohne Bearbeitung erreicht hat, und zwar in Deklarationsreihenfolge. + +Die Listen-Views, das Dataset und das Übersetzungsbündel sind ebenfalls geschriebene Metadaten, und jede davon buchstabiert Feldnamen aus: `columns: [{ field: 'subject' }, { field: 'status' }, …]`, `measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`, `fields: { subject: { label: '主题' }, … }`. Eine Projektion, die das Feld nennt, trifft eine Entscheidung darüber, diese Spalte, dieses Label, diese Aggregation, und die Runtime hält sich genau an diese Entscheidung. Sie fügt nicht hinzu, worum Sie nicht gebeten haben. + +Also der Test, formuliert für einen Reviewer: + +> **Der Namenstest.** Fragen Sie für jede Projektion, die der Diff erreichen könnte, ob sie das Feld nennt oder nur das Objekt. Nennt sie nur das Objekt, ist sie abgeleitet: Der Diff, den Sie lesen, ist die ganze Änderung, und die Runtime hat sie bereits verbreitet. Nennt sie das Feld, ist sie festgeschrieben: Sie hat sich nicht bewegt, und ob sie es hätte tun sollen, entscheiden Sie. + +Er ergänzt den Entfernungstest aus dem Definitionsbeitrag. Der Entfernungstest ordnet eine Definition dem Kern oder einer Projektion zu. Der Namenstest ordnet die Projektionen, die eine Kernänderung erreichen kann, in abgeleitet und festgeschrieben. Mit beiden weiß ein Reviewer allein aus dem Diff, was die Runtime getan hat und was noch auf eine Entscheidung wartet. + +## Was das am Lesen des Diffs ändert + +Die praktische Folge ist eine Arbeitsreihenfolge, und sie ist kürzer als die, der die meisten Teams folgen. + +**Erstens: Die Definitionszeile ist das Review für jede abgeleitete Oberfläche.** Es gibt keine Tabellenmigration, die man daneben liest, keinen Serializer, keinen Controller, kein Tool-Manifest. Im Lauf oben existiert die Spalte `budget`, weil das Feld existiert; das REST-Schreiben akzeptiert `budget`, weil das Feld existiert; das `describe_object` des Agents zeigt es, weil das Feld existiert. Ist das Feld richtig, sind sie richtig, und ist das Feld falsch, sind sie genau so falsch wie das Feld. Die fünf Zeilen zu reviewen hieß, sieben Oberflächen zu reviewen; das ist der Satz, den der Titel versprochen hat, und der Satz, den der Glossareintrag mit „ein Diff, der den Kern berührt, ändert die Autorität“ meint. + +**Zweitens: Durchsuchen Sie die festgeschriebenen Projektionen nach dem Namen des Feldes, und erwarten Sie, nichts zu finden.** Ein hinzugefügtes Feld erscheint in keiner View, keinem Dataset und keiner Übersetzung, weil keine davon bearbeitet wurde. Das ist kein Versäumnis, das die Runtime versteckt; es ist die Warnung, die das Gate ausgegeben hat. Die Frage des Reviewers ist die, die die `fix:`-Zeile des Gates stellt: Sollte dieses Feld einen Konsumenten haben, und welchen? Für `budget` wahrscheinlich eine Spalte in *All Tasks* und ein `zh-CN`-Label; wahrscheinlich noch keine Dashboard-Kennzahl. Das sind drei kleine, bewusste Änderungen an drei benannten Dateien, und jede ist so reviewbar wie die ersten fünf Zeilen. + +**Drittens: Behandeln Sie eine Entfernung als eine andere Art von Änderung.** Alles oben betrifft ein hinzugefügtes Feld. Der Schema-Sync des Treibers ist rein additiv: Er legt fehlende Tabellen an und fügt fehlende Spalten hinzu, ändert oder löscht aber nie bestehende, sodass ein entferntes oder umbenanntes Feld seine Spalte als Drift zurücklässt, die `os migrate` meldet und ein Mensch abgleicht. Die abgeleiteten Oberflächen folgen einer Entfernung sofort, der REST-Datensatz verliert den Schlüssel, `describe_object` verliert das Feld, die Tabelle aber nicht. Und jede festgeschriebene Projektion, die das entfernte Feld noch nennt, ist jetzt falsch, was Ihnen dasselbe Gate als hängende Bindung meldet. Der Namenstest gilt weiterhin; die Liste der Dinge, die man ansehen muss, ist nur länger, und sie beginnt mit der Migration. + +Diese Reihenfolge ist auch die fehlende Hälfte der [Review-Checkliste in vier Durchgängen](/en/blog/how-to-review-ai-generated-app-changes/), deren vierter Durchgang „Felder und Views zuletzt“ lautet. Der Namenstest sagt, welche Views: die, die das Feld nennen. Die übrigen wurden reviewt, als das Feld reviewt wurde. + +## Zur Verteidigung des Festschreibens + +Es wäre leicht, die rechte Spalte der Tabelle als Einschränkung zu lesen, und die ehrliche Lesart ist das Gegenteil. + +Ein Grid, das jede neue Spalte automatisch hinzufügt, würde einen Bildschirm jedes Mal umordnen, wenn ein Entwickler ein Feld hinzufügt, in das ein Flow schreiben soll. Ein Dashboard, das jedes numerische Feld darstellt, würde `recurrence_interval` darstellen. Eine Übersetzung, die Labels erfindet, wäre eine Übersetzung, die niemand geschrieben hat. Festgeschriebene Projektionen sind festgeschrieben, weil ein Mensch eine Darstellungsentscheidung getroffen hat, und eine Runtime, die sie stillschweigend überschreibt, wäre genau das, was dieses Format verhindern soll: Verhalten, das niemand deklariert hat. Der eigene Fallback der Console, alle Felder auszulegen, wenn keine Formular-View deklariert ist, ist die Ausnahme, die die Regel bestätigt: Er greift nur dort, wo noch niemand etwas entschieden hat. + +Was das Format dem Reviewer gibt, ist nicht, dass festgeschriebene Projektionen folgen, sondern dass sie *auffindbar* sind. Sie sind Metadaten im selben Repository, sie nennen das Feld, und das Gate zählt sie auf. Die Alternative in einer handgeschriebenen Codebasis ist eine Spaltenliste in einer React-Komponente, ein Serializer in einem anderen Verzeichnis, ein Label in einer JSON-Datei und eine Diagrammkonfiguration in einem dritten System, ohne dass irgendetwas weiß, dass sie dasselbe Feld betreffen. + +## Was das nicht abdeckt + +Eine Messung an einem Beispiel ist eine Messung an einem Beispiel. Vier Dinge bleiben außerhalb. + +**Handgepflegte Views bleiben handgepflegt.** Jede Listen-View im Todo-Beispiel deklariert ihre Spalten, und ebenso jede View im CRM-Beispiel. Ein neues Feld erreicht ein festgeschriebenes Grid nur, wenn jemand es hinzufügt, und dieser Jemand ist oft der Agent, der das Feld hinzugefügt hat, im selben PR. Der Namenstest sagt Ihnen, dass Sie dort hinsehen sollen; das Hinsehen übernimmt er nicht. + +**Übersetzungen müssen geschrieben werden.** Die Runtime fällt auf das deklarierte Label zurück, sodass ein fehlender `zh-CN`-Eintrag `Budget` statt nichts anzeigt. Das ist der richtige Standard und trotzdem eine Lücke, die ein chinesischer Nutzer sieht. Eine Übersetzung ist eine festgeschriebene Projektion im strengsten Sinn: Jeder String darin ist eine Entscheidung. + +**Externe Konsumenten liegen außerhalb des Stacks.** Das Gate sagt es selbst: „a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view“ (ein Konsument kann berechtigterweise außerhalb des Stacks liegen: ein API-Client, ein Hook oder Paket, das dieser Stack nicht mitführt, eine im Studio erstellte View). Ein Dashboard in einem anderen Werkzeug, das `estimated_hours` beim Namen auswählt, ist an dieses Feld festgeschrieben, und nichts im Repository weiß davon. Der Namenstest gilt dafür; die Aufzählung nicht. + +**Semantische Änderungen sind keine strukturellen Änderungen.** Ein Feld hinzuzufügen ist der einfache Fall, weshalb er für einen sauberen Vorher-Nachher-Vergleich gewählt wurde. Ändern Sie stattdessen die *Bedeutung* eines Feldes, fügen Sie etwa `status` eine Option `cancelled` hinzu, und die abgeleiteten Oberflächen folgen ebenso treu, aber die View *Overdue Tasks* des Todo-Beispiels und seine Kennzahl *Overdue Tasks* kodieren überfällig beide als `status != completed`, sodass eine abgebrochene Aufgabe mit vergangenem Fälligkeitsdatum an beiden Stellen als überfällig zählen würde. Das Feld deklariert diese Definition durchaus einmal: `due_date` trägt `` dueLike: true, settledWhen: P`record.status == 'completed'` `` (`examples/app-todo/src/objects/task.object.ts`, Zeilen 73–74 in 18.3.0), aber die Spezifikation erklärt beide Schlüssel als reine Anzeige, gelesen von den Datumszellen der Console, sodass der View-Filter und die Kennzahl die Regel weiterhin selbst ausbuchstabieren. Das ist eine Lektüre der View-, Dashboard- und Objektdefinitionen, keine Messung, und es ist die Art von Folge, auf die der Namenstest Sie hinweist, alle drei nennen `status`, ohne für Sie zu entscheiden. + +## Richten Sie Ihren Agent darauf + +Der Lauf oben ist aus dem Repository reproduzierbar und dauert etwa eine Minute, sobald die Pakete installiert sind, was heißt, dass auch Ihr Agent ihn für Sie erledigen kann. Richten Sie einen Coding-Agent auf ein ObjectStack-Projekt: Der Scaffolder schreibt eine `AGENTS.md` und installiert das Skills-Bündel, die laufende App stellt MCP unter `/api/v1/mcp` bereit, und [wie man ihn konfiguriert](/en/blog/how-to-point-your-agent-at-objectstack/) behandelt die Regeldatei, die Skills und den Tool-Server. Wenn er eine Änderung an einem Feld vorlegt, verlangen Sie von ihm die Namenstest-Tabelle seines eigenen Diffs: welche Oberflächen gefolgt sind, welche Projektionen das Feld nennen und was er mit jeder vorhat. Die `field-no-consumers`-Zeile des Gates ist die erste Zeile dieser Tabelle, und der Agent kann sie lesen, bevor Sie es tun. + +Alles in diesem Beitrag lief auf dem Open-Source-Stack. „Dieselbe Schleife gehostet, im Browser, ohne Installation? Das ist ObjectOS, die kommerzielle Laufzeitumgebung auf Basis von ObjectStack.“ ObjectOS läuft gehostet im Browser ohne Installation (ObjectOS Cloud) oder selbst verwaltet auf Ihrer eigenen Infrastruktur (ObjectOS Enterprise). Ihre Ontologie besteht aus gewöhnlichen ObjectStack-Metadaten. Der Export aus ObjectOS Cloud oder ObjectOS Enterprise zur Ausführung auf der quelloffenen ObjectStack Runtime ist geplant. Der Namenstest ändert sich nicht mit dem Host; die Definition, die er liest, ist dieselbe Datei. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx new file mode 100644 index 00000000..b7d36c9d --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx @@ -0,0 +1,124 @@ +--- +title: "Las vistas, los dashboards y las apps son proyecciones de la ontología: qué cambia eso para la revisión" +description: "Añade un campo a una app de ObjectStack: la tabla, la API REST, la llamada describe de MCP y el formulario de la Console lo siguen solos; las vistas, el dataset y la traducción fijados, no. La prueba del nombre dice cuál es cuál." +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontología + - Ontología de negocio ejecutable + - Diff revisable + - Revisión de metadatos +--- + +Este es el diff completo. Cinco líneas, un campo, añadido al objeto `todo_task` del ejemplo Todo que viene en el repositorio de ObjectStack: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +Arrancadas en el runtime de ObjectStack, esas cinco líneas cambiaron la tabla de la base de datos, la lectura y la escritura REST, el endpoint de metadatos, las respuestas de `describe_object` y `query_records` de MCP y el formulario de creación de la Console: siete superficies, ninguna de ellas editada. No cambiaron las tres vistas de lista, el dataset del dashboard ni la traducción al chino, porque cada una nombra los campos por su nombre. El primer grupo se deriva de la definición; el segundo está fijado a ella. **La prueba del nombre** clasifica cualquier proyección en uno de los dos antes de abrirla: *si nombra el campo, la revisas tú; si solo nombra el objeto, ya lo hizo el runtime.* El resto de este artículo es la medición que hay detrás de esa frase, hecha en ObjectStack `18.3.0`, la versión `latest` en npm cuando se ejecutó, con el código del ejemplo en el commit `83d7e4ae` de esa versión, y lo que cambia en la forma de leer un diff. + +## Dónde vive la definición de una proyección + +Tres lugares ya definen qué es una proyección, y este artículo no los repite; el ensayo [La ontología es el software](/es/blog/the-ontology-is-the-software/) traza la misma línea como el primero de los dos límites que mantiene sobre la palabra. [El artículo de definición](/es/blog/what-does-the-ontology-is-the-software-mean/) nombra las cuatro proyecciones y da la prueba de eliminación para distinguir el núcleo de una proyección. [La página de la ontología](/en/ontology/) enuncia la regla en una línea: cambia el núcleo y las proyecciones lo siguen; elimina una proyección y el negocio que mostraba no cambia. La [entrada del glosario](/en/glossary/ontology-projection/) añade la consecuencia para quien revisa: un diff que solo toca proyecciones cambia la presentación, y un diff que toca el núcleo cambia la autoridad. + +Lo que ninguno dice es *cómo* sigue una proyección. «Seguir» abarca dos mecanismos distintos, y quien revisa necesita saber cuál tiene delante. Esa es la pregunta que responde la ejecución de abajo. + +## La ejecución: un campo, once lecturas + +La base es el ejemplo Todo incluido (`examples/app-todo`), copiado del repositorio en el commit `83d7e4ae` (la etiqueta de versión `@objectstack/cli@18.3.0`) y ejecutado con los paquetes `18.3.0` publicados mediante `os dev`. La base de datos es un archivo SQLite local; la clave de API se emitió por REST desde el administrador de desarrollo sembrado. Cada lectura se tomó dos veces: una con la definición confirmada en el repositorio, otra después del diff de cinco líneas de arriba y de un reinicio con `--compile`. «Antes» y «después» son las dos respuestas. + +| Superficie | Cómo se leyó | Antes | Después | ¿Sigue? | +| :--- | :--- | :--- | :--- | :--- | +| Tabla de la base de datos `todo_task` | `PRAGMA table_info` sobre el archivo SQLite | 28 columnas | 29; `budget float` añadida a la tabla existente | Derivada | +| Lectura REST | `GET /api/v1/data/todo_task` | 28 claves por registro | 29 claves | Derivada | +| Escritura REST | `POST /api/v1/data/todo_task` con `budget: 1200` | `400` — `Unknown field 'budget' on object 'todo_task'`, código `INVALID_FIELD` | `201`, registro creado | Derivada | +| Endpoint de metadatos | `GET /api/v1/meta/object/todo_task` | 28 campos | 29; `{ label: 'Budget', type: 'currency', min: 0, … }` | Derivada | +| MCP `describe_object` | `tools/call` sobre `/api/v1/mcp` | 28 campos | 29 | Derivada | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | Derivada | +| Formulario de creación de la Console | `/_console/apps/com.example.todo/todo_task/new` | 16 entradas | 17; *Budget* entre *Actual Hours* y *Notes*, en orden de declaración | Derivada | +| Detalle de registro de la Console | la página del nuevo registro | — | `BUDGET 1,200.00` en la pestaña *Overview* | Derivada | +| Vista de lista de la Console *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 columnas | 6 columnas, sin cambios | Fijada | +| Traducción, `zh-CN` | `GET /api/v1/meta/object/todo_task` con `Accept-Language: zh-CN` | `subject` → 主题 | `budget` → `Budget` (cae al inglés) | Fijada | +| Dataset del dashboard `task_metrics` | la definición del dataset | 6 dimensiones, 3 medidas, cada una nombra un campo | sin cambios; nada puede graficar `budget` hasta que una medida lo nombre | Fijada | + +Dos lecturas no se movieron y no son proyecciones: el documento OpenAPI sigue listando 17 rutas para `todo_task` sin esquema por campo, y el `tools/list` de MCP sigue respondiendo las mismas 12 herramientas (`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`). Ambos describen objetos, no campos; el campo aparece dentro de sus respuestas, no en su forma. + +![La prueba del nombre: el diff de cinco líneas en el centro; a la izquierda, las superficies que solo nombran el objeto y lo siguieron sin edición, con sus lecturas de antes y después; a la derecha, las proyecciones que nombran el campo y se quedaron exactamente como se escribieron; abajo, el propio aviso de la puerta de build de que el nuevo campo no tiene consumidor](./naming-test-en.svg) + +Una lectura más vino gratis. `os dev --compile` ejecuta las reglas de tiempo de autoría antes de escribir el artefacto, y esto es lo que imprimió la puerta sobre el nuevo campo: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer +``` + +`os explain field-no-consumers` define un consumidor como «a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it» (una columna de vista, una columna de grid en línea, una sección de formulario, un binding de página, un nodo de flow, un miembro de dataset o de cubo, un widget de dashboard, una fórmula, una validación, un hook o una acción que lo nombre); el texto de 18.3.0 cuenta además un grupo de campos declarado y la clave de coincidencia de un seed o de un mapeo de importación. Lo separa de un portador, que «names a field without reading it» (nombra un campo sin leerlo), como «a translation label, a seed value, an import-mapping target, a permission grant» (una etiqueta de traducción, un valor de seed, un destino de mapeo de importación, una concesión de permiso). La lista de la puerta es la columna derecha de la tabla. Es el runtime diciéndote, antes del arranque, qué proyecciones están fijadas a un campo y que ninguna ha recogido todavía este. + +## La prueba del nombre + +¿Por qué siete superficies siguen y cuatro se quedan quietas? No porque unas sean más importantes. Por lo que nombra cada una. + +La tabla, las rutas REST, el endpoint de metadatos y las herramientas MCP se generan a partir del objeto. La sincronización de esquema del driver SQL recorre `obj.fields` y crea una columna por cada entrada; las rutas de datos se registran por objeto y leen los campos que el objeto tenga; `describe_object` mapea `Object.entries(def.fields)` a su respuesta; la Console, al no encontrar una vista de formulario declarada para `todo_task`, dispone los campos propios del objeto. Ninguna guarda una lista de nombres de campo. Guardan el nombre del objeto y piden el resto a la definición en cada llamada, y por eso el campo llegó a todas sin edición, y llegó en orden de declaración. + +Las vistas de lista, el dataset y el paquete de traducción también son metadatos escritos, y cada uno deletrea nombres de campo: `columns: [{ field: 'subject' }, { field: 'status' }, …]`, `measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`, `fields: { subject: { label: '主题' }, … }`. Una proyección que nombra el campo está tomando una decisión sobre él —esta columna, esta etiqueta, esta agregación— y el runtime respeta esa decisión al pie de la letra. No añade lo que no pediste. + +Así que la prueba, enunciada para quien revisa: + +> **La prueba del nombre.** Para cada proyección a la que el diff podría llegar, pregunta si nombra el campo o solo el objeto. Si solo nombra el objeto, es derivada: el diff que estás leyendo es todo el cambio, y el runtime ya lo propagó. Si nombra el campo, está fijada: no se movió, y si debería haberlo hecho es decisión tuya. + +Se combina con la prueba de eliminación del artículo de definición. La prueba de eliminación clasifica una definición como núcleo o proyección. La prueba del nombre clasifica las proyecciones a las que puede llegar un cambio en el núcleo como derivadas o fijadas. Con las dos, quien revisa sabe, solo por el diff, qué hizo el runtime y qué sigue esperando una decisión. + +## Qué cambia en la forma de leer el diff + +La consecuencia práctica es un orden de trabajo, y es más corto que el que siguen la mayoría de los equipos. + +**Primero, la línea de la definición es la revisión de cada superficie derivada.** No hay una migración de tabla que leer al lado, ni serializador, ni controlador, ni manifiesto de herramientas. En la ejecución de arriba, la columna `budget` existe porque existe el campo; la escritura REST acepta `budget` porque existe el campo; el `describe_object` del agente lo muestra porque existe el campo. Si el campo está bien, ellas están bien, y si el campo está mal, están mal exactamente de la misma manera que el campo. Revisar las cinco líneas fue revisar siete superficies, que es la frase que prometía el título y la frase que la entrada del glosario quiere decir con «un diff que toca el núcleo cambia la autoridad». + +**Segundo, busca el nombre del campo en las proyecciones fijadas, y espera no encontrar nada.** Un campo añadido no aparecerá en ninguna vista, dataset ni traducción, porque ninguna ha sido editada. No es una omisión que el runtime esconda; es el aviso que imprimió la puerta. La pregunta de quien revisa es la que plantea la línea `fix:` de la puerta: ¿debería este campo tener un consumidor, y cuál? Para `budget`, probablemente una columna en *All Tasks* y una etiqueta `zh-CN`; probablemente todavía no una medida de dashboard. Son tres ediciones pequeñas y deliberadas en tres archivos con nombre, y cada una es tan revisable como las cinco primeras líneas. + +**Tercero, trata una eliminación como otro tipo de cambio.** Todo lo anterior trata de un campo añadido. La sincronización de esquema del driver solo suma: crea las tablas que faltan y añade las columnas que faltan, y nunca altera ni elimina las existentes, así que un campo eliminado o renombrado deja su columna atrás como una deriva que `os migrate` informa y que una persona concilia. Las superficies derivadas siguen una eliminación al instante —el registro REST pierde la clave, `describe_object` pierde el campo— mientras que la tabla no. Y cada proyección fijada que todavía nombra el campo eliminado pasa a estar mal, cosa que la misma puerta te dirá como un binding colgante. La prueba del nombre sigue valiendo; la lista de cosas que mirar es simplemente más larga, y empieza por la migración. + +Este orden es también la mitad que le faltaba a la [lista de revisión en cuatro pasadas](/en/blog/how-to-review-ai-generated-app-changes/), cuya cuarta pasada es «campos y vistas, al final». La prueba del nombre dice qué vistas: las que nombran el campo. Las demás quedaron revisadas cuando se revisó el campo. + +## En defensa de fijar + +Sería fácil leer la columna derecha de la tabla como una limitación, y la lectura honesta es la contraria. + +Un grid que añadiera cada columna nueva automáticamente reordenaría una pantalla cada vez que un desarrollador añadiera un campo para que un flow escriba en él. Un dashboard que graficara cada campo numérico graficaría `recurrence_interval`. Una traducción que inventara etiquetas sería una traducción que nadie escribió. Las proyecciones fijadas están fijadas porque una persona tomó una decisión de presentación, y un runtime que la anulara en silencio sería justo lo que este formato existe para impedir: un comportamiento que nadie declaró. El propio respaldo de la Console —disponer todos los campos cuando no hay vista de formulario declarada— es la excepción que confirma la regla: solo se aplica donde nadie ha decidido nada todavía. + +Lo que el formato le da a quien revisa no es que las proyecciones fijadas sigan, sino que se pueden *encontrar*. Son metadatos en el mismo repositorio, nombran el campo y la puerta las enumera. La alternativa, en un código escrito a mano, es una lista de columnas en un componente React, un serializador en otro directorio, una etiqueta en un archivo JSON y una configuración de gráfico en un tercer sistema, sin nada que sepa que tratan del mismo campo. + +## Lo que esto no cubre + +Una medición sobre un ejemplo es una medición sobre un ejemplo. Cuatro cosas quedan fuera. + +**Las vistas ajustadas a mano siguen ajustadas a mano.** Cada vista de lista del ejemplo Todo declara sus columnas, y también cada vista del ejemplo CRM. Un campo nuevo llega a un grid fijado solo cuando alguien lo añade, y ese alguien suele ser el agente que añadió el campo, en el mismo PR. La prueba del nombre te dice que mires ahí; no mira por ti. + +**Las traducciones hay que escribirlas.** El runtime cae a la etiqueta declarada, así que una entrada `zh-CN` que falta muestra `Budget` en lugar de nada. Es el valor por defecto correcto y aun así un hueco que un usuario chino ve. Una traducción es una proyección fijada en el sentido más estricto: cada cadena que contiene es una decisión. + +**Los consumidores externos están fuera del stack.** La propia puerta lo dice: «a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view» (un consumidor puede vivir legítimamente fuera del stack: un cliente de API, un hook o un paquete que este stack no lleva, una vista creada en Studio). Un dashboard en otra herramienta que selecciona `estimated_hours` por nombre está fijado a ese campo y nada en el repositorio lo sabe. La prueba del nombre se le aplica; la enumeración no. + +**Los cambios semánticos no son cambios estructurales.** Añadir un campo es el caso fácil, y por eso se eligió para un antes y después limpio. Cambia en su lugar el *significado* de un campo —añade una opción `cancelled` a `status`, por ejemplo— y las superficies derivadas siguen con la misma fidelidad, pero la vista *Overdue Tasks* del ejemplo Todo y su métrica *Overdue Tasks* codifican ambas el vencimiento como `status != completed`, así que una tarea cancelada con la fecha de vencimiento pasada contaría como vencida en las dos. El campo sí declara esa definición una vez —`due_date` lleva `` dueLike: true, settledWhen: P`record.status == 'completed'` `` (`examples/app-todo/src/objects/task.object.ts`, líneas 73–74 en 18.3.0)—, pero la especificación marca ambas claves como solo de visualización, leídas por las celdas de fecha de la Console, así que el filtro de la vista y la métrica siguen deletreando la regla por su cuenta. Es una lectura de las definiciones de la vista, del dashboard y del objeto, no una medición, y es el tipo de consecuencia hacia la que te apunta la prueba del nombre —las tres nombran `status`— sin decidir por ti. + +## Apunta tu agente hacia ella + +La ejecución de arriba es reproducible desde el repositorio y tarda alrededor de un minuto una vez instalados los paquetes, lo que significa que también es algo que tu agente puede hacer por ti. Apunta un agente de código a un proyecto de ObjectStack —el scaffolder escribe un `AGENTS.md` e instala el paquete de skills, la app en ejecución sirve MCP en `/api/v1/mcp`, y [cómo configurarlo](/en/blog/how-to-point-your-agent-at-objectstack/) cubre el archivo de reglas, las skills y el servidor de herramientas— y cuando abra un cambio en un campo, pídele la tabla de la prueba del nombre de su propio diff: qué superficies siguieron, qué proyecciones nombran el campo y qué propone hacer con cada una. La línea `field-no-consumers` de la puerta es la primera fila de esa tabla, y el agente puede leerla antes que tú. + +Todo en este artículo se ejecutó sobre el stack open source. «¿Quieres el mismo ciclo alojado, en el navegador, sin instalar nada? Eso es ObjectOS, el entorno de ejecución comercial construido sobre ObjectStack.» ObjectOS funciona alojado en el navegador sin instalar nada (ObjectOS Cloud) o autogestionado en tu propia infraestructura (ObjectOS Enterprise). Tu ontología son metadatos ordinarios de ObjectStack. La exportación desde ObjectOS Cloud u ObjectOS Enterprise para ejecutarla en el runtime open source de ObjectStack está prevista. La prueba del nombre no cambia con el host; la definición que lee es el mismo archivo. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx new file mode 100644 index 00000000..60ce9b25 --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx @@ -0,0 +1,124 @@ +--- +title: "Les vues, les tableaux de bord et les apps sont des projections de l’ontologie : ce que cela change pour la revue" +description: "Ajoutez un champ à une app ObjectStack : la table, l’API REST, l’appel describe de MCP et le formulaire de la Console suivent seuls ; les vues, le dataset et la traduction épinglés non. Le test du nom dit lequel est lequel." +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontologie + - Ontologie métier exécutable + - Diff relisible + - Revue des métadonnées +--- + +Voici le diff entier. Cinq lignes, un champ, ajouté à l’objet `todo_task` de l’exemple Todo fourni dans le dépôt ObjectStack : + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +Démarrées sur le runtime ObjectStack, ces cinq lignes ont changé la table de la base de données, la lecture et l’écriture REST, l’endpoint de métadonnées, les réponses de `describe_object` et `query_records` de MCP et le formulaire de création de la Console : sept surfaces, dont aucune n’a été modifiée. Elles n’ont pas changé les trois vues de liste, le dataset du tableau de bord ni la traduction chinoise, parce que chacune nomme les champs par leur nom. Le premier groupe est dérivé de la définition ; le second y est épinglé. **Le test du nom** range n’importe quelle projection dans l’un des deux avant que vous l’ouvriez : *si elle nomme le champ, vous la relisez ; si elle ne nomme que l’objet, le runtime l’a déjà fait.* Le reste de cet article est la mesure derrière cette phrase, prise sur ObjectStack `18.3.0`, la version `latest` sur npm au moment de l’exécution, avec le code de l’exemple au commit `83d7e4ae` de cette version, et ce qu’elle change à la lecture d’un diff. + +## Où vit la définition d’une projection + +Trois endroits définissent déjà ce qu’est une projection, et cet article ne les répète pas ; l’essai [L’ontologie est le logiciel](/fr/blog/the-ontology-is-the-software/) trace la même ligne comme la première des deux frontières qu’il maintient sur le mot. [L’article de définition](/fr/blog/what-does-the-ontology-is-the-software-mean/) nomme les quatre projections et donne le test de suppression pour distinguer le noyau d’une projection. [La page ontologie](/en/ontology/) énonce la règle en une ligne : changez le noyau et les projections suivent ; retirez une projection et le métier qu’elle montrait est inchangé. L’[entrée du glossaire](/en/glossary/ontology-projection/) ajoute la conséquence pour un relecteur : un diff qui ne touche que des projections change la présentation, et un diff qui touche le noyau change l’autorité. + +Ce qu’aucun ne dit, c’est *comment* une projection suit. « Suivre » recouvre deux mécanismes différents, et un relecteur doit savoir lequel il a sous les yeux. C’est la question à laquelle répond l’exécution ci-dessous. + +## L’exécution : un champ, onze lectures + +Le point de départ est l’exemple Todo fourni (`examples/app-todo`), copié hors du dépôt au commit `83d7e4ae` (le tag de version `@objectstack/cli@18.3.0`) et lancé sur les paquets `18.3.0` publiés avec `os dev`. La base de données est un fichier SQLite local ; la clé d’API a été émise par REST depuis l’administrateur de développement amorcé. Chaque lecture a été prise deux fois : une fois sur la définition committée, une fois après le diff de cinq lignes ci-dessus et un redémarrage avec `--compile`. « Avant » et « après » sont les deux réponses. + +| Surface | Comment elle a été lue | Avant | Après | Suit ? | +| :--- | :--- | :--- | :--- | :--- | +| Table de base de données `todo_task` | `PRAGMA table_info` sur le fichier SQLite | 28 colonnes | 29 ; `budget float` ajoutée à la table existante | Dérivée | +| Lecture REST | `GET /api/v1/data/todo_task` | 28 clés par enregistrement | 29 clés | Dérivée | +| Écriture REST | `POST /api/v1/data/todo_task` avec `budget: 1200` | `400` — `Unknown field 'budget' on object 'todo_task'`, code `INVALID_FIELD` | `201`, enregistrement créé | Dérivée | +| Endpoint de métadonnées | `GET /api/v1/meta/object/todo_task` | 28 champs | 29 ; `{ label: 'Budget', type: 'currency', min: 0, … }` | Dérivée | +| MCP `describe_object` | `tools/call` sur `/api/v1/mcp` | 28 champs | 29 | Dérivée | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | Dérivée | +| Formulaire de création de la Console | `/_console/apps/com.example.todo/todo_task/new` | 16 saisies | 17 ; *Budget* entre *Actual Hours* et *Notes*, dans l’ordre de déclaration | Dérivée | +| Détail d’enregistrement de la Console | la page du nouvel enregistrement | — | `BUDGET 1,200.00` dans l’onglet *Overview* | Dérivée | +| Vue de liste de la Console *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 colonnes | 6 colonnes, inchangée | Épinglée | +| Traduction, `zh-CN` | `GET /api/v1/meta/object/todo_task` avec `Accept-Language: zh-CN` | `subject` → 主题 | `budget` → `Budget` (retombe sur l’anglais) | Épinglée | +| Dataset du tableau de bord `task_metrics` | la définition du dataset | 6 dimensions, 3 mesures, chacune nommant un champ | inchangé ; rien ne peut tracer `budget` tant qu’une mesure ne le nomme pas | Épinglée | + +Deux lectures n’ont pas bougé et ne sont pas des projections : le document OpenAPI liste toujours 17 chemins pour `todo_task`, sans schéma par champ, et le `tools/list` de MCP répond toujours les mêmes 12 outils (`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`). Tous deux décrivent des objets, pas des champs ; le champ apparaît dans leurs réponses, pas dans leur forme. + +![Le test du nom : au centre, le diff de cinq lignes ; à gauche, les surfaces qui ne nomment que l’objet et l’ont suivi sans modification, avec leurs lectures avant et après ; à droite, les projections qui nomment le champ et sont restées exactement telles qu’écrites ; en bas, l’avertissement de la porte de build elle-même : le nouveau champ n’a aucun consommateur](./naming-test-en.svg) + +Une lecture de plus est venue gratuitement. `os dev --compile` exécute les règles de temps d’écriture avant d’écrire l’artefact, et voici ce que la porte a affiché à propos du nouveau champ : + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer +``` + +`os explain field-no-consumers` définit un consommateur comme « a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it » (une colonne de vue, une colonne de grille inline, une section de formulaire, un binding de page, un nœud de workflow, un membre de dataset ou de cube, un widget de tableau de bord, une formule, une validation, un hook ou une action qui le nomme) ; le texte de la 18.3.0 compte en outre un groupe de champs déclaré et la clé de correspondance d’un seed ou d’un mapping d’import. Il le distingue d’un porteur, qui « names a field without reading it » (nomme un champ sans le lire), comme « a translation label, a seed value, an import-mapping target, a permission grant » (un libellé de traduction, une valeur de seed, une cible de mapping d’import, une attribution de permission). La liste de la porte est la colonne de droite du tableau. C’est le runtime qui vous dit, avant le démarrage, quelles projections sont épinglées à un champ, et qu’aucune n’a encore repris celui-ci. + +## Le test du nom + +Pourquoi sept surfaces suivent-elles et quatre restent-elles en place ? Pas parce que certaines sont plus importantes. À cause de ce que chacune nomme. + +La table, les routes REST, l’endpoint de métadonnées et les outils MCP sont générés à partir de l’objet. La synchronisation de schéma du driver SQL parcourt `obj.fields` et crée une colonne pour chaque entrée ; les routes de données sont enregistrées par objet et lisent les champs que l’objet possède ; `describe_object` transpose `Object.entries(def.fields)` dans sa réponse ; la Console, ne trouvant aucune vue de formulaire déclarée pour `todo_task`, dispose les propres champs de l’objet. Aucune ne détient une liste de noms de champs. Elles détiennent le nom de l’objet et demandent le reste à la définition à chaque appel, ce qui explique que le champ les ait toutes atteintes sans modification, et dans l’ordre de déclaration. + +Les vues de liste, le dataset et le paquet de traduction sont eux aussi des métadonnées écrites, et chacun épelle des noms de champs : `columns: [{ field: 'subject' }, { field: 'status' }, …]`, `measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`, `fields: { subject: { label: '主题' }, … }`. Une projection qui nomme le champ fait un choix à son sujet — cette colonne, ce libellé, cette agrégation — et le runtime honore ce choix à la lettre. Il n’ajoute pas ce que vous n’avez pas demandé. + +Voici donc le test, énoncé pour un relecteur : + +> **Le test du nom.** Pour chaque projection que le diff pourrait atteindre, demandez-vous si elle nomme le champ ou seulement l’objet. Si elle ne nomme que l’objet, elle est dérivée : le diff que vous lisez est tout le changement, et le runtime l’a déjà propagé. Si elle nomme le champ, elle est épinglée : elle n’a pas bougé, et c’est à vous de juger si elle aurait dû. + +Il se combine avec le test de suppression de l’article de définition. Le test de suppression range une définition dans le noyau ou dans une projection. Le test du nom range les projections qu’un changement du noyau peut atteindre en dérivées et épinglées. À eux deux, un relecteur sait, à partir du seul diff, ce que le runtime a fait et ce qui attend encore une décision. + +## Ce que cela change à la lecture du diff + +La conséquence pratique est un ordre de travail, et il est plus court que celui que suivent la plupart des équipes. + +**D’abord, la ligne de définition est la revue de chaque surface dérivée.** Il n’y a pas de migration de table à lire à côté, pas de sérialiseur, pas de contrôleur, pas de manifeste d’outils. Dans l’exécution ci-dessus, la colonne `budget` existe parce que le champ existe ; l’écriture REST accepte `budget` parce que le champ existe ; le `describe_object` de l’agent l’affiche parce que le champ existe. Si le champ est juste, elles sont justes, et si le champ est faux, elles sont fausses exactement de la même manière que lui. Relire les cinq lignes, c’était relire sept surfaces : c’est la phrase que le titre promettait, et ce que l’entrée du glossaire entend par « un diff qui touche le noyau change l’autorité ». + +**Ensuite, cherchez le nom du champ dans les projections épinglées, et attendez-vous à ne rien trouver.** Un champ ajouté n’apparaîtra dans aucune vue, aucun dataset ni aucune traduction, parce qu’aucun n’a été modifié. Ce n’est pas une omission que le runtime cache ; c’est l’avertissement que la porte a affiché. La question du relecteur est celle que pose la ligne `fix:` de la porte : ce champ doit-il avoir un consommateur, et lequel ? Pour `budget`, probablement une colonne dans *All Tasks* et un libellé `zh-CN` ; probablement pas encore une mesure de tableau de bord. Ce sont trois modifications petites et délibérées dans trois fichiers nommés, et chacune est aussi relisible que les cinq premières lignes. + +**Enfin, traitez une suppression comme un autre genre de changement.** Tout ce qui précède concerne un champ ajouté. La synchronisation de schéma du driver est uniquement additive : elle crée les tables manquantes et ajoute les colonnes manquantes, et elle ne modifie ni ne supprime jamais celles qui existent, si bien qu’un champ supprimé ou renommé laisse sa colonne derrière lui, comme une dérive que `os migrate` signale et qu’un humain réconcilie. Les surfaces dérivées suivent une suppression instantanément — l’enregistrement REST perd la clé, `describe_object` perd le champ — tandis que la table, non. Et chaque projection épinglée qui nomme encore le champ supprimé est désormais fausse, ce que la même porte vous signalera comme un binding pendant. Le test du nom s’applique toujours ; la liste des choses à regarder est simplement plus longue, et elle commence par la migration. + +Cet ordre est aussi la moitié manquante de la [checklist de revue en quatre passes](/en/blog/how-to-review-ai-generated-app-changes/), dont la quatrième passe est « champs et vues, en dernier ». Le test du nom dit quelles vues : celles qui nomment le champ. Les autres ont été relues en même temps que le champ. + +## Pour la défense de l’épinglage + +Il serait facile de lire la colonne de droite du tableau comme une limite, et la lecture honnête est l’inverse. + +Une grille qui ajouterait automatiquement chaque nouvelle colonne réorganiserait un écran chaque fois qu’un développeur ajoute un champ pour qu’un workflow y écrive. Un tableau de bord qui tracerait chaque champ numérique tracerait `recurrence_interval`. Une traduction qui inventerait des libellés serait une traduction que personne n’a écrite. Les projections épinglées sont épinglées parce qu’un humain a pris une décision de présentation, et un runtime qui la passerait outre en silence serait exactement ce que ce format existe pour empêcher : un comportement que personne n’a déclaré. Le repli de la Console elle-même — disposer tous les champs quand aucune vue de formulaire n’est déclarée — est l’exception qui confirme la règle : il ne s’applique que là où personne n’a encore rien décidé. + +Ce que le format donne au relecteur, ce n’est pas que les projections épinglées suivent, mais qu’on peut les *trouver*. Ce sont des métadonnées dans le même dépôt, elles nomment le champ, et la porte les énumère. L’alternative, dans une base de code écrite à la main, c’est une liste de colonnes dans un composant React, un sérialiseur dans un autre répertoire, un libellé dans un fichier JSON et une configuration de graphique dans un troisième système, sans rien qui sache qu’ils concernent le même champ. + +## Ce que cela ne couvre pas + +Une mesure sur un exemple est une mesure sur un exemple. Quatre choses restent en dehors. + +**Les vues ajustées à la main restent ajustées à la main.** Chaque vue de liste de l’exemple Todo déclare ses colonnes, et chaque vue de l’exemple CRM aussi. Un nouveau champ n’atteint une grille épinglée que si quelqu’un l’y ajoute, et ce quelqu’un est souvent l’agent qui a ajouté le champ, dans la même PR. Le test du nom vous dit de regarder là ; il ne regarde pas à votre place. + +**Les traductions doivent être écrites.** Le runtime retombe sur le libellé déclaré, si bien qu’une entrée `zh-CN` manquante affiche `Budget` plutôt que rien. C’est le bon défaut, et c’est quand même un trou qu’un utilisateur chinois voit. Une traduction est une projection épinglée au sens le plus strict : chaque chaîne qu’elle contient est une décision. + +**Les consommateurs externes sont hors de la stack.** La porte le dit elle-même : « a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view » (un consommateur peut légitimement vivre hors de la stack : un client d’API, un hook ou un paquet que cette stack ne porte pas, une vue créée dans Studio). Un tableau de bord dans un autre outil qui sélectionne `estimated_hours` par son nom est épinglé à ce champ, et rien dans le dépôt ne le sait. Le test du nom s’y applique ; l’énumération, non. + +**Les changements sémantiques ne sont pas des changements structurels.** Ajouter un champ est le cas facile, et c’est pourquoi il a été choisi pour un avant-après net. Changez plutôt le *sens* d’un champ — ajoutez par exemple une option `cancelled` à `status` — et les surfaces dérivées suivent tout aussi fidèlement, mais la vue *Overdue Tasks* de l’exemple Todo et sa métrique *Overdue Tasks* codent toutes deux le retard comme `status != completed`, si bien qu’une tâche annulée dont l’échéance est passée compterait comme en retard dans les deux. Le champ déclare bien cette définition une fois — `due_date` porte `` dueLike: true, settledWhen: P`record.status == 'completed'` `` (`examples/app-todo/src/objects/task.object.ts`, lignes 73–74 en 18.3.0) —, mais la spécification marque les deux clés comme servant uniquement à l’affichage, lues par les cellules de date de la Console, si bien que le filtre de la vue et la métrique énoncent encore la règle chacun de leur côté. C’est une lecture des définitions de la vue, du tableau de bord et de l’objet, pas une mesure, et c’est le genre de conséquence vers laquelle le test du nom vous oriente — toutes trois nomment `status` — sans décider pour vous. + +## Pointez votre agent dessus + +L’exécution ci-dessus est reproductible depuis le dépôt et prend environ une minute une fois les paquets installés, ce qui veut dire que votre agent peut aussi la faire pour vous. Pointez un agent de code sur un projet ObjectStack — le scaffolder écrit un `AGENTS.md` et installe le paquet de skills, l’app en marche sert MCP sur `/api/v1/mcp`, et [comment le configurer](/en/blog/how-to-point-your-agent-at-objectstack/) couvre le fichier de règles, les skills et le serveur d’outils — et quand il ouvre un changement sur un champ, demandez-lui le tableau du test du nom de son propre diff : quelles surfaces ont suivi, quelles projections nomment le champ et ce qu’il propose pour chacune. La ligne `field-no-consumers` de la porte est la première ligne de ce tableau, et l’agent peut la lire avant vous. + +Tout ce qui figure dans cet article a tourné sur la stack open source. « Vous voulez la même boucle, hébergée, dans le navigateur, sans rien installer ? C’est ObjectOS, l’environnement d’exécution commercial construit sur ObjectStack. » ObjectOS fonctionne hébergé dans le navigateur, sans rien installer (ObjectOS Cloud), ou autogéré sur votre propre infrastructure (ObjectOS Enterprise). Votre ontologie est faite de métadonnées ObjectStack ordinaires. Son export depuis ObjectOS Cloud ou ObjectOS Enterprise, pour l’exécuter sur le runtime open source ObjectStack, est prévu. Le test du nom ne change pas avec l’hôte ; la définition qu’il lit est le même fichier. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx new file mode 100644 index 00000000..2703b95f --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx @@ -0,0 +1,124 @@ +--- +title: "ビュー、ダッシュボード、アプリはオントロジーの投影である:それはレビューの何を変えるか" +description: "同梱の ObjectStack アプリに項目を一つ足すと、テーブル、REST API、MCP の describe 呼び出し、Console のフォームは自分で追従し、項目名を固定したビュー、データセット、翻訳は動かない。命名テストがどちらかを教える。" +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - オントロジー + - 実行可能なビジネスオントロジー + - レビューできる diff + - メタデータレビュー +--- + +これが diff の全部だ。五行、項目一つ。ObjectStack リポジトリに同梱されている Todo サンプルの `todo_task` オブジェクトに足したものである: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +ObjectStack ランタイムで起動すると、この五行はデータベースのテーブル、REST の読み取りと書き込み、メタデータエンドポイント、MCP の `describe_object` と `query_records` の応答、そして Console の新規作成フォームを変えた。七つの面で、どれ一つ編集していない。三つのリストビュー、ダッシュボードのデータセット、中国語の翻訳は変わらなかった。どれも項目を名前で指しているからだ。前者のグループは定義から導出され、後者は定義に固定されている。**命名テスト**は、どの投影でも開く前に二つのどちらかへ振り分ける:*項目の名前を書いているなら、あなたがレビューする。オブジェクトの名前しか書いていないなら、ランタイムがもう済ませている。* 本稿の残りは、この一文の裏にある計測だ。ObjectStack `18.3.0`——実行時点で npm の `latest` だったリリース——で、サンプルのソースはそのリリースのコミット `83d7e4ae` から取った。あわせて、それが diff の読み方の何を変えるかを書く。 + +## 投影の定義はどこにあるか + +投影とは何かは、すでに三か所で定義されていて、本稿はそれを繰り返さない。[『オントロジーがソフトウェアである』](/ja/blog/the-ontology-is-the-software/)というエッセイも、この語を使うたびに守る二つの境界の一つ目として、同じ線を引いている。[定義の記事](/ja/blog/what-does-the-ontology-is-the-software-mean/)は四つの投影を挙げ、中核と投影を見分ける削除テストを示す。[オントロジーのページ](/en/ontology/)は規則を一行で述べる:中核を変えれば投影は追従する。投影を取り除いても、それが示していたビジネスは変わらない。[用語集の項目](/en/glossary/ontology-projection/)はレビュアーにとっての帰結を足す:投影だけに触れる diff は見せ方を変え、中核に触れる diff は権限を変える。 + +どれも書いていないのは、投影が*どうやって*追従するかだ。「追従する」には二つの異なる仕組みが含まれていて、レビュアーは目の前にあるのがどちらなのかを知る必要がある。以下の実行が答えるのはその問いである。 + +## 実行:項目一つ、読み取り十一個 + +対象は同梱の Todo サンプル(`examples/app-todo`)だ。リポジトリのコミット `83d7e4ae`(`@objectstack/cli@18.3.0` のリリースタグ)からコピーし、公開済みの `18.3.0` パッケージを使って `os dev` で動かした。データベースはローカルの SQLite ファイルで、API キーはシードされた開発用管理者から REST で発行した。読み取りはすべて二回行った。一回はコミットされた定義のまま、もう一回は上の五行の diff を当てて `--compile` 付きで再起動したあとだ。「前」と「後」がその二つの答えである。 + +| 面 | 読み方 | 前 | 後 | 追従するか | +| :--- | :--- | :--- | :--- | :--- | +| データベースのテーブル `todo_task` | SQLite ファイルへの `PRAGMA table_info` | 28 列 | 29 列。既存のテーブルに `budget float` が追加 | 導出 | +| REST の読み取り | `GET /api/v1/data/todo_task` | レコードあたり 28 キー | 29 キー | 導出 | +| REST の書き込み | `budget: 1200` 付きの `POST /api/v1/data/todo_task` | `400`——`Unknown field 'budget' on object 'todo_task'`、コード `INVALID_FIELD` | `201`、レコード作成 | 導出 | +| メタデータエンドポイント | `GET /api/v1/meta/object/todo_task` | 28 項目 | 29 項目。`{ label: 'Budget', type: 'currency', min: 0, … }` | 導出 | +| MCP `describe_object` | `/api/v1/mcp` への `tools/call` | 28 項目 | 29 項目 | 導出 | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | 導出 | +| Console の新規作成フォーム | `/_console/apps/com.example.todo/todo_task/new` | 入力欄 16 | 17。*Budget* が宣言順どおり *Actual Hours* と *Notes* の間に | 導出 | +| Console のレコード詳細 | 新しいレコードのページ | — | *Overview* タブに `BUDGET 1,200.00` | 導出 | +| Console のリストビュー *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 列 | 6 列、変化なし | 固定 | +| 翻訳 `zh-CN` | `Accept-Language: zh-CN` 付きの `GET /api/v1/meta/object/todo_task` | `subject` → 主题 | `budget` → `Budget`(英語にフォールバック) | 固定 | +| ダッシュボードのデータセット `task_metrics` | データセットの定義 | 6 ディメンション、3 メジャー、どれも項目を名指し | 変化なし。メジャーが `budget` を名指すまで、何も `budget` をグラフにできない | 固定 | + +動かなかった読み取りが二つあり、どちらも投影ではない。OpenAPI ドキュメントは `todo_task` について相変わらず 17 本のパスを並べ、項目ごとのスキーマはない。MCP の `tools/list` も相変わらず同じ 12 個のツール(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)を返す。どちらも項目ではなくオブジェクトを記述している。項目は応答の中身には現れるが、その形には現れない。 + +![命名テスト:中央に五行の diff。左はオブジェクトの名前だけを書き、編集なしで追従した面と前後の読み取り。右は項目の名前を書き、書かれたとおりに留まった投影。下にはビルドゲート自身の警告——新しい項目には消費者がいない](./naming-test-en.svg) + +もう一つの読み取りはおまけで付いてきた。`os dev --compile` は成果物を書き出す前にオーサリング時ルールを走らせる。ゲートが新しい項目について出力したのはこれだ: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer +``` + +`os explain field-no-consumers` は消費者を "a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it"(それを名指すビュー列、インライングリッド列、フォームセクション、ページバインディング、フローノード、データセットまたはキューブのメンバー、ダッシュボードウィジェット、数式、検証、フック、アクション)と定義する。18.3.0 の文面はさらに、宣言された項目グループと、シードやインポートマッピングの照合キーも数に入れる。そして消費者を、"names a field without reading it"(項目を読まずに名指すだけ)の運び手から区別する。例は "a translation label, a seed value, an import-mapping target, a permission grant"(翻訳ラベル、シード値、インポートマッピングの宛先、権限の付与)だ。ゲートのこの一覧は、表の右側の列そのものだ。どの投影が項目に固定されているか、そしてそのどれもまだこの項目を拾っていないことを、ランタイムが起動前に教えている。 + +## 命名テスト + +なぜ七つの面が追従し、四つはその場に留まるのか。どれかがより重要だからではない。それぞれが何を名指しているかの違いだ。 + +テーブル、REST ルート、メタデータエンドポイント、MCP ツールはオブジェクトから生成される。SQL ドライバのスキーマ同期は `obj.fields` をたどり、エントリごとに列を作る。データルートはオブジェクトごとに登録され、オブジェクトが持つ項目をそのまま読む。`describe_object` は `Object.entries(def.fields)` を応答に写す。Console は `todo_task` にフォームビューが宣言されていないのを見て、オブジェクト自身の項目を並べる。どれも項目名のリストを持っていない。持っているのはオブジェクトの名前で、残りは呼ばれるたびに定義へ尋ねる。だから項目は編集なしで全部に届き、しかも宣言順に届いた。 + +リストビュー、データセット、翻訳バンドルもオーサリングされたメタデータであり、どれも項目名を書き出している:`columns: [{ field: 'subject' }, { field: 'status' }, …]`、`measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`、`fields: { subject: { label: '主题' }, … }`。項目を名指す投影は、その項目について選択をしている——この列、このラベル、この集計——そしてランタイムはその選択を正確に守る。頼んでいないものは足さない。 + +だから、レビュアー向けに言い直したテストはこうなる: + +> **命名テスト。** diff が届きうる投影の一つひとつについて、それが項目を名指しているのか、オブジェクトしか名指していないのかを問う。オブジェクトしか名指していなければ、それは導出されている。読んでいる diff が変更の全部で、ランタイムはもう伝播させた。項目を名指していれば、それは固定されている。動いていないし、動くべきだったかどうかはあなたが決める。 + +これは定義の記事の削除テストと組み合わさる。削除テストは定義を中核か投影かに振り分ける。命名テストは、中核の変更が届きうる投影を導出か固定かに振り分ける。二つがあれば、レビュアーは diff だけから、ランタイムが何をしたか、何がまだ判断を待っているかがわかる。 + +## diff の読み方の何が変わるか + +実際の帰結は作業の順番であり、多くのチームが従っている順番より短い。 + +**第一に、定義の行が、導出されるすべての面のレビューになる。** 横に並べて読むテーブルのマイグレーションも、シリアライザも、コントローラも、ツールのマニフェストもない。上の実行で `budget` 列があるのは項目があるからで、REST の書き込みが `budget` を受け付けるのも項目があるからで、エージェントの `describe_object` に出るのも項目があるからだ。項目が正しければそれらも正しく、項目が間違っていれば、それらも項目とまったく同じ形で間違う。五行をレビューすれば七つの面をレビューしたことになる。それがタイトルの約束した一文であり、用語集が「中核に触れる diff は権限を変える」と言うときの意味でもある。 + +**第二に、固定された投影の中で項目の名前を検索する——そして何も見つからないと予想しておく。** 足された項目は、どのビューにも、データセットにも、翻訳にも現れない。どれも編集されていないからだ。それはランタイムが隠した漏れではない。ゲートが出力した警告そのものだ。レビュアーの問いは、ゲートの `fix:` 行が投げかける問いと同じだ:この項目には消費者が要るか、要るならどれか。`budget` なら、おそらく *All Tasks* の列と `zh-CN` のラベル。ダッシュボードのメジャーはたぶんまだ要らない。三つの名前のあるファイルへの、三つの小さく意図的な編集で、どれも最初の五行と同じくらいレビューしやすい。 + +**第三に、削除は別の種類の変更として扱う。** ここまでの話はすべて項目の追加についてだ。ドライバのスキーマ同期は追加しかしない。欠けたテーブルを作り、欠けた列を足すが、既存の列を変更したり削除したりは決してしない。だから削除や改名された項目は列を置き去りにし、それは `os migrate` が報告して人が突き合わせるドリフトになる。導出される面は削除にすぐ追従する——REST のレコードからキーが消え、`describe_object` から項目が消える——が、テーブルは追従しない。そして削除された項目をまだ名指している固定の投影はすべて間違いになり、同じゲートがそれを宙に浮いたバインディングとして教えてくれる。命名テストはそのまま使える。見るべきものの一覧が長くなり、マイグレーションから始まるだけだ。 + +この順番は、[四段階のレビューチェックリスト](/en/blog/how-to-review-ai-generated-app-changes/)の欠けていた半分でもある。その四段階目は「項目とビューは最後に」だ。命名テストはどのビューかを言う:その項目を名指しているビューだ。残りは、項目をレビューしたときにレビュー済みである。 + +## 固定の弁護 + +表の右側の列を制約として読むのは簡単だが、誠実な読み方はその逆だ。 + +新しい列を自動で全部足すグリッドは、開発者がフローの書き込み先として項目を一つ足すたびに画面を並べ替えてしまう。数値項目を全部グラフにするダッシュボードは `recurrence_interval` までグラフにする。ラベルを勝手に作る翻訳は、誰も書いていない翻訳だ。固定された投影が固定されているのは、人が見せ方について決めたからで、それを黙って上書きするランタイムこそ、この形式が防ぐために存在するもの——誰も宣言していない振る舞い——になる。Console 自身のフォールバック——フォームビューが宣言されていなければ全項目を並べる——は、規則を裏付ける例外だ。まだ誰も何も決めていない場所でしか効かない。 + +この形式がレビュアーに与えるのは、固定された投影が追従することではなく、それが*見つけられる*ことだ。同じリポジトリのメタデータで、項目を名指していて、ゲートが列挙する。手書きのコードベースでの代わりは、React コンポーネントの中の列リスト、別のディレクトリのシリアライザ、JSON ファイルのラベル、第三のシステムのグラフ設定で、それらが同じ項目についてのものだと知っているものは何もない。 + +## 本稿が扱わないこと + +一つのサンプルでの計測は、一つのサンプルでの計測だ。四つのことがその外に残る。 + +**手で調整したビューは手で調整したままだ。** Todo サンプルのリストビューはすべて列を宣言しており、CRM サンプルのビューも同じだ。新しい項目が固定のグリッドに届くのは誰かが足したときだけで、その誰かは多くの場合、同じ PR で項目を足したエージェントだ。命名テストはそこを見ろと言う。見るのはテストではない。 + +**翻訳は書かれなければならない。** ランタイムは宣言されたラベルにフォールバックするので、`zh-CN` のエントリが欠けていれば何もないのではなく `Budget` と表示される。それは正しいデフォルトだが、中国語のユーザーには見える穴でもある。翻訳は最も厳密な意味で固定された投影だ。その中の文字列はどれも一つの決定である。 + +**外部の消費者はスタックの外にいる。** ゲート自身がそう言っている:"a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view"(消費者は正当にスタックの外にいることがある——API クライアント、このスタックが持たないフックやパッケージ、Studio で作られたビュー)。別のツールで `estimated_hours` を名前で選ぶダッシュボードはその項目に固定されているが、リポジトリの中の何もそれを知らない。命名テストはそれにも当てはまる。列挙は当てはまらない。 + +**意味の変更は構造の変更ではない。** 項目の追加は簡単なケースで、だからこそ前後をきれいに比べるために選んだ。代わりに項目の*意味*を変える——たとえば `status` に `cancelled` という選択肢を足す——と、導出される面は同じように忠実に追従する。だが Todo サンプルの *Overdue Tasks* ビューと *Overdue Tasks* 指標はどちらも期限切れを `status != completed` として書いているので、期限を過ぎたキャンセル済みのタスクは両方で期限切れに数えられる。項目自身はその定義を一度だけ宣言している——`due_date` は `` dueLike: true, settledWhen: P`record.status == 'completed'` `` を持つ(18.3.0 の `examples/app-todo/src/objects/task.object.ts` 73–74 行目)——が、仕様は両方のキーを表示専用と定めており、読むのは Console の日付セルだ。だからビューのフィルタと指標は、いまも規則をそれぞれ自分で書いている。これはビュー、ダッシュボード、オブジェクトの定義を読んだ結果であって計測ではない。そして命名テストが指し示す——三つとも `status` を名指している——が、代わりに決めてはくれない種類の帰結である。 + +## エージェントをここに向ける + +上の実行はリポジトリから再現でき、パッケージをインストールしたあとなら一分ほどで終わる。つまりエージェントが代わりにやれることでもある。コーディングエージェントを ObjectStack プロジェクトに向ける——スキャフォルダは `AGENTS.md` を書きスキルバンドルをインストールし、動いているアプリは `/api/v1/mcp` で MCP を提供し、[設定のしかた](/en/blog/how-to-point-your-agent-at-objectstack/)がルールファイル、スキル、ツールサーバーを扱っている。そしてエージェントが項目への変更を出してきたら、その diff 自身の命名テストの表を求める:どの面が追従したか、どの投影がその項目を名指しているか、それぞれについてどうするつもりか。ゲートの `field-no-consumers` の行がその表の最初の一行で、エージェントはあなたより先にそれを読める。 + +本稿のすべてはオープンソースのスタックで動いた。「同じループをホスト型で、ブラウザだけで、何もインストールせずに使いたい?それが ObjectOS — ObjectStack の上に構築された商用ランタイム環境です。」ObjectOS は、ブラウザ上でホストされインストール不要の ObjectOS Cloud としても、自社インフラでセルフマネージドの ObjectOS Enterprise としても動作します。オントロジーは通常の ObjectStack メタデータです。ObjectOS Cloud または ObjectOS Enterprise からエクスポートしてオープンソースの ObjectStack ランタイムで実行する機能は、計画中です。命名テストはホストによって変わらない。それが読む定義は同じファイルだ。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx new file mode 100644 index 00000000..6623557e --- /dev/null +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx @@ -0,0 +1,124 @@ +--- +title: "뷰, 대시보드, 앱은 온톨로지의 투영이다: 그것이 리뷰에서 바꾸는 것" +description: "ObjectStack 번들 앱에 필드 하나를 더하면 테이블, REST API, MCP describe 호출, Console 폼은 스스로 따라오고, 필드 이름을 고정한 뷰, 데이터셋, 번역은 그대로다. 명명 테스트가 어느 쪽인지 알려 준다." +author: ObjectStack Team +date: 2026-11-10 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - 온톨로지 + - 실행 가능한 비즈니스 온톨로지 + - 리뷰 가능한 diff + - 메타데이터 리뷰 +--- + +이것이 diff 전부다. 다섯 줄, 필드 하나. ObjectStack 저장소에 함께 들어 있는 Todo 예제의 `todo_task` 객체에 더한 것이다: + +```diff + actual_hours: Field.number({ + label: 'Actual Hours', + scale: 2, + min: 0, + }), ++ ++ budget: Field.currency({ ++ label: 'Budget', ++ min: 0, ++ }), +``` + +ObjectStack 런타임에서 띄우자 이 다섯 줄은 데이터베이스 테이블, REST 읽기와 쓰기, 메타데이터 엔드포인트, MCP의 `describe_object`와 `query_records` 응답, 그리고 Console의 새로 만들기 폼을 바꿨다. 일곱 개의 면이고, 그중 어느 것도 편집하지 않았다. 세 개의 목록 뷰, 대시보드의 데이터셋, 중국어 번역은 바뀌지 않았다. 각각이 필드를 이름으로 가리키기 때문이다. 앞의 무리는 정의에서 파생되고, 뒤의 무리는 정의에 고정되어 있다. **명명 테스트**는 어떤 투영이든 열어 보기 전에 둘 중 하나로 나눈다: *필드의 이름을 적고 있으면 당신이 리뷰한다. 객체의 이름만 적고 있으면 런타임이 이미 했다.* 이 글의 나머지는 그 문장 뒤에 있는 측정이다. ObjectStack `18.3.0`——실행 시점에 npm의 `latest`였던 릴리스——에서, 예제 소스는 그 릴리스의 커밋 `83d7e4ae`에서 가져왔다. 그리고 그것이 diff를 읽는 방식에서 무엇을 바꾸는지를 쓴다. + +## 투영의 정의는 어디에 있는가 + +투영이 무엇인지는 이미 세 곳에서 정의되어 있고, 이 글은 그것을 되풀이하지 않는다. [「온톨로지가 곧 소프트웨어다」](/ko/blog/the-ontology-is-the-software/)라는 에세이도, 이 말을 쓸 때마다 지키는 두 경계 중 첫 번째로 같은 선을 긋는다. [정의 글](/ko/blog/what-does-the-ontology-is-the-software-mean/)은 네 가지 투영을 꼽고, 핵심과 투영을 가르는 제거 테스트를 제시한다. [온톨로지 페이지](/en/ontology/)는 규칙을 한 줄로 말한다: 핵심을 바꾸면 투영이 따라온다. 투영을 제거해도 그것이 보여 주던 비즈니스는 바뀌지 않는다. [용어집 항목](/en/glossary/ontology-projection/)은 리뷰어에게 주는 귀결을 더한다: 투영만 건드리는 diff는 보여 주는 방식을 바꾸고, 핵심을 건드리는 diff는 권한을 바꾼다. + +그 어느 곳도 말하지 않는 것은 투영이 *어떻게* 따라오는가다. 「따라온다」에는 서로 다른 두 메커니즘이 들어 있고, 리뷰어는 눈앞의 것이 어느 쪽인지 알아야 한다. 아래의 실행이 답하는 것이 바로 그 질문이다. + +## 실행: 필드 하나, 판독 열한 개 + +대상은 함께 들어 있는 Todo 예제(`examples/app-todo`)다. 저장소의 커밋 `83d7e4ae`(`@objectstack/cli@18.3.0` 릴리스 태그)에서 복사해, 공개된 `18.3.0` 패키지로 `os dev`를 실행했다. 데이터베이스는 로컬 SQLite 파일이고, API 키는 시드된 개발용 관리자로 REST를 통해 발급했다. 모든 판독은 두 번 했다. 한 번은 커밋된 정의 그대로, 한 번은 위의 다섯 줄 diff를 적용하고 `--compile`로 재시작한 뒤다. 「전」과 「후」가 그 두 답이다. + +| 면 | 읽은 방법 | 전 | 후 | 따라오는가 | +| :--- | :--- | :--- | :--- | :--- | +| 데이터베이스 테이블 `todo_task` | SQLite 파일에 `PRAGMA table_info` | 28열 | 29열. 기존 테이블에 `budget float` 추가 | 파생 | +| REST 읽기 | `GET /api/v1/data/todo_task` | 레코드당 키 28개 | 키 29개 | 파생 | +| REST 쓰기 | `budget: 1200`을 담은 `POST /api/v1/data/todo_task` | `400` — `Unknown field 'budget' on object 'todo_task'`, 코드 `INVALID_FIELD` | `201`, 레코드 생성 | 파생 | +| 메타데이터 엔드포인트 | `GET /api/v1/meta/object/todo_task` | 필드 28개 | 29개. `{ label: 'Budget', type: 'currency', min: 0, … }` | 파생 | +| MCP `describe_object` | `/api/v1/mcp`에 `tools/call` | 필드 28개 | 29개 | 파생 | +| MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | 파생 | +| Console 새로 만들기 폼 | `/_console/apps/com.example.todo/todo_task/new` | 입력란 16개 | 17개. *Budget*이 선언 순서대로 *Actual Hours*와 *Notes* 사이에 | 파생 | +| Console 레코드 상세 | 새 레코드의 페이지 | — | *Overview* 탭에 `BUDGET 1,200.00` | 파생 | +| Console 목록 뷰 *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6열 | 6열, 변화 없음 | 고정 | +| 번역, `zh-CN` | `Accept-Language: zh-CN`을 붙인 `GET /api/v1/meta/object/todo_task` | `subject` → 主题 | `budget` → `Budget`(영어로 폴백) | 고정 | +| 대시보드 데이터셋 `task_metrics` | 데이터셋 정의 | 디멘션 6개, 메저 3개, 각각 필드 하나를 지목 | 변화 없음. 어떤 메저가 `budget`을 지목하기 전까지는 아무것도 그것을 차트로 그릴 수 없다 | 고정 | + +움직이지 않은 판독이 둘 있고, 둘 다 투영이 아니다. OpenAPI 문서는 `todo_task`에 대해 여전히 경로 17개를 나열하며 필드별 스키마는 없다. MCP `tools/list`도 여전히 같은 도구 12개(`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`)를 응답한다. 둘 다 필드가 아니라 객체를 기술한다. 필드는 응답의 내용 안에는 나타나지만 그 모양에는 나타나지 않는다. + +![명명 테스트: 가운데에 다섯 줄 diff. 왼쪽은 객체 이름만 적고 편집 없이 따라온 면과 전후 판독. 오른쪽은 필드 이름을 적고 작성된 그대로 머문 투영. 아래에는 빌드 게이트 자신의 경고 — 새 필드에는 소비자가 없다](./naming-test-en.svg) + +판독 하나는 덤으로 따라왔다. `os dev --compile`은 산출물을 쓰기 전에 작성 시점 규칙을 돌린다. 게이트가 새 필드에 대해 출력한 것은 이것이다: + +```text +⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) + fix: add it to a view column or a form section, or remove the declaration + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer +``` + +`os explain field-no-consumers`는 소비자를 "a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it"(그것을 지목하는 뷰 열, 인라인 그리드 열, 폼 섹션, 페이지 바인딩, 플로 노드, 데이터셋 또는 큐브 멤버, 대시보드 위젯, 수식, 검증, 훅, 액션)으로 정의한다. 18.3.0의 문구는 이어서 선언된 필드 그룹과, 시드나 가져오기 매핑의 매칭 키도 셈에 넣는다. 그리고 소비자를 "names a field without reading it"(필드를 읽지 않고 이름만 적는) 운반자와 구분한다. 예로 "a translation label, a seed value, an import-mapping target, a permission grant"(번역 라벨, 시드 값, 가져오기 매핑의 대상, 권한 부여)를 든다. 게이트의 이 목록이 곧 표의 오른쪽 열이다. 어떤 투영이 필드에 고정되어 있는지, 그리고 그중 어느 것도 아직 이 필드를 받아들이지 않았다는 것을 런타임이 부팅 전에 알려 주는 것이다. + +## 명명 테스트 + +왜 일곱 개의 면은 따라오고 네 개는 제자리에 있는가? 어떤 것이 더 중요해서가 아니다. 각각이 무엇을 지목하느냐 때문이다. + +테이블, REST 라우트, 메타데이터 엔드포인트, MCP 도구는 객체에서 생성된다. SQL 드라이버의 스키마 동기화는 `obj.fields`를 훑으며 항목마다 열을 만든다. 데이터 라우트는 객체별로 등록되고 객체가 가진 필드를 그대로 읽는다. `describe_object`는 `Object.entries(def.fields)`를 응답으로 옮긴다. Console은 `todo_task`에 폼 뷰가 선언되지 않은 것을 보고 객체 자신의 필드를 배치한다. 이 중 어느 것도 필드 이름의 목록을 들고 있지 않다. 들고 있는 것은 객체의 이름이고, 나머지는 호출될 때마다 정의에 묻는다. 그래서 필드는 편집 없이 모두에 닿았고, 선언 순서대로 닿았다. + +목록 뷰, 데이터셋, 번역 번들도 작성된 메타데이터이고, 각각 필드 이름을 적어 둔다: `columns: [{ field: 'subject' }, { field: 'status' }, …]`, `measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`, `fields: { subject: { label: '主题' }, … }`. 필드를 지목하는 투영은 그 필드에 대해 선택을 하고 있다 — 이 열, 이 라벨, 이 집계 — 그리고 런타임은 그 선택을 정확히 지킨다. 요청하지 않은 것은 더하지 않는다. + +그래서 리뷰어를 위해 말하면 테스트는 이렇다: + +> **명명 테스트.** diff가 닿을 수 있는 투영 하나하나에 대해, 그것이 필드를 지목하는지 객체만 지목하는지 묻는다. 객체만 지목한다면 파생된 것이다. 당신이 읽고 있는 diff가 변경의 전부이고, 런타임은 이미 그것을 전파했다. 필드를 지목한다면 고정된 것이다. 움직이지 않았고, 움직였어야 하는지는 당신이 판단한다. + +이것은 정의 글의 제거 테스트와 맞물린다. 제거 테스트는 정의를 핵심과 투영으로 나눈다. 명명 테스트는 핵심 변경이 닿을 수 있는 투영을 파생과 고정으로 나눈다. 둘이 있으면 리뷰어는 diff만 보고도 런타임이 무엇을 했고 무엇이 아직 결정을 기다리는지 안다. + +## diff를 읽는 방식에서 무엇이 바뀌는가 + +실제 귀결은 작업 순서이고, 대부분의 팀이 따르는 순서보다 짧다. + +**첫째, 정의 줄이 모든 파생 면에 대한 리뷰다.** 곁에 두고 읽을 테이블 마이그레이션도, 직렬화기도, 컨트롤러도, 도구 매니페스트도 없다. 위의 실행에서 `budget` 열이 있는 것은 필드가 있어서이고, REST 쓰기가 `budget`을 받아들이는 것도 필드가 있어서이고, 에이전트의 `describe_object`에 그것이 보이는 것도 필드가 있어서다. 필드가 맞으면 그것들도 맞고, 필드가 틀리면 그것들도 필드와 정확히 같은 방식으로 틀린다. 다섯 줄을 리뷰하면 일곱 개의 면을 리뷰한 것이다. 그것이 제목이 약속한 문장이고, 용어집 항목이 「핵심을 건드리는 diff는 권한을 바꾼다」고 할 때의 뜻이다. + +**둘째, 고정된 투영에서 필드 이름을 검색한다 — 그리고 아무것도 찾지 못할 것으로 예상한다.** 더해진 필드는 어떤 뷰에도, 데이터셋에도, 번역에도 나타나지 않는다. 그중 어느 것도 편집되지 않았기 때문이다. 그것은 런타임이 숨긴 누락이 아니라 게이트가 출력한 경고다. 리뷰어의 질문은 게이트의 `fix:` 줄이 던지는 질문과 같다: 이 필드에 소비자가 있어야 하는가, 있다면 무엇인가? `budget`이라면 아마 *All Tasks*의 열 하나와 `zh-CN` 라벨 하나다. 대시보드 메저는 아마 아직 아니다. 이름 붙은 세 파일에 대한 작고 의도적인 편집 세 개이고, 각각이 처음의 다섯 줄만큼 리뷰하기 쉽다. + +**셋째, 삭제는 다른 종류의 변경으로 다룬다.** 위의 모든 것은 필드를 추가하는 이야기다. 드라이버의 스키마 동기화는 추가만 한다. 빠진 테이블을 만들고 빠진 열을 더하지만, 기존 열을 바꾸거나 지우지는 않는다. 그래서 삭제되거나 이름이 바뀐 필드는 자기 열을 남겨 두고, 그것은 `os migrate`가 보고하고 사람이 맞춰 보는 드리프트가 된다. 파생 면은 삭제를 곧바로 따른다 — REST 레코드에서 키가 사라지고 `describe_object`에서 필드가 사라진다 — 하지만 테이블은 따르지 않는다. 그리고 삭제된 필드를 여전히 지목하는 고정 투영은 모두 틀린 것이 되며, 같은 게이트가 그것을 매달린 바인딩으로 알려 준다. 명명 테스트는 그대로 적용된다. 살펴볼 목록이 더 길어지고, 마이그레이션에서 시작할 뿐이다. + +이 순서는 [네 단계 리뷰 체크리스트](/en/blog/how-to-review-ai-generated-app-changes/)에서 빠져 있던 절반이기도 하다. 그 네 번째 단계는 「필드와 뷰는 마지막에」다. 명명 테스트는 어떤 뷰인지 말해 준다: 그 필드를 지목하는 뷰다. 나머지는 필드를 리뷰할 때 이미 리뷰되었다. + +## 고정을 변호하며 + +표의 오른쪽 열을 한계로 읽기는 쉽지만, 정직한 읽기는 그 반대다. + +새 열을 모두 자동으로 더하는 그리드는, 개발자가 플로의 쓰기 대상으로 필드 하나를 더할 때마다 화면을 다시 배치할 것이다. 숫자 필드를 모두 그리는 대시보드는 `recurrence_interval`까지 그릴 것이다. 라벨을 지어내는 번역은 아무도 쓰지 않은 번역이다. 고정된 투영이 고정된 것은 사람이 보여 주는 방식에 대해 결정했기 때문이고, 그것을 조용히 덮어쓰는 런타임이야말로 이 형식이 막으려고 존재하는 것 — 아무도 선언하지 않은 동작 — 이 된다. Console 자신의 폴백 — 폼 뷰가 선언되지 않으면 모든 필드를 배치한다 — 은 규칙을 증명하는 예외다. 아직 아무도 아무것도 결정하지 않은 곳에서만 작동한다. + +이 형식이 리뷰어에게 주는 것은 고정된 투영이 따라온다는 것이 아니라 그것을 *찾을 수 있다*는 것이다. 같은 저장소의 메타데이터이고, 필드를 지목하며, 게이트가 열거한다. 손으로 쓴 코드베이스에서의 대안은 React 컴포넌트 안의 열 목록, 다른 디렉터리의 직렬화기, JSON 파일의 라벨, 세 번째 시스템의 차트 설정이고, 그것들이 같은 필드에 관한 것임을 아는 것은 아무것도 없다. + +## 이 글이 다루지 않는 것 + +예제 하나에서의 측정은 예제 하나에서의 측정이다. 네 가지가 그 바깥에 남는다. + +**손으로 다듬은 뷰는 손으로 다듬은 그대로다.** Todo 예제의 모든 목록 뷰는 열을 선언하고, CRM 예제의 모든 뷰도 그렇다. 새 필드가 고정된 그리드에 닿는 것은 누군가 그것을 더할 때뿐이고, 그 누군가는 대개 같은 PR에서 필드를 더한 에이전트다. 명명 테스트는 거기를 보라고 말한다. 보는 일 자체를 하지는 않는다. + +**번역은 누군가 써야 한다.** 런타임은 선언된 라벨로 폴백하므로, `zh-CN` 항목이 빠져 있으면 빈칸이 아니라 `Budget`이 표시된다. 그것은 옳은 기본값이지만 중국어 사용자에게는 여전히 보이는 빈틈이다. 번역은 가장 엄격한 의미의 고정 투영이다. 그 안의 모든 문자열이 하나의 결정이다. + +**외부 소비자는 스택 바깥에 있다.** 게이트 스스로 그렇게 말한다: "a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view"(소비자는 정당하게 스택 바깥에 있을 수 있다 — API 클라이언트, 이 스택이 담고 있지 않은 훅이나 패키지, Studio에서 작성한 뷰). 다른 도구에서 `estimated_hours`를 이름으로 고르는 대시보드는 그 필드에 고정되어 있지만, 저장소 안의 어떤 것도 그것을 모른다. 명명 테스트는 거기에도 적용된다. 열거는 적용되지 않는다. + +**의미의 변경은 구조의 변경이 아니다.** 필드 추가는 쉬운 경우이고, 그래서 깔끔한 전후 비교를 위해 골랐다. 대신 필드의 *의미*를 바꿔 보자 — 이를테면 `status`에 `cancelled` 선택지를 더한다 — 그러면 파생 면은 똑같이 충실하게 따라오지만, Todo 예제의 *Overdue Tasks* 뷰와 *Overdue Tasks* 지표는 둘 다 기한 초과를 `status != completed`로 적어 두었으므로, 기한이 지난 취소된 작업은 두 곳 모두에서 기한 초과로 셈해진다. 필드 자신은 그 정의를 한 번 선언해 두었다 — `due_date`에는 `` dueLike: true, settledWhen: P`record.status == 'completed'` ``가 있다(18.3.0의 `examples/app-todo/src/objects/task.object.ts` 73–74행) — 하지만 사양은 두 키를 표시 전용으로 규정하고 그것을 읽는 것은 Console의 날짜 셀이다. 그래서 뷰의 필터와 지표는 여전히 규칙을 저마다 다시 적고 있다. 이것은 뷰, 대시보드, 객체 정의를 읽은 결과이지 측정이 아니며, 명명 테스트가 가리켜 주지만 — 셋 다 `status`를 지목한다 — 대신 결정해 주지는 않는 종류의 귀결이다. + +## 에이전트를 여기에 향하게 하라 + +위의 실행은 저장소에서 재현할 수 있고, 패키지를 설치한 뒤라면 1분쯤 걸린다. 즉 에이전트가 대신 해 줄 수 있는 일이기도 하다. 코딩 에이전트를 ObjectStack 프로젝트에 향하게 하라 — 스캐폴더는 `AGENTS.md`를 쓰고 스킬 번들을 설치하며, 실행 중인 앱은 `/api/v1/mcp`에서 MCP를 제공하고, [설정하는 방법](/en/blog/how-to-point-your-agent-at-objectstack/)이 규칙 파일, 스킬, 도구 서버를 다룬다. 그리고 에이전트가 필드에 대한 변경을 내놓으면, 그 diff 자신의 명명 테스트 표를 요구하라: 어떤 면이 따라왔는지, 어떤 투영이 그 필드를 지목하는지, 각각에 대해 무엇을 하려는지. 게이트의 `field-no-consumers` 줄이 그 표의 첫 행이고, 에이전트는 당신보다 먼저 그것을 읽을 수 있다. + +이 글의 모든 것은 오픈소스 스택에서 실행되었다. 「같은 루프를 호스팅으로, 브라우저에서, 아무것도 설치하지 않고 쓰고 싶다면? 그것이 ObjectOS, ObjectStack 위에 구축된 상용 런타임 환경입니다.」 ObjectOS는 설치 없이 브라우저에서 호스팅되는 ObjectOS Cloud로도, 자체 인프라에서 직접 운영하는 ObjectOS Enterprise로도 실행됩니다. 온톨로지는 일반적인 ObjectStack 메타데이터입니다. ObjectOS Cloud 또는 ObjectOS Enterprise에서 내보내 오픈소스 ObjectStack 런타임에서 실행하는 기능은 계획 중입니다. 명명 테스트는 호스트에 따라 바뀌지 않는다. 그것이 읽는 정의는 같은 파일이다. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx index 638e7ac6..00cef6e9 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx @@ -31,33 +31,33 @@ This is the whole diff. Five lines, one field, added to the `todo_task` object o + }), ``` -Booted on the ObjectStack runtime, those five lines changed the database table, the REST read and write, the metadata endpoint, the MCP `describe_object` and `query_records` answers and the Console's create form — seven surfaces, none of which was edited. They did not change the three list views, the dashboard's dataset or the Chinese translation, because each of those names fields by name. The first group is derived from the definition; the second is pinned to it. **The naming test** sorts any projection into one of the two before you open it: *if it names the field, you review it; if it names only the object, the runtime already did.* The rest of this post is the measurement behind that sentence, taken on ObjectStack `18.0.0-next.2` with the example's source at commit `43567a8f`, and what it changes about reading a diff. +Booted on the ObjectStack runtime, those five lines changed the database table, the REST read and write, the metadata endpoint, the MCP `describe_object` and `query_records` answers and the Console's create form — seven surfaces, none of which was edited. They did not change the three list views, the dashboard's dataset or the Chinese translation, because each of those names fields by name. The first group is derived from the definition; the second is pinned to it. **The naming test** sorts any projection into one of the two before you open it: *if it names the field, you review it; if it names only the object, the runtime already did.* The rest of this post is the measurement behind that sentence, taken on ObjectStack `18.3.0`, the `latest` release on npm when it was run, with the example's source at that release's commit `83d7e4ae`, and what it changes about reading a diff. ## Where the definition of a projection lives -Three places already define what a projection is, and this post does not restate them. [The definition post](/en/blog/what-does-the-ontology-is-the-software-mean/) names the four projections and gives the removal test for telling core from projection. [The ontology page](/en/ontology/) states the rule in one line: change the core and the projections follow; remove a projection and the business it showed is unchanged. The [glossary entry](/en/glossary/ontology-projection/) adds the consequence for a reviewer: a diff that touches only projections changes presentation, and a diff that touches the core changes authority. +Three places already define what a projection is, and this post does not restate them; the essay [The Ontology Is the Software](/en/blog/the-ontology-is-the-software/) draws the same line as the first of the two boundaries it keeps on the word. [The definition post](/en/blog/what-does-the-ontology-is-the-software-mean/) names the four projections and gives the removal test for telling core from projection. [The ontology page](/en/ontology/) states the rule in one line: change the core and the projections follow; remove a projection and the business it showed is unchanged. The [glossary entry](/en/glossary/ontology-projection/) adds the consequence for a reviewer: a diff that touches only projections changes presentation, and a diff that touches the core changes authority. What none of them says is *how* a projection follows. "Follow" covers two different mechanisms, and a reviewer needs to know which one is in front of them. That is the question the run below answers. ## The run: one field, eleven readings -The setup is the bundled Todo example (`examples/app-todo`), copied out of the repository at commit `43567a8f` and run on the published `18.0.0-next.2` packages with `os dev`. The database is the default SQLite file; the API key was minted over REST from the seeded dev admin. Every reading was taken twice: once on the committed definition, once after the five-line diff above and a restart with `--compile`. "Before" and "after" are the two answers. +The setup is the bundled Todo example (`examples/app-todo`), copied out of the repository at commit `83d7e4ae` (the `@objectstack/cli@18.3.0` release tag) and run on the published `18.3.0` packages with `os dev`. The database is a local SQLite file; the API key was minted over REST from the seeded dev admin. Every reading was taken twice: once on the committed definition, once after the five-line diff above and a restart with `--compile`. "Before" and "after" are the two answers. | Surface | How it was read | Before | After | Follows? | | :--- | :--- | :--- | :--- | :--- | -| Database table `todo_task` | `PRAGMA table_info` on the SQLite file | 27 columns | 28; `budget float` added to the existing table | Derived | -| REST read | `GET /api/v1/data/todo_task` | 27 keys per record | 28 keys | Derived | +| Database table `todo_task` | `PRAGMA table_info` on the SQLite file | 28 columns | 29; `budget float` added to the existing table | Derived | +| REST read | `GET /api/v1/data/todo_task` | 28 keys per record | 29 keys | Derived | | REST write | `POST /api/v1/data/todo_task` with `budget: 1200` | `400` — `Unknown field 'budget' on object 'todo_task'`, code `INVALID_FIELD` | `201`, record created | Derived | -| Metadata endpoint | `GET /api/v1/meta/object/todo_task` | 27 fields | 28; `{ label: 'Budget', type: 'currency', min: 0, … }` | Derived | -| MCP `describe_object` | `tools/call` on `/api/v1/mcp` | 19 fields | 20 | Derived | +| Metadata endpoint | `GET /api/v1/meta/object/todo_task` | 28 fields | 29; `{ label: 'Budget', type: 'currency', min: 0, … }` | Derived | +| MCP `describe_object` | `tools/call` on `/api/v1/mcp` | 28 fields | 29 | Derived | | MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | Derived | -| Console create form | `/_console/apps/todo_app/todo_task/new` | 16 inputs | 17; *Budget* between *Actual Hours* and *Notes*, in declaration order | Derived | -| Console record detail | the new record's page | — | `BUDGET 1,200.00` in *Details* | Derived | +| Console create form | `/_console/apps/com.example.todo/todo_task/new` | 16 inputs | 17; *Budget* between *Actual Hours* and *Notes*, in declaration order | Derived | +| Console record detail | the new record's page | — | `BUDGET 1,200.00` on the *Overview* tab | Derived | | Console list view *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 columns | 6 columns, unchanged | Pinned | | Translation, `zh-CN` | `GET /api/v1/meta/object/todo_task` with `Accept-Language: zh-CN` | `subject` → 主题 | `budget` → `Budget` (English falls through) | Pinned | | Dashboard dataset `task_metrics` | the dataset definition | 6 dimensions, 3 measures, each naming a field | unchanged; nothing can chart `budget` until a measure names it | Pinned | -Two readings did not move and are not projections: the OpenAPI document still lists 14 paths for `todo_task` with no per-field schema, and the MCP `tools/list` still answers the same 12 tools (`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`). Both describe objects, not fields; the field shows up inside their answers, not in their shape. +Two readings did not move and are not projections: the OpenAPI document still lists 17 paths for `todo_task` with no per-field schema, and the MCP `tools/list` still answers the same 12 tools (`list_objects`, `describe_object`, `validate_expression`, `query_records`, `aggregate_records`, `get_record`, `create_record`, `update_record`, `delete_record`, `list_actions`, `run_action`, `resume_run`). Both describe objects, not fields; the field shows up inside their answers, not in their shape. ![The naming test: one five-line diff in the middle; on the left the surfaces that name only the object and followed it without an edit, with their before and after readings; on the right the projections that name the field and stayed exactly as authored; at the foot, the build gate's own warning that the new field has no consumer](./naming-test-en.svg) @@ -66,10 +66,10 @@ One more reading came for free. `os dev --compile` runs the author-time rules be ```text ⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) fix: add it to a view column or a form section, or remove the declaration - rule: field-no-consumers at objects[0].fields.budget + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer ``` -`os explain field-no-consumers` defines a consumer as "a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it", and separates it from a carrier, which "names a field without reading it", such as "a translation label, a seed value, an import-mapping target, a permission grant". The gate's list is the right-hand column of the table. It is the runtime telling you, before boot, which projections are pinned to a field and that none of them has picked this one up yet. +`os explain field-no-consumers` defines a consumer as "a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it" (the 18.3.0 text goes on to count a declared field group and a seed or import-mapping match key), and separates it from a carrier, which "names a field without reading it", such as "a translation label, a seed value, an import-mapping target, a permission grant". The gate's list is the right-hand column of the table. It is the runtime telling you, before boot, which projections are pinned to a field and that none of them has picked this one up yet. ## The naming test @@ -115,10 +115,10 @@ A measurement on one example is a measurement on one example. Four things stay o **External consumers are outside the stack.** The gate says so itself: "a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view". A dashboard in another tool that selects `estimated_hours` by name is pinned to that field and nothing in the repository knows it. The naming test applies to it; the enumeration does not. -**Semantic changes are not structural changes.** Adding a field is the easy case, which is why it was chosen for a clean before-and-after. Change the *meaning* of a field instead — add a `cancelled` option to `status`, say — and the derived surfaces follow just as faithfully, but the Todo example's *Overdue Tasks* view and its *Overdue Tasks* metric both encode overdue as `status != completed`, so a cancelled task with a past due date would count as overdue on both. That is a reading of the view and dataset definitions, not a measurement, and it is the kind of consequence the naming test points you at — both projections name `status` — without deciding for you. +**Semantic changes are not structural changes.** Adding a field is the easy case, which is why it was chosen for a clean before-and-after. Change the *meaning* of a field instead — add a `cancelled` option to `status`, say — and the derived surfaces follow just as faithfully, but the Todo example's *Overdue Tasks* view and its *Overdue Tasks* metric both encode overdue as `status != completed`, so a cancelled task with a past due date would count as overdue on both. The field does declare that definition once — `due_date` carries `` dueLike: true, settledWhen: P`record.status == 'completed'` `` (`examples/app-todo/src/objects/task.object.ts`, lines 73–74 at 18.3.0) — but the spec marks both keys display-only, read by the Console's date cells, so the view filter and the metric still spell the rule out for themselves. That is a reading of the view, dashboard and object definitions, not a measurement, and it is the kind of consequence the naming test points you at — all three name `status` — without deciding for you. ## Point your agent at it The run above is reproducible from the repository and takes about a minute once the packages are installed, which means it is also something your agent can do for you. Point a coding agent at an ObjectStack project — the scaffolder writes an `AGENTS.md` and installs the skills bundle, the running app serves MCP at `/api/v1/mcp`, and [how to configure it](/en/blog/how-to-point-your-agent-at-objectstack/) covers the rule file, the skills and the tool server — and when it opens a change to a field, ask it for the naming-test table of its own diff: which surfaces followed, which projections name the field, and what it proposes to do about each. The gate's `field-no-consumers` line is the first row of that table, and the agent can read it before you do. -Everything in this post ran on the open-source stack. "Want the same loop hosted, in the browser, nothing to install? That's ObjectOS, the commercial runtime environment built on ObjectStack." ObjectOS runs hosted in the browser with nothing to install (ObjectOS Cloud) or self-managed on your own infrastructure (ObjectOS Enterprise), and on either edition you can export your ontology and run it on the open-source ObjectStack runtime. The naming test does not change with the host; the definition it reads is the same file. +Everything in this post ran on the open-source stack. "Want the same loop hosted, in the browser, nothing to install? That's ObjectOS, the commercial runtime environment built on ObjectStack." ObjectOS runs hosted in the browser with nothing to install (ObjectOS Cloud) or self-managed on your own infrastructure (ObjectOS Enterprise). Your ontology is ordinary ObjectStack metadata. Exporting it from ObjectOS Cloud or ObjectOS Enterprise to run on the open-source ObjectStack runtime is planned. The naming test does not change with the host; the definition it reads is the same file. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx index 64890875..9d4c82ec 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx @@ -31,33 +31,33 @@ tags: + }), ``` -在 ObjectStack 运行时上启动之后,这五行改动了数据库表、REST 的读与写、元数据接口、MCP 的 `describe_object` 与 `query_records` 应答,以及控制台的新建表单——七个面,没有一个被编辑过。它们没有改动三个列表视图、仪表盘的数据集和中文翻译,因为这三样东西都逐个写明了字段名。前一组是从定义推导出来的,后一组是钉在定义上的。**命名测试**在你打开任何一个投影之前就能把它归到两组之一:它写明了这个字段,你就审它;它只写明了对象,运行时已经替你审过了。本文余下的部分,是这句话背后的测量——在 ObjectStack `18.0.0-next.2` 上、示例源码取自提交 `43567a8f`——以及它对读一份 diff 这件事改变了什么。 +在 ObjectStack 运行时上启动之后,这五行改动了数据库表、REST 的读与写、元数据接口、MCP 的 `describe_object` 与 `query_records` 应答,以及控制台的新建表单——七个面,没有一个被编辑过。它们没有改动三个列表视图、仪表盘的数据集和中文翻译,因为这三样东西都逐个写明了字段名。前一组是从定义推导出来的,后一组是钉在定义上的。**命名测试**在你打开任何一个投影之前就能把它归到两组之一:它写明了这个字段,你就审它;它只写明了对象,运行时已经替你审过了。本文余下的部分,是这句话背后的测量——在 ObjectStack `18.3.0`(运行时 npm 上的 `latest` 版本)上、示例源码取自该版本的发布提交 `83d7e4ae`——以及它对读一份 diff 这件事改变了什么。 ## 投影的定义住在哪里 -投影是什么,已经有三处定义,本文不重复。[定义那篇](/zh-Hans/blog/what-does-the-ontology-is-the-software-mean/)点名了四种投影,并给出区分核心与投影的删除测试。[本体页](/zh-Hans/ontology/)用一句话说明规则:改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。[术语表条目](/zh-Hans/glossary/ontology-projection/)补上了对审阅者的推论:只碰投影的 diff 改的是呈现,碰到核心的 diff 改的是权限与事实。 +投影是什么,已经有三处定义,本文不重复;[《本体即软件》](/zh-Hans/blog/the-ontology-is-the-software/)这篇长文也划了同一条线,那是它每次用这个词都要声明的两条边界中的第一条。[定义那篇](/zh-Hans/blog/what-does-the-ontology-is-the-software-mean/)点名了四种投影,并给出区分核心与投影的删除测试。[本体页](/zh-Hans/ontology/)用一句话说明规则:改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。[术语表条目](/zh-Hans/glossary/ontology-projection/)补上了对审阅者的推论:只碰投影的 diff 改的是呈现,碰到核心的 diff 改的是权限与事实。 它们都没说的是,投影是*怎么*跟着变的。「跟着变」底下藏着两种不同的机制,审阅者得知道眼前的是哪一种。下面这次运行回答的就是这个问题。 ## 这次运行:一个字段,十一个读数 -对象是自带的 Todo 示例(`examples/app-todo`),从仓库提交 `43567a8f` 复制出来,用已发布的 `18.0.0-next.2` 包以 `os dev` 启动。数据库是默认的 SQLite 文件;API key 用种子化的开发管理员通过 REST 签发。每个读数取两次:一次在提交原样的定义上,一次在上面那五行 diff 之后、带 `--compile` 重启之后。「之前」和「之后」就是这两次的答案。 +对象是自带的 Todo 示例(`examples/app-todo`),从仓库提交 `83d7e4ae`(`@objectstack/cli@18.3.0` 的发布标签)复制出来,用已发布的 `18.3.0` 包以 `os dev` 启动。数据库是一个本地 SQLite 文件;API key 用种子化的开发管理员通过 REST 签发。每个读数取两次:一次在提交原样的定义上,一次在上面那五行 diff 之后、带 `--compile` 重启之后。「之前」和「之后」就是这两次的答案。 | 面 | 怎么读的 | 之前 | 之后 | 跟着变? | | :--- | :--- | :--- | :--- | :--- | -| 数据库表 `todo_task` | 对 SQLite 文件执行 `PRAGMA table_info` | 27 列 | 28 列;`budget float` 加到了已有的表上 | 推导 | -| REST 读 | `GET /api/v1/data/todo_task` | 每条记录 27 个键 | 28 个键 | 推导 | +| 数据库表 `todo_task` | 对 SQLite 文件执行 `PRAGMA table_info` | 28 列 | 29 列;`budget float` 加到了已有的表上 | 推导 | +| REST 读 | `GET /api/v1/data/todo_task` | 每条记录 28 个键 | 29 个键 | 推导 | | REST 写 | `POST /api/v1/data/todo_task`,带 `budget: 1200` | `400`——`Unknown field 'budget' on object 'todo_task'`,错误码 `INVALID_FIELD` | `201`,记录已创建 | 推导 | -| 元数据接口 | `GET /api/v1/meta/object/todo_task` | 27 个字段 | 28 个;`{ label: 'Budget', type: 'currency', min: 0, … }` | 推导 | -| MCP `describe_object` | 对 `/api/v1/mcp` 发 `tools/call` | 19 个字段 | 20 个 | 推导 | +| 元数据接口 | `GET /api/v1/meta/object/todo_task` | 28 个字段 | 29 个;`{ label: 'Budget', type: 'currency', min: 0, … }` | 推导 | +| MCP `describe_object` | 对 `/api/v1/mcp` 发 `tools/call` | 28 个字段 | 29 个 | 推导 | | MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | 推导 | -| 控制台新建表单 | `/_console/apps/todo_app/todo_task/new` | 16 个输入项 | 17 个;*Budget* 按声明顺序出现在 *Actual Hours* 和 *Notes* 之间 | 推导 | -| 控制台记录详情 | 新记录的页面 | — | *Details* 区显示 `BUDGET 1,200.00` | 推导 | +| 控制台新建表单 | `/_console/apps/com.example.todo/todo_task/new` | 16 个输入项 | 17 个;*Budget* 按声明顺序出现在 *Actual Hours* 和 *Notes* 之间 | 推导 | +| 控制台记录详情 | 新记录的页面 | — | *Overview* 标签页显示 `BUDGET 1,200.00` | 推导 | | 控制台列表视图 *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 列 | 6 列,没有变化 | 钉死 | | 翻译,`zh-CN` | 带 `Accept-Language: zh-CN` 的 `GET /api/v1/meta/object/todo_task` | `subject` → 主题 | `budget` → `Budget`(回落到英文) | 钉死 | | 仪表盘数据集 `task_metrics` | 数据集定义本身 | 6 个维度、3 个度量,每个都点名一个字段 | 没有变化;在某个度量点名 `budget` 之前,没有任何图表能画它 | 钉死 | -另有两个读数没有变化,而它们也不是投影:OpenAPI 文档仍然为 `todo_task` 列出 14 条路径,没有逐字段的 schema;MCP 的 `tools/list` 仍然应答同样的 12 个工具(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)。两者描述的都是对象,不是字段;字段出现在它们的应答里,而不是它们的形状里。 +另有两个读数没有变化,而它们也不是投影:OpenAPI 文档仍然为 `todo_task` 列出 17 条路径,没有逐字段的 schema;MCP 的 `tools/list` 仍然应答同样的 12 个工具(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)。两者描述的都是对象,不是字段;字段出现在它们的应答里,而不是它们的形状里。 ![命名测试:中间是那份五行 diff;左边是只写明对象、未经编辑就跟上了定义的面,附前后读数;右边是写明了字段名、原样未动的投影;底部是构建门禁自己打出的警告——新字段没有任何消费者](./naming-test.svg) @@ -66,10 +66,10 @@ tags: ```text ⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) fix: add it to a view column or a form section, or remove the declaration - rule: field-no-consumers at objects[0].fields.budget + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer ``` -`os explain field-no-consumers` 把「消费者」定义为「a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it」,并把它和「载体」区分开——载体「names a field without reading it」,例如「a translation label, a seed value, an import-mapping target, a permission grant」。门禁的这份清单,正是上表的右半栏。这是运行时在启动之前就告诉你:哪些投影是钉在字段上的,而其中还没有任何一个接住这个新字段。 +`os explain field-no-consumers` 把「消费者」定义为「a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it」(18.3.0 的原文接着还把声明的字段分组、以及按某字段匹配的种子数据或导入映射算进去),并把它和「载体」区分开——载体「names a field without reading it」,例如「a translation label, a seed value, an import-mapping target, a permission grant」。门禁的这份清单,正是上表的右半栏。这是运行时在启动之前就告诉你:哪些投影是钉在字段上的,而其中还没有任何一个接住这个新字段。 ## 命名测试 @@ -115,10 +115,10 @@ tags: **外部消费者在栈之外。** 门禁自己就这么说:「a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view」。另一个工具里按名字选取 `estimated_hours` 的仪表盘,钉在那个字段上,而仓库里没有任何东西知道它。命名测试对它适用;枚举对它不适用。 -**语义改动不是结构改动。** 加字段是最简单的情形,所以才选它来做一次干净的前后对照。换成改一个字段的*含义*——比如给 `status` 加一个 `cancelled` 选项——推导面会同样忠实地跟上,但 Todo 示例的 *Overdue Tasks* 视图和它的 *Overdue Tasks* 指标都把「逾期」编码成 `status != completed`,于是一条截止日期已过的已取消任务,在两处都会被算作逾期。这是对视图和数据集定义的一次阅读,不是一次测量,而它正是命名测试会把你指过去——两个投影都写明了 `status`——却不替你拍板的那类后果。 +**语义改动不是结构改动。** 加字段是最简单的情形,所以才选它来做一次干净的前后对照。换成改一个字段的*含义*——比如给 `status` 加一个 `cancelled` 选项——推导面会同样忠实地跟上,但 Todo 示例的 *Overdue Tasks* 视图和它的 *Overdue Tasks* 指标都把「逾期」编码成 `status != completed`,于是一条截止日期已过的已取消任务,在两处都会被算作逾期。字段本身确实把这个定义声明了一次——`due_date` 上写着 `` dueLike: true, settledWhen: P`record.status == 'completed'` ``(18.3.0 的 `examples/app-todo/src/objects/task.object.ts` 第 73–74 行)——但 spec 把这两个键都标为只用于显示,由控制台的日期单元格读取,所以视图的过滤条件和那个指标仍然各自把规则写了一遍。这是对视图、仪表盘和对象定义的一次阅读,不是一次测量,而它正是命名测试会把你指过去——三处都写明了 `status`——却不替你拍板的那类后果。 ## 把你的 Agent 指向它 上面这次运行可以从仓库复现,包装好之后大约一分钟,这意味着它也是你的 Agent 可以替你做的事。把一个编码 Agent 指向一个 ObjectStack 项目——脚手架会写出 `AGENTS.md` 并安装技能包,运行中的应用在 `/api/v1/mcp` 提供 MCP,[如何配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆盖了规则文件、技能和工具服务器——当它开出一个改字段的变更时,向它要它自己这份 diff 的命名测试表:哪些面跟上了,哪些投影写明了这个字段,它打算对每一个怎么办。门禁那行 `field-no-consumers` 是这张表的第一行,而 Agent 可以比你先读到它。 -本文的一切都跑在开源栈上。「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise);无论哪个版本,你都可以导出本体,拿到开源的 ObjectStack 运行时上运行。命名测试不随宿主而变;它读的那份定义,是同一个文件。 +本文的一切都跑在开源栈上。「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise)。你的本体是普通的 ObjectStack 元数据。从 ObjectOS Cloud 或 ObjectOS Enterprise 导出本体、在开源的 ObjectStack 运行时上运行,目前尚在计划中。命名测试不随宿主而变;它读的那份定义,是同一个文件。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx index 5bfa952e..a8ed916e 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx @@ -14,7 +14,7 @@ tags: - 本體 - 可執行業務本體 - 可審閱的 diff - - 後設資料審閱 + - 中繼資料審閱 --- 下面就是整份 diff。五行,一個欄位,加在 ObjectStack 倉庫自帶的 Todo 示例的 `todo_task` 物件上: @@ -32,33 +32,33 @@ tags: + }), ``` -在 ObjectStack 執行時上啟動之後,這五行改動了資料庫表、REST 的讀與寫、後設資料介面、MCP 的 `describe_object` 與 `query_records` 應答,以及控制台的新建表單——七個面,沒有一個被編輯過。它們沒有改動三個列表檢視、儀表盤的資料集和中文翻譯,因為這三樣東西都逐個寫明瞭欄位名。前一組是從定義推匯出來的,後一組是釘在定義上的。**命名測試**在你開啟任何一個投影之前就能把它歸到兩組之一:它寫明瞭這個欄位,你就審它;它只寫明瞭物件,執行時已經替你審過了。本文餘下的部分,是這句話背後的測量——在 ObjectStack `18.0.0-next.2` 上、示例原始碼取自提交 `43567a8f`——以及它對讀一份 diff 這件事改變了什麼。 +在 ObjectStack 執行環境上啟動之後,這五行改動了資料庫表、REST 的讀與寫、中繼資料介面、MCP 的 `describe_object` 與 `query_records` 應答,以及控制台的新建表單——七個面,沒有一個被編輯過。它們沒有改動三個列表檢視、儀表盤的資料集和中文翻譯,因為這三樣東西都逐個寫明瞭欄位名。前一組是從定義推匯出來的,後一組是釘在定義上的。**命名測試**在你開啟任何一個投影之前就能把它歸到兩組之一:它寫明瞭這個欄位,你就審它;它只寫明瞭物件,執行環境已經替你審過了。本文餘下的部分,是這句話背後的測量——在 ObjectStack `18.3.0`(執行環境 npm 上的 `latest` 版本)上、示例原始碼取自該版本的釋出提交 `83d7e4ae`——以及它對讀一份 diff 這件事改變了什麼。 ## 投影的定義住在哪裡 -投影是什麼,已經有三處定義,本文不重複。[定義那篇](/zh-Hant/blog/what-does-the-ontology-is-the-software-mean/)點名了四種投影,並給出區分核心與投影的刪除測試。[本體頁](/zh-Hant/ontology/)用一句話說明規則:改動核心,投影跟著變;刪掉一個投影,它所展示的業務不變。[術語表條目](/zh-Hant/glossary/ontology-projection/)補上了對審閱者的推論:只碰投影的 diff 改的是呈現,碰到核心的 diff 改的是權限與事實。 +投影是什麼,已經有三處定義,本文不重複;[《本體即軟體》](/zh-Hant/blog/the-ontology-is-the-software/)這篇長文也劃了同一條線,那是它每次用這個詞都要宣告的兩條邊界中的第一條。[定義那篇](/zh-Hant/blog/what-does-the-ontology-is-the-software-mean/)點名了四種投影,並給出區分核心與投影的刪除測試。[本體頁](/zh-Hant/ontology/)用一句話說明規則:改動核心,投影跟著變;刪掉一個投影,它所展示的業務不變。[術語表條目](/zh-Hant/glossary/ontology-projection/)補上了對審閱者的推論:只碰投影的 diff 改的是呈現,碰到核心的 diff 改的是權限與事實。 它們都沒說的是,投影是*怎麼*跟著變的。「跟著變」底下藏著兩種不同的機制,審閱者得知道眼前的是哪一種。下面這次執行回答的就是這個問題。 ## 這次執行:一個欄位,十一個讀數 -物件是自帶的 Todo 示例(`examples/app-todo`),從倉庫提交 `43567a8f` 複製出來,用已釋出的 `18.0.0-next.2` 包以 `os dev` 啟動。資料庫是預設的 SQLite 檔案;API key 用種子化的開發管理員通過 REST 簽發。每個讀數取兩次:一次在提交原樣的定義上,一次在上面那五行 diff 之後、帶 `--compile` 重啟之後。「之前」和「之後」就是這兩次的答案。 +物件是自帶的 Todo 示例(`examples/app-todo`),從倉庫提交 `83d7e4ae`(`@objectstack/cli@18.3.0` 的釋出標籤)複製出來,用已釋出的 `18.3.0` 包以 `os dev` 啟動。資料庫是一個本地 SQLite 檔案;API key 用種子化的開發管理員通過 REST 簽發。每個讀數取兩次:一次在提交原樣的定義上,一次在上面那五行 diff 之後、帶 `--compile` 重啟之後。「之前」和「之後」就是這兩次的答案。 | 面 | 怎麼讀的 | 之前 | 之後 | 跟著變? | | :--- | :--- | :--- | :--- | :--- | -| 資料庫表 `todo_task` | 對 SQLite 檔案執行 `PRAGMA table_info` | 27 列 | 28 列;`budget float` 加到了已有的表上 | 推導 | -| REST 讀 | `GET /api/v1/data/todo_task` | 每條記錄 27 個鍵 | 28 個鍵 | 推導 | +| 資料庫表 `todo_task` | 對 SQLite 檔案執行 `PRAGMA table_info` | 28 列 | 29 列;`budget float` 加到了已有的表上 | 推導 | +| REST 讀 | `GET /api/v1/data/todo_task` | 每條記錄 28 個鍵 | 29 個鍵 | 推導 | | REST 寫 | `POST /api/v1/data/todo_task`,帶 `budget: 1200` | `400`——`Unknown field 'budget' on object 'todo_task'`,錯誤碼 `INVALID_FIELD` | `201`,記錄已建立 | 推導 | -| 後設資料介面 | `GET /api/v1/meta/object/todo_task` | 27 個欄位 | 28 個;`{ label: 'Budget', type: 'currency', min: 0, … }` | 推導 | -| MCP `describe_object` | 對 `/api/v1/mcp` 發 `tools/call` | 19 個欄位 | 20 個 | 推導 | +| 中繼資料介面 | `GET /api/v1/meta/object/todo_task` | 28 個欄位 | 29 個;`{ label: 'Budget', type: 'currency', min: 0, … }` | 推導 | +| MCP `describe_object` | 對 `/api/v1/mcp` 發 `tools/call` | 28 個欄位 | 29 個 | 推導 | | MCP `query_records` | `where: { budget: { $gte: 1000 } }, fields: ['subject', 'budget']` | — | `[["Renew the domain", 1200]]` | 推導 | -| 控制台新建表單 | `/_console/apps/todo_app/todo_task/new` | 16 個輸入項 | 17 個;*Budget* 按宣告順序出現在 *Actual Hours* 和 *Notes* 之間 | 推導 | -| 控制台記錄詳情 | 新記錄的頁面 | — | *Details* 區顯示 `BUDGET 1,200.00` | 推導 | +| 控制台新建表單 | `/_console/apps/com.example.todo/todo_task/new` | 16 個輸入項 | 17 個;*Budget* 按宣告順序出現在 *Actual Hours* 和 *Notes* 之間 | 推導 | +| 控制台記錄詳情 | 新記錄的頁面 | — | *Overview* 標籤頁顯示 `BUDGET 1,200.00` | 推導 | | 控制台列表檢視 *All Tasks* | `GET /api/v1/meta/view` → `todo_task.default` | 6 列 | 6 列,沒有變化 | 釘死 | | 翻譯,`zh-CN` | 帶 `Accept-Language: zh-CN` 的 `GET /api/v1/meta/object/todo_task` | `subject` → 主題 | `budget` → `Budget`(回落到英文) | 釘死 | | 儀表盤資料集 `task_metrics` | 資料集定義本身 | 6 個維度、3 個度量,每個都點名一個欄位 | 沒有變化;在某個度量點名 `budget` 之前,沒有任何圖表能畫它 | 釘死 | -另有兩個讀數沒有變化,而它們也不是投影:OpenAPI 文件仍然為 `todo_task` 列出 14 條路徑,沒有逐欄位的 schema;MCP 的 `tools/list` 仍然應答同樣的 12 個工具(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)。兩者描述的都是物件,不是欄位;欄位出現在它們的應答裡,而不是它們的形狀裡。 +另有兩個讀數沒有變化,而它們也不是投影:OpenAPI 文件仍然為 `todo_task` 列出 17 條路徑,沒有逐欄位的 schema;MCP 的 `tools/list` 仍然應答同樣的 12 個工具(`list_objects`、`describe_object`、`validate_expression`、`query_records`、`aggregate_records`、`get_record`、`create_record`、`update_record`、`delete_record`、`list_actions`、`run_action`、`resume_run`)。兩者描述的都是物件,不是欄位;欄位出現在它們的應答裡,而不是它們的形狀裡。 ![命名測試:中間是那份五行 diff;左邊是隻寫明物件、未經編輯就跟上了定義的面,附前後讀數;右邊是寫明瞭欄位名、原樣未動的投影;底部是構建門禁自己打出的警告——新欄位沒有任何消費者](./naming-test.svg) @@ -67,24 +67,24 @@ tags: ```text ⚠ object "todo_task" · field "budget": declared, but nothing in this stack displays or reads it (inert) fix: add it to a view column or a form section, or remove the declaration - rule: field-no-consumers at objects[0].fields.budget + rule: field-no-consumers at objects[0].fields.budget — `os explain field-no-consumers` for what counts as a consumer ``` -`os explain field-no-consumers` 把「消費者」定義為「a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it」,並把它和「載體」區分開——載體「names a field without reading it」,例如「a translation label, a seed value, an import-mapping target, a permission grant」。門禁的這份清單,正是上表的右半欄。這是執行時在啟動之前就告訴你:哪些投影是釘在欄位上的,而其中還沒有任何一個接住這個新欄位。 +`os explain field-no-consumers` 把「消費者」定義為「a view column, an inline grid column, a form section, a page binding, a flow node, a dataset or cube member, a dashboard widget, a formula, a validation, a hook or an action that names it」(18.3.0 的原文接著還把宣告的欄位分組、以及按某欄位匹配的種子資料或匯入對映算進去),並把它和「載體」區分開——載體「names a field without reading it」,例如「a translation label, a seed value, an import-mapping target, a permission grant」。門禁的這份清單,正是上表的右半欄。這是執行環境在啟動之前就告訴你:哪些投影是釘在欄位上的,而其中還沒有任何一個接住這個新欄位。 ## 命名測試 為什麼七個面跟著變、四個面原地不動?不是因為有的更重要。是因為它們各自寫明瞭什麼。 -資料表、REST 路由、後設資料介面和 MCP 工具都是從物件生成的。SQL 驅動的結構同步遍歷 `obj.fields`,為每一項建一列;資料路由按物件註冊,讀物件當下有的全部欄位;`describe_object` 把 `Object.entries(def.fields)` 對映成它的應答;控制台發現 `todo_task` 沒有宣告表單檢視,就按物件自己的欄位排版。這些地方沒有一處持有一份欄位名清單。它們持有的是物件名,每次呼叫時向定義要其餘的一切——所以欄位不經編輯就到達了它們全部,而且是按宣告順序到達的。 +資料表、REST 路由、中繼資料介面和 MCP 工具都是從物件生成的。SQL 驅動的結構同步遍歷 `obj.fields`,為每一項建一列;資料路由按物件註冊,讀物件當下有的全部欄位;`describe_object` 把 `Object.entries(def.fields)` 對映成它的應答;控制台發現 `todo_task` 沒有宣告表單檢視,就按物件自己的欄位排版。這些地方沒有一處持有一份欄位名清單。它們持有的是物件名,每次呼叫時向定義要其餘的一切——所以欄位不經編輯就到達了它們全部,而且是按宣告順序到達的。 -列表檢視、資料集和翻譯包同樣是編寫出來的後設資料,而且每一個都逐字寫明欄位名:`columns: [{ field: 'subject' }, { field: 'status' }, …]`、`measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`、`fields: { subject: { label: '主題' }, … }`。寫明瞭欄位的投影是在對這個欄位做一個選擇——這一列、這個標籤、這個聚合——執行時會原封不動地兌現這個選擇。你沒要的,它不會替你加。 +列表檢視、資料集和翻譯包同樣是編寫出來的中繼資料,而且每一個都逐字寫明欄位名:`columns: [{ field: 'subject' }, { field: 'status' }, …]`、`measures: [{ name: 'est_hours', field: 'estimated_hours', … }]`、`fields: { subject: { label: '主題' }, … }`。寫明瞭欄位的投影是在對這個欄位做一個選擇——這一列、這個標籤、這個聚合——執行環境會原封不動地兌現這個選擇。你沒要的,它不會替你加。 於是,把測試說給審閱者聽: -> **命名測試。** 對這份 diff 可能波及的每一個投影,問一句:它寫明的是這個欄位,還是隻有物件?只寫明物件的,是推匯出來的:你正在讀的 diff 就是全部改動,執行時已經把它傳播到位。寫明瞭欄位的,是釘死的:它沒有動,該不該動,由你判斷。 +> **命名測試。** 對這份 diff 可能波及的每一個投影,問一句:它寫明的是這個欄位,還是隻有物件?只寫明物件的,是推匯出來的:你正在讀的 diff 就是全部改動,執行環境已經把它傳播到位。寫明瞭欄位的,是釘死的:它沒有動,該不該動,由你判斷。 -它和定義那篇的刪除測試是配套的。刪除測試把一份定義歸為核心或投影;命名測試把一次核心改動能波及的投影歸為推導或釘死。兩條合在一起,審閱者光憑 diff 就知道執行時做了什麼、還有什麼在等一個決定。 +它和定義那篇的刪除測試是配套的。刪除測試把一份定義歸為核心或投影;命名測試把一次核心改動能波及的投影歸為推導或釘死。兩條合在一起,審閱者光憑 diff 就知道執行環境做了什麼、還有什麼在等一個決定。 ## 這對讀 diff 改變了什麼 @@ -92,7 +92,7 @@ tags: **第一,定義那幾行,就是對所有推導面的審閱。** 沒有要陪著讀的建表遷移,沒有序列化器,沒有控制器,沒有工具清單。在上面的執行裡,`budget` 列存在是因為欄位存在;REST 寫入接受 `budget` 是因為欄位存在;Agent 的 `describe_object` 顯示它是因為欄位存在。欄位對,它們就對;欄位錯,它們就以和欄位一模一樣的方式錯。審五行審掉了七個面——這正是標題承諾的那句話,也正是術語表條目所謂「碰到核心的 diff 改的是權限與事實」的意思。 -**第二,在釘死的投影裡搜這個欄位的名字——並且預期什麼都搜不到。** 新加的欄位不會出現在任何檢視、資料集或翻譯裡,因為它們一個都沒被編輯過。這不是執行時藏起來的遺漏,而是門禁打印出來的那條警告。審閱者要問的,正是門禁 `fix:` 那一行提出的問題:這個欄位該不該有一個消費者,是哪一個?對 `budget` 來說,大機率是 *All Tasks* 上的一列和一個 `zh-CN` 標籤;大機率暫時不是一個儀表盤度量。那是對三個具名檔案的三處小而有意的編輯,每一處都和最初那五行一樣可審。 +**第二,在釘死的投影裡搜這個欄位的名字——並且預期什麼都搜不到。** 新加的欄位不會出現在任何檢視、資料集或翻譯裡,因為它們一個都沒被編輯過。這不是執行環境藏起來的遺漏,而是門禁打印出來的那條警告。審閱者要問的,正是門禁 `fix:` 那一行提出的問題:這個欄位該不該有一個消費者,是哪一個?對 `budget` 來說,大機率是 *All Tasks* 上的一列和一個 `zh-CN` 標籤;大機率暫時不是一個儀表盤度量。那是對三個具名檔案的三處小而有意的編輯,每一處都和最初那五行一樣可審。 **第三,把刪除當成另一種改動。** 上面說的全是加欄位。驅動的結構同步只做加法:它建缺失的表、加缺失的列,從不修改或刪除已有的列,所以一個被刪掉或改名的欄位會把它的列留在原地,成為 `os migrate` 報告出來、由人來對賬的漂移。推導面會立刻跟上刪除——REST 記錄丟掉那個鍵,`describe_object` 丟掉那個欄位——資料表卻不會。而每一個仍然寫明被刪欄位的釘死投影現在都錯了,同一道門禁會把它當作懸空繫結報出來。命名測試照樣適用;只是要看的清單更長,而且從遷移開始。 @@ -102,9 +102,9 @@ tags: 把上表的右半欄讀成一種侷限很容易,而誠實的讀法恰恰相反。 -一張會自動把所有新列加進來的網格,會在每次開發者為了讓某條流程有地方寫而加個欄位時,把整個頁面重排一遍。一張會把所有數值欄位都畫出來的儀表盤,會把 `recurrence_interval` 畫出來。一份會自己發明標籤的翻譯,是一份沒人寫過的翻譯。釘死的投影之所以釘死,是因為有人做過一個呈現上的決定;一個悄悄推翻這個決定的執行時,正是這整套格式要防止的那件事——沒有任何人宣告過的行為。控制台自己的回退——沒宣告表單檢視時就把所有欄位排出來——恰好是證明這條規則的例外:它只在還沒有任何人做過決定的地方生效。 +一張會自動把所有新列加進來的網格,會在每次開發者為了讓某條流程有地方寫而加個欄位時,把整個頁面重排一遍。一張會把所有數值欄位都畫出來的儀表盤,會把 `recurrence_interval` 畫出來。一份會自己發明標籤的翻譯,是一份沒人寫過的翻譯。釘死的投影之所以釘死,是因為有人做過一個呈現上的決定;一個悄悄推翻這個決定的執行環境,正是這整套格式要防止的那件事——沒有任何人宣告過的行為。控制台自己的回退——沒宣告表單檢視時就把所有欄位排出來——恰好是證明這條規則的例外:它只在還沒有任何人做過決定的地方生效。 -這套格式給審閱者的,不是釘死的投影會跟著變,而是它們*找得到*。它們是同一個倉庫裡的後設資料,它們寫明瞭欄位名,門禁能把它們枚舉出來。另一種情況——一份手寫的程式碼庫——是某個 React 元件裡的一份列清單、另一個目錄裡的一個序列化器、一個 JSON 檔案裡的一個標籤、第三套系統裡的一份圖表配置,而沒有任何東西知道它們說的是同一個欄位。 +這套格式給審閱者的,不是釘死的投影會跟著變,而是它們*找得到*。它們是同一個倉庫裡的中繼資料,它們寫明瞭欄位名,門禁能把它們枚舉出來。另一種情況——一份手寫的程式碼庫——是某個 React 元件裡的一份列清單、另一個目錄裡的一個序列化器、一個 JSON 檔案裡的一個標籤、第三套系統裡的一份圖表配置,而沒有任何東西知道它們說的是同一個欄位。 ## 本文不覆蓋的 @@ -112,14 +112,14 @@ tags: **手工調過的檢視仍然是手工的。** Todo 示例裡每個列表檢視都聲明瞭列,CRM 示例裡每個檢視也是。新欄位到達一張釘死的網格,只能靠有人把它加進去——而這個「有人」常常就是加這個欄位的那個 Agent,在同一個 PR 裡。命名測試告訴你往那裡看;它不替你看。 -**翻譯必須有人寫。** 執行時會回落到宣告的標籤,所以缺一條 `zh-CN` 會渲染成 `Budget`,而不是空白。這是對的預設值,但仍然是中文使用者看得見的缺口。翻譯是最嚴格意義上的釘死投影:它裡面的每一個字串都是一個決定。 +**翻譯必須有人寫。** 執行環境會回落到宣告的標籤,所以缺一條 `zh-CN` 會渲染成 `Budget`,而不是空白。這是對的預設值,但仍然是中文使用者看得見的缺口。翻譯是最嚴格意義上的釘死投影:它裡面的每一個字串都是一個決定。 **外部消費者在棧之外。** 門禁自己就這麼說:「a consumer can legitimately live outside the stack — an API client, a hook or package this stack does not carry, a Studio-authored view」。另一個工具裡按名字選取 `estimated_hours` 的儀表盤,釘在那個欄位上,而倉庫裡沒有任何東西知道它。命名測試對它適用;列舉對它不適用。 -**語義改動不是結構改動。** 加欄位是最簡單的情形,所以才選它來做一次乾淨的前後對照。換成改一個欄位的*含義*——比如給 `status` 加一個 `cancelled` 選項——推導面會同樣忠實地跟上,但 Todo 示例的 *Overdue Tasks* 檢視和它的 *Overdue Tasks* 指標都把「逾期」編碼成 `status != completed`,於是一條截止日期已過的已取消任務,在兩處都會被算作逾期。這是對檢視和資料集定義的一次閱讀,不是一次測量,而它正是命名測試會把你指過去——兩個投影都寫明瞭 `status`——卻不替你拍板的那類後果。 +**語義改動不是結構改動。** 加欄位是最簡單的情形,所以才選它來做一次乾淨的前後對照。換成改一個欄位的*含義*——比如給 `status` 加一個 `cancelled` 選項——推導面會同樣忠實地跟上,但 Todo 示例的 *Overdue Tasks* 檢視和它的 *Overdue Tasks* 指標都把「逾期」編碼成 `status != completed`,於是一條截止日期已過的已取消任務,在兩處都會被算作逾期。欄位本身確實把這個定義聲明瞭一次——`due_date` 上寫著 `` dueLike: true, settledWhen: P`record.status == 'completed'` ``(18.3.0 的 `examples/app-todo/src/objects/task.object.ts` 第 73–74 行)——但 spec 把這兩個鍵都標為只用於顯示,由控制台的日期單元格讀取,所以檢視的過濾條件和那個指標仍然各自把規則寫了一遍。這是對檢視、儀表盤和物件定義的一次閱讀,不是一次測量,而它正是命名測試會把你指過去——三處都寫明瞭 `status`——卻不替你拍板的那類後果。 ## 把你的 Agent 指向它 上面這次執行可以從倉庫復現,包裝好之後大約一分鐘,這意味著它也是你的 Agent 可以替你做的事。把一個編碼 Agent 指向一個 ObjectStack 專案——腳手架會寫出 `AGENTS.md` 並安裝技能包,執行中的應用在 `/api/v1/mcp` 提供 MCP,[如何配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆蓋了規則檔案、技能和工具伺服器——當它開出一個改欄位的變更時,向它要它自己這份 diff 的命名測試表:哪些面跟上了,哪些投影寫明瞭這個欄位,它打算對每一個怎麼辦。門禁那行 `field-no-consumers` 是這張表的第一行,而 Agent 可以比你先讀到它。 -本文的一切都跑在開源棧上。「想把同一個迴圈託管起來、在瀏覽器裡用、什麼都不用安裝?那就是 ObjectOS——建立在 ObjectStack 之上的商業執行環境。」ObjectOS 既可託管在瀏覽器中、無需安裝(ObjectOS Cloud),也可自管部署在你自己的基礎設施上(ObjectOS Enterprise);無論哪個版本,你都可以匯出本體,拿到開源的 ObjectStack 執行時上執行。命名測試不隨宿主而變;它讀的那份定義,是同一個檔案。 +本文的一切都跑在開源棧上。「想把同一個迴圈託管起來、在瀏覽器裡用、什麼都不用安裝?那就是 ObjectOS——建立在 ObjectStack 之上的商業執行環境。」ObjectOS 既可託管在瀏覽器中、無需安裝(ObjectOS Cloud),也可自管部署在你自己的基礎設施上(ObjectOS Enterprise)。你的本體是普通的 ObjectStack 中繼資料。從 ObjectOS Cloud 或 ObjectOS Enterprise 匯出本體、在開源的 ObjectStack 執行環境上執行,目前尚在計劃中。命名測試不隨宿主而變;它讀的那份定義,是同一個檔案。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg index 8066cb8c..2071b614 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test-en.svg @@ -1,4 +1,4 @@ - + diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg index c350e053..95a88397 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/naming-test.svg @@ -1,4 +1,4 @@ - + From 156848ab2eab7eb2bea6a79e3981f738cb2aebbd Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 6 Nov 2026 02:53:23 +0000 Subject: [PATCH 5/5] content(blog): projections post quotes the current T4, not the retired ObjectOS line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The closing paragraph in all eight locales quoted the retired "same loop hosted … commercial runtime environment" T4, which src/lib/positioning.ts says must not be reintroduced. It now quotes the current T4 verbatim (T4_STANDALONE for en, each locale's positioning t4 elsewhere), drops the hosted/self-managed sentence that restated the old quote, and keeps the E1 "export is planned" sentence. zh-Hant regenerated with pnpm gen:zh-hant. Co-Authored-By: Claude --- .../index.de.mdx | 2 +- .../index.es.mdx | 2 +- .../index.fr.mdx | 2 +- .../index.ja.mdx | 2 +- .../index.ko.mdx | 2 +- .../index.mdx | 2 +- .../index.zh-Hans.mdx | 2 +- .../index.zh-Hant.mdx | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx index bc2aa2ab..a776dcff 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.de.mdx @@ -121,4 +121,4 @@ Eine Messung an einem Beispiel ist eine Messung an einem Beispiel. Vier Dinge bl Der Lauf oben ist aus dem Repository reproduzierbar und dauert etwa eine Minute, sobald die Pakete installiert sind, was heißt, dass auch Ihr Agent ihn für Sie erledigen kann. Richten Sie einen Coding-Agent auf ein ObjectStack-Projekt: Der Scaffolder schreibt eine `AGENTS.md` und installiert das Skills-Bündel, die laufende App stellt MCP unter `/api/v1/mcp` bereit, und [wie man ihn konfiguriert](/en/blog/how-to-point-your-agent-at-objectstack/) behandelt die Regeldatei, die Skills und den Tool-Server. Wenn er eine Änderung an einem Feld vorlegt, verlangen Sie von ihm die Namenstest-Tabelle seines eigenen Diffs: welche Oberflächen gefolgt sind, welche Projektionen das Feld nennen und was er mit jeder vorhat. Die `field-no-consumers`-Zeile des Gates ist die erste Zeile dieser Tabelle, und der Agent kann sie lesen, bevor Sie es tun. -Alles in diesem Beitrag lief auf dem Open-Source-Stack. „Dieselbe Schleife gehostet, im Browser, ohne Installation? Das ist ObjectOS, die kommerzielle Laufzeitumgebung auf Basis von ObjectStack.“ ObjectOS läuft gehostet im Browser ohne Installation (ObjectOS Cloud) oder selbst verwaltet auf Ihrer eigenen Infrastruktur (ObjectOS Enterprise). Ihre Ontologie besteht aus gewöhnlichen ObjectStack-Metadaten. Der Export aus ObjectOS Cloud oder ObjectOS Enterprise zur Ausführung auf der quelloffenen ObjectStack Runtime ist geplant. Der Namenstest ändert sich nicht mit dem Host; die Definition, die er liest, ist dieselbe Datei. +Alles in diesem Beitrag lief auf dem Open-Source-Stack. „Zum Ausführen liefert das ObjectStack-Repository ObjectOS Community — das Image ghcr.io/objectstack-ai/objectstack, os start und die Console, kostenlos und selbst gehostet — während ObjectOS Cloud die gehostete Edition ist und ObjectOS Enterprise die selbst verwaltete, lizenzierte.“ Ihre Ontologie besteht aus gewöhnlichen ObjectStack-Metadaten. Der Export aus ObjectOS Cloud oder ObjectOS Enterprise zur Ausführung auf der quelloffenen ObjectStack Runtime ist geplant. Der Namenstest ändert sich nicht mit dem Host; die Definition, die er liest, ist dieselbe Datei. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx index b7d36c9d..229880e3 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.es.mdx @@ -121,4 +121,4 @@ Una medición sobre un ejemplo es una medición sobre un ejemplo. Cuatro cosas q La ejecución de arriba es reproducible desde el repositorio y tarda alrededor de un minuto una vez instalados los paquetes, lo que significa que también es algo que tu agente puede hacer por ti. Apunta un agente de código a un proyecto de ObjectStack —el scaffolder escribe un `AGENTS.md` e instala el paquete de skills, la app en ejecución sirve MCP en `/api/v1/mcp`, y [cómo configurarlo](/en/blog/how-to-point-your-agent-at-objectstack/) cubre el archivo de reglas, las skills y el servidor de herramientas— y cuando abra un cambio en un campo, pídele la tabla de la prueba del nombre de su propio diff: qué superficies siguieron, qué proyecciones nombran el campo y qué propone hacer con cada una. La línea `field-no-consumers` de la puerta es la primera fila de esa tabla, y el agente puede leerla antes que tú. -Todo en este artículo se ejecutó sobre el stack open source. «¿Quieres el mismo ciclo alojado, en el navegador, sin instalar nada? Eso es ObjectOS, el entorno de ejecución comercial construido sobre ObjectStack.» ObjectOS funciona alojado en el navegador sin instalar nada (ObjectOS Cloud) o autogestionado en tu propia infraestructura (ObjectOS Enterprise). Tu ontología son metadatos ordinarios de ObjectStack. La exportación desde ObjectOS Cloud u ObjectOS Enterprise para ejecutarla en el runtime open source de ObjectStack está prevista. La prueba del nombre no cambia con el host; la definición que lee es el mismo archivo. +Todo en este artículo se ejecutó sobre el stack open source. «Para ejecutarlo, el repositorio de ObjectStack incluye ObjectOS Community — la imagen ghcr.io/objectstack-ai/objectstack, os start y la Console, gratuita y autoalojada — mientras que ObjectOS Cloud es la edición alojada y ObjectOS Enterprise la autogestionada, con licencia.» Tu ontología son metadatos ordinarios de ObjectStack. La exportación desde ObjectOS Cloud u ObjectOS Enterprise para ejecutarla en el runtime open source de ObjectStack está prevista. La prueba del nombre no cambia con el host; la definición que lee es el mismo archivo. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx index 60ce9b25..65c4dbee 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.fr.mdx @@ -121,4 +121,4 @@ Une mesure sur un exemple est une mesure sur un exemple. Quatre choses restent e L’exécution ci-dessus est reproductible depuis le dépôt et prend environ une minute une fois les paquets installés, ce qui veut dire que votre agent peut aussi la faire pour vous. Pointez un agent de code sur un projet ObjectStack — le scaffolder écrit un `AGENTS.md` et installe le paquet de skills, l’app en marche sert MCP sur `/api/v1/mcp`, et [comment le configurer](/en/blog/how-to-point-your-agent-at-objectstack/) couvre le fichier de règles, les skills et le serveur d’outils — et quand il ouvre un changement sur un champ, demandez-lui le tableau du test du nom de son propre diff : quelles surfaces ont suivi, quelles projections nomment le champ et ce qu’il propose pour chacune. La ligne `field-no-consumers` de la porte est la première ligne de ce tableau, et l’agent peut la lire avant vous. -Tout ce qui figure dans cet article a tourné sur la stack open source. « Vous voulez la même boucle, hébergée, dans le navigateur, sans rien installer ? C’est ObjectOS, l’environnement d’exécution commercial construit sur ObjectStack. » ObjectOS fonctionne hébergé dans le navigateur, sans rien installer (ObjectOS Cloud), ou autogéré sur votre propre infrastructure (ObjectOS Enterprise). Votre ontologie est faite de métadonnées ObjectStack ordinaires. Son export depuis ObjectOS Cloud ou ObjectOS Enterprise, pour l’exécuter sur le runtime open source ObjectStack, est prévu. Le test du nom ne change pas avec l’hôte ; la définition qu’il lit est le même fichier. +Tout ce qui figure dans cet article a tourné sur la stack open source. « Pour l’exécuter, le dépôt ObjectStack fournit ObjectOS Community — l’image ghcr.io/objectstack-ai/objectstack, os start et la Console, gratuite et auto-hébergée — tandis qu’ObjectOS Cloud est l’édition hébergée et ObjectOS Enterprise l’édition autogérée, sous licence. » Votre ontologie est faite de métadonnées ObjectStack ordinaires. Son export depuis ObjectOS Cloud ou ObjectOS Enterprise, pour l’exécuter sur le runtime open source ObjectStack, est prévu. Le test du nom ne change pas avec l’hôte ; la définition qu’il lit est le même fichier. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx index 2703b95f..fd4d75a5 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ja.mdx @@ -121,4 +121,4 @@ ObjectStack ランタイムで起動すると、この五行はデータベー 上の実行はリポジトリから再現でき、パッケージをインストールしたあとなら一分ほどで終わる。つまりエージェントが代わりにやれることでもある。コーディングエージェントを ObjectStack プロジェクトに向ける——スキャフォルダは `AGENTS.md` を書きスキルバンドルをインストールし、動いているアプリは `/api/v1/mcp` で MCP を提供し、[設定のしかた](/en/blog/how-to-point-your-agent-at-objectstack/)がルールファイル、スキル、ツールサーバーを扱っている。そしてエージェントが項目への変更を出してきたら、その diff 自身の命名テストの表を求める:どの面が追従したか、どの投影がその項目を名指しているか、それぞれについてどうするつもりか。ゲートの `field-no-consumers` の行がその表の最初の一行で、エージェントはあなたより先にそれを読める。 -本稿のすべてはオープンソースのスタックで動いた。「同じループをホスト型で、ブラウザだけで、何もインストールせずに使いたい?それが ObjectOS — ObjectStack の上に構築された商用ランタイム環境です。」ObjectOS は、ブラウザ上でホストされインストール不要の ObjectOS Cloud としても、自社インフラでセルフマネージドの ObjectOS Enterprise としても動作します。オントロジーは通常の ObjectStack メタデータです。ObjectOS Cloud または ObjectOS Enterprise からエクスポートしてオープンソースの ObjectStack ランタイムで実行する機能は、計画中です。命名テストはホストによって変わらない。それが読む定義は同じファイルだ。 +本稿のすべてはオープンソースのスタックで動いた。「それを実行するために、ObjectStack リポジトリは ObjectOS Community(ghcr.io/objectstack-ai/objectstack イメージ、os start、Console からなる、無料のセルフホスト版)を提供し、ObjectOS Cloud はホスト型のエディション、ObjectOS Enterprise はセルフマネージドのライセンス制のエディションです。」オントロジーは通常の ObjectStack メタデータです。ObjectOS Cloud または ObjectOS Enterprise からエクスポートしてオープンソースの ObjectStack ランタイムで実行する機能は、計画中です。命名テストはホストによって変わらない。それが読む定義は同じファイルだ。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx index 6623557e..00b85f8e 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.ko.mdx @@ -121,4 +121,4 @@ ObjectStack 런타임에서 띄우자 이 다섯 줄은 데이터베이스 테 위의 실행은 저장소에서 재현할 수 있고, 패키지를 설치한 뒤라면 1분쯤 걸린다. 즉 에이전트가 대신 해 줄 수 있는 일이기도 하다. 코딩 에이전트를 ObjectStack 프로젝트에 향하게 하라 — 스캐폴더는 `AGENTS.md`를 쓰고 스킬 번들을 설치하며, 실행 중인 앱은 `/api/v1/mcp`에서 MCP를 제공하고, [설정하는 방법](/en/blog/how-to-point-your-agent-at-objectstack/)이 규칙 파일, 스킬, 도구 서버를 다룬다. 그리고 에이전트가 필드에 대한 변경을 내놓으면, 그 diff 자신의 명명 테스트 표를 요구하라: 어떤 면이 따라왔는지, 어떤 투영이 그 필드를 지목하는지, 각각에 대해 무엇을 하려는지. 게이트의 `field-no-consumers` 줄이 그 표의 첫 행이고, 에이전트는 당신보다 먼저 그것을 읽을 수 있다. -이 글의 모든 것은 오픈소스 스택에서 실행되었다. 「같은 루프를 호스팅으로, 브라우저에서, 아무것도 설치하지 않고 쓰고 싶다면? 그것이 ObjectOS, ObjectStack 위에 구축된 상용 런타임 환경입니다.」 ObjectOS는 설치 없이 브라우저에서 호스팅되는 ObjectOS Cloud로도, 자체 인프라에서 직접 운영하는 ObjectOS Enterprise로도 실행됩니다. 온톨로지는 일반적인 ObjectStack 메타데이터입니다. ObjectOS Cloud 또는 ObjectOS Enterprise에서 내보내 오픈소스 ObjectStack 런타임에서 실행하는 기능은 계획 중입니다. 명명 테스트는 호스트에 따라 바뀌지 않는다. 그것이 읽는 정의는 같은 파일이다. +이 글의 모든 것은 오픈소스 스택에서 실행되었다. 「이를 실행하기 위해 ObjectStack 저장소는 ObjectOS Community(ghcr.io/objectstack-ai/objectstack 이미지, os start, Console로 이루어진 무료 직접 호스팅 에디션)를 제공하며, ObjectOS Cloud는 호스팅 에디션, ObjectOS Enterprise는 직접 운영하는 라이선스 에디션입니다.」 온톨로지는 일반적인 ObjectStack 메타데이터입니다. ObjectOS Cloud 또는 ObjectOS Enterprise에서 내보내 오픈소스 ObjectStack 런타임에서 실행하는 기능은 계획 중입니다. 명명 테스트는 호스트에 따라 바뀌지 않는다. 그것이 읽는 정의는 같은 파일이다. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx index 00cef6e9..e3d3988c 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.mdx @@ -121,4 +121,4 @@ A measurement on one example is a measurement on one example. Four things stay o The run above is reproducible from the repository and takes about a minute once the packages are installed, which means it is also something your agent can do for you. Point a coding agent at an ObjectStack project — the scaffolder writes an `AGENTS.md` and installs the skills bundle, the running app serves MCP at `/api/v1/mcp`, and [how to configure it](/en/blog/how-to-point-your-agent-at-objectstack/) covers the rule file, the skills and the tool server — and when it opens a change to a field, ask it for the naming-test table of its own diff: which surfaces followed, which projections name the field, and what it proposes to do about each. The gate's `field-no-consumers` line is the first row of that table, and the agent can read it before you do. -Everything in this post ran on the open-source stack. "Want the same loop hosted, in the browser, nothing to install? That's ObjectOS, the commercial runtime environment built on ObjectStack." ObjectOS runs hosted in the browser with nothing to install (ObjectOS Cloud) or self-managed on your own infrastructure (ObjectOS Enterprise). Your ontology is ordinary ObjectStack metadata. Exporting it from ObjectOS Cloud or ObjectOS Enterprise to run on the open-source ObjectStack runtime is planned. The naming test does not change with the host; the definition it reads is the same file. +Everything in this post ran on the open-source stack. "To run it, the ObjectStack repository ships ObjectOS Community — the ghcr.io/objectstack-ai/objectstack image, os start and the Console, free and self-hosted — while ObjectOS Cloud is the hosted edition and ObjectOS Enterprise the self-managed, licensed one." Your ontology is ordinary ObjectStack metadata. Exporting it from ObjectOS Cloud or ObjectOS Enterprise to run on the open-source ObjectStack runtime is planned. The naming test does not change with the host; the definition it reads is the same file. diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx index 9d4c82ec..1b71d12f 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hans.mdx @@ -121,4 +121,4 @@ tags: 上面这次运行可以从仓库复现,包装好之后大约一分钟,这意味着它也是你的 Agent 可以替你做的事。把一个编码 Agent 指向一个 ObjectStack 项目——脚手架会写出 `AGENTS.md` 并安装技能包,运行中的应用在 `/api/v1/mcp` 提供 MCP,[如何配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆盖了规则文件、技能和工具服务器——当它开出一个改字段的变更时,向它要它自己这份 diff 的命名测试表:哪些面跟上了,哪些投影写明了这个字段,它打算对每一个怎么办。门禁那行 `field-no-consumers` 是这张表的第一行,而 Agent 可以比你先读到它。 -本文的一切都跑在开源栈上。「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise)。你的本体是普通的 ObjectStack 元数据。从 ObjectOS Cloud 或 ObjectOS Enterprise 导出本体、在开源的 ObjectStack 运行时上运行,目前尚在计划中。命名测试不随宿主而变;它读的那份定义,是同一个文件。 +本文的一切都跑在开源栈上。「为了运行它,ObjectStack 仓库提供 ObjectOS Community —— ghcr.io/objectstack-ai/objectstack 镜像、os start 和 Console,免费且自托管 —— 而 ObjectOS Cloud 是托管版本,ObjectOS Enterprise 是自管理、需许可的版本。」你的本体是普通的 ObjectStack 元数据。从 ObjectOS Cloud 或 ObjectOS Enterprise 导出本体、在开源的 ObjectStack 运行时上运行,目前尚在计划中。命名测试不随宿主而变;它读的那份定义,是同一个文件。 diff --git a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx index a8ed916e..6aeffa97 100644 --- a/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx +++ b/content/blog/views-dashboards-apps-are-projections-of-the-ontology/index.zh-Hant.mdx @@ -122,4 +122,4 @@ tags: 上面這次執行可以從倉庫復現,包裝好之後大約一分鐘,這意味著它也是你的 Agent 可以替你做的事。把一個編碼 Agent 指向一個 ObjectStack 專案——腳手架會寫出 `AGENTS.md` 並安裝技能包,執行中的應用在 `/api/v1/mcp` 提供 MCP,[如何配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆蓋了規則檔案、技能和工具伺服器——當它開出一個改欄位的變更時,向它要它自己這份 diff 的命名測試表:哪些面跟上了,哪些投影寫明瞭這個欄位,它打算對每一個怎麼辦。門禁那行 `field-no-consumers` 是這張表的第一行,而 Agent 可以比你先讀到它。 -本文的一切都跑在開源棧上。「想把同一個迴圈託管起來、在瀏覽器裡用、什麼都不用安裝?那就是 ObjectOS——建立在 ObjectStack 之上的商業執行環境。」ObjectOS 既可託管在瀏覽器中、無需安裝(ObjectOS Cloud),也可自管部署在你自己的基礎設施上(ObjectOS Enterprise)。你的本體是普通的 ObjectStack 中繼資料。從 ObjectOS Cloud 或 ObjectOS Enterprise 匯出本體、在開源的 ObjectStack 執行環境上執行,目前尚在計劃中。命名測試不隨宿主而變;它讀的那份定義,是同一個檔案。 +本文的一切都跑在開源棧上。「為了執行它,ObjectStack 倉庫提供 ObjectOS Community —— ghcr.io/objectstack-ai/objectstack 映像、os start 和 Console,免費且自託管 —— 而 ObjectOS Cloud 是託管版本,ObjectOS Enterprise 是自管理、需許可的版本。」你的本體是普通的 ObjectStack 中繼資料。從 ObjectOS Cloud 或 ObjectOS Enterprise 匯出本體、在開源的 ObjectStack 執行環境上執行,目前尚在計劃中。命名測試不隨宿主而變;它讀的那份定義,是同一個檔案。