From 177a68476dd6f681b6b270eab38e56d3576a792b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 12:39:02 +0000 Subject: [PATCH 01/24] =?UTF-8?q?content(blog):=20what=20"the=20ontology?= =?UTF-8?q?=20is=20the=20software"=20means=20=E2=80=94=20the=20pillar=20de?= =?UTF-8?q?finition=20post=20(slot=202026-10-08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Post 1 of the positioning wave: the executable business ontology defined from the objectstack README and the Business Ontology concepts page, quoted rather than paraphrased — the six-kind core mapped onto the bundled CRM example, the four projections with a removal test to tell core from projection, the three things it is not, where code goes, and the four promises led by Executable. ObjectOS appears only as the canonical T4 + E1. Eight locales (zh-Hant generated by gen:zh-hant), one cover and two diagrams, in English text and in Chinese text for the zh-Hans/zh-Hant pair. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- .../cover-en.svg | 69 ++++++ .../cover.svg | 68 ++++++ .../index.de.mdx | 197 +++++++++++++++++ .../index.es.mdx | 197 +++++++++++++++++ .../index.fr.mdx | 197 +++++++++++++++++ .../index.ja.mdx | 197 +++++++++++++++++ .../index.ko.mdx | 197 +++++++++++++++++ .../index.mdx | 197 +++++++++++++++++ .../index.zh-Hans.mdx | 197 +++++++++++++++++ .../index.zh-Hant.mdx | 198 ++++++++++++++++++ .../six-kind-core-en.svg | 119 +++++++++++ .../six-kind-core.svg | 119 +++++++++++ .../validated-not-reasoned-en.svg | 83 ++++++++ .../validated-not-reasoned.svg | 83 ++++++++ 14 files changed, 2118 insertions(+) create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/cover-en.svg create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/cover.svg create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core-en.svg create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core.svg create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned-en.svg create mode 100644 content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned.svg diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/cover-en.svg b/content/blog/what-does-the-ontology-is-the-software-mean/cover-en.svg new file mode 100644 index 0000000..4e40777 --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/cover-en.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + WHAT THE SENTENCE MEANS + The ontology + is the software. + One executable business ontology. + AI writes it, the runtime runs it, + agents operate it, you own it. + + Executable + AI-writable + Agent-operable + You own it + + + + + + + 6 + kinds in the core + objects and fields · relations · actions + permissions · flows · agent and tool definitions + + + + + 4 + projections, outside it + views · dashboards · apps · translations + same metadata, same gate, not part of the ontology + + + + + 3 + things it is not + a knowledge-representation ontology · a semantic layer + a formal ontology in the philosophical sense + + + Code does not disappear: hooks, action bodies, + CEL and constrained JSX run inside the runtime. + + + An executable business ontology, defined — from the ObjectStack README, quoted rather than paraphrased. + diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/cover.svg b/content/blog/what-does-the-ontology-is-the-software-mean/cover.svg new file mode 100644 index 0000000..becfd3f --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/cover.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + 这句话到底是什么意思 + 本体即软件。 + 一份可执行的业务本体。 + AI 编写它,运行时运行它, + Agent 操作它,你拥有它。 + + 可执行 + AI 可编写 + Agent 可操作 + 归你所有 + + + + + + + 6 + 类核心 + 对象与字段 · 关系 · 动作 + 权限 · 流程 · Agent 与工具定义 + + + + + 4 + 种投影,在本体之外 + 视图 · 仪表盘 · 应用 · 翻译 + 同样的元数据、同一道门禁,但不是本体的一部分 + + + + + 3 + 个「不是」 + 不是知识表示本体 · 不是语义层 + 不是哲学意义上的形式本体 + + + 代码不会消失:钩子、动作体、CEL 和受限 JSX + 都在运行时的围栏内运行。 + + + 可执行业务本体的完整定义——引自 ObjectStack README,引用而非转述。 + diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx new file mode 100644 index 0000000..8e64c4e --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx @@ -0,0 +1,197 @@ +--- +title: "Was bedeutet „Die Ontologie ist die Software“? Eine ausführbare Geschäftsontologie, definiert" +description: "Der Satz ist ObjectStacks Positionierung; dies ist seine Definition: ein Kern aus sechs Arten, vier Projektionen, drei Dinge, die sie nicht ist, und ein Test, der Kern von Projektion trennt." +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontologie + - Ausführbare Geschäftsontologie +--- + +**Die Kurzfassung.** „Die Ontologie ist die Software.“ ist der erste Satz der ObjectStack-README, und die Zeile darunter ist die Definition: „Eine ausführbare Geschäftsontologie. AI schreibt sie, die Runtime führt sie aus, Agents bedienen sie, Sie besitzen sie.“ Eine ausführbare Geschäftsontologie ist die Definition Ihres Geschäfts, die eine Runtime direkt ausführt. Ihr Kern besteht aus sechs Arten von Dingen – Objekte und Felder, Relationen, Aktionen, Berechtigungen, Flows sowie Agent- und Tool-Definitionen –, kompiliert in ein Artefakt, aus dem Datenbank, API, UI und Agent-Tools abgeleitet werden. Views, Dashboards, Apps und Übersetzungen sind Projektionen davon, nicht Teil davon. Sie ist keine Ontologie im Sinne der Wissensrepräsentation, kein Semantic Layer über Ihren bestehenden Systemen und keine formale Ontologie im philosophischen Sinn. Code verschwindet nicht; er wandert in die Runtime, als Hooks, Aktionsrümpfe, CEL und eingeschränktes JSX. + +Das ist die ganze Behauptung. Der Rest dieses Beitrags nimmt sie auseinander: warum der Satz *ist* sagt und nicht *beschreibt*, die sechs Arten, die Projektionen und ein Test, der beides trennt, die drei Verneinungen, wohin Code geht, und die vier Versprechen, auf die sich die Zeile reduziert. Die Quellen sind die README und die Konzeptseite, auf die sie verlinkt, zitiert statt paraphrasiert, dazu das CRM-Beispiel aus demselben Repository, gemessen statt beschrieben. + +## Warum der Satz „ist“ sagt + +Die meisten Unternehmensontologien beschreiben Software. Sie stehen neben den Systemen, die das Geschäft tatsächlich betreiben – ein Modell des Kunden, ein Graph davon, was womit verbunden ist, eine kontrollierte Definition dessen, was Umsatz bedeutet –, und die Systeme laufen darunter weiter, ohne davon zu wissen. Die Beschreibung ist nützlich, und sie ist auch optional: Löschen Sie sie, und nichts bleibt stehen. + +Der ObjectStack-Satz ist als Identität formuliert, weil die Beziehung umgekehrt ist. Die Definition ist das, was läuft. Löschen Sie die Ontologie, und es bleibt keine Anwendung übrig: keine Tabellen, keine Endpunkte, keine Bildschirme, keine Tools, die ein Agent aufrufen könnte. Die Konzeptseite bringt die Pipeline in eine Zeile: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +Sie schreiben typisierte Metadaten in `objectstack.config.ts`; sie kompilieren zum Artefakt `objectstack.json`; die Runtime liest das Artefakt und leitet den Rest ab. Die Bildunterschrift der README zum selben Bild ist noch kürzer: „Eine typisierte Definition → Datenbank · REST API · Client-SDK · UI · MCP-Tools.“ Es gibt kein zweites Artefakt, das die Software *ist*, während die Ontologie nur über sie spricht. Das bedeutet „ist“ hier, und alles Weitere folgt daraus. + +## Der Kern aus sechs Arten + +Die Konzeptseite definiert den Kern als sechs Arten von Dingen. Die Tabelle unten behält ihren Wortlaut in der mittleren Spalte und zeigt in der rechten Spalte, wo jede Art im mitgelieferten CRM-Beispiel (`examples/app-crm` im ObjectStack-Repository) lebt, damit die Definition an einer echten Definition geprüft werden kann und nicht an einem Slogan. + +| Kern | Was sie deklariert | Im mitgelieferten CRM-Beispiel | +| :--- | :--- | :--- | +| **Objekte und Felder** | Die Entitäten des Geschäfts und ihre typisierten Attribute – Validierung, Formeln, Standardwerte, Dateien | `src/objects/`: Account, Kontakt, Lead, Opportunity, Opportunity-Position, Aktivität | +| **Relationen** | Lookups und Master-Detail-Verknüpfungen zwischen Objekten | `Field.lookup('crm_account', …)` auf der Opportunity; die Position ist ihr Master-Detail-Kind | +| **Aktionen** | Die Operationen, die ein Benutzer oder ein Agent auf einem Datensatz auslösen darf, und die Bedingungen, unter denen sie erscheinen | `src/actions/convert-lead.action.ts`, nur sichtbar, solange der Lead noch nicht konvertiert ist | +| **Berechtigungen** | Wer was lesen, anlegen, bearbeiten und löschen darf – RBAC, Sicherheit auf Zeilen- und Feldebene, Freigaben | `src/security/sales-positions.ts`: ein Set `crm_sales_user` und ein Gastprofil, das nur Leads anlegen darf | +| **Flows** | Die Geschäftsprozesse – DAG-Flows, Datensatz-Trigger, geplante Jobs, Genehmigungen, Workflows | `src/flows/convert-lead.flow.ts`, ein Screen-Flow, der Account und Opportunity anlegt und den Lead dann als konvertiert markiert | +| **Agent- und Tool-Definitionen** | Die AI-Agents, die die App mitbringt, die Tools, die sie aufrufen dürfen, und die Skills, die sie formen | In diesem Beispiel nicht deklariert. Jedes Objekt ist bereits ein MCP-Tool; Aktionen melden sich mit `ai: { exposed: true }` an | + +Eine Messung, aufgenommen mit dem Befehl, den die README selbst unter „measure it yourself“ ausgibt: Beim Commit `9f9510f2` umfasst die Definition des CRM-Beispiels **1.917 Zeilen TypeScript in 31 Dateien**. Diese Dateien enthalten sechs Objekte, drei Views, ein Dashboard, eine App, einen Lead-Konvertierungs-Flow, eine Aktion, einen Hook, zwei Berechtigungssets, drei Positionen, ein Übersetzungsbündel, eine Seite und die Seed-Daten – die ganze Anwendung, als Diff, den Sie lesen können. Der Punkt der Zahl ist nicht, dass sie abstrakt klein wäre. Der Punkt ist, dass ein Agent die ganze Definition auf einmal halten und „Was geht kaputt, wenn ich das ändere?“ aus der Definition selbst beantworten kann – was die README Apps nennt, die klein genug sind, dass AI sie als Ganzes fassen kann. + +Der Kern ist außerdem, per Konstruktion, der Teil, der Ihnen gehört. Die Konzeptseite: „Es ist eine Definition, offen und versioniert. Sie liegt als gewöhnliches TypeScript in Ihrem Repository, ist als Diff prüfbar und ist unter Apache-2.0 spezifiziert und lizenziert – kein Graph, der im System eines Anbieters gehalten wird und nur über dessen Konsole erreichbar ist.“ + +## Projektionen und der Entfernungstest + +Views, Dashboards, Apps und Übersetzungen werden in denselben Metadaten geschrieben und von demselben Gate validiert, aber sie sind nicht Teil der Ontologie. Die Konzeptseite nennt sie Projektionen und erklärt jede in einem Nebensatz: „Eine Listenansicht projiziert die Felder und Berechtigungen eines Objekts auf ein Raster; ein Dashboard projiziert Abfragen über Objekte auf Diagramme; eine App fasst Objekte, Views und Aktionen zu einem navigierbaren Produkt zusammen; eine Übersetzung projiziert jedes Label in eine Sprache.“ + +![Der Kern aus sechs Arten kompiliert über die ObjectStack-Runtime in Datenbank, API, Console-UI und MCP-Tools; Views, Dashboards, Apps und Übersetzungen stehen als Projektionen außerhalb des Ontologie-Kastens, und Hooks, Aktionsrümpfe, CEL und eingeschränktes JSX laufen innerhalb des Zauns der Runtime](./six-kind-core-en.svg) + +Der Satz, der die Unterscheidung handhabbar macht, ist dieser: „Ändern Sie den Kern, und die Projektionen folgen; entfernen Sie eine Projektion, und das Geschäft, das sie zeigte, ist unverändert.“ Machen Sie daraus einen Test, und Sie können jede Definition in einem Projekt mit einem Zug einordnen. Nennen wir ihn den **Entfernungstest**: + +> Löschen Sie die Definition. Wenn sich das Datenbankschema, die API oder das, was ein Benutzer oder Agent tun darf, geändert hat, war sie Kern. Wenn nur ein Bildschirm, ein Diagramm, ein Menü oder ein Label verschwunden ist, war sie eine Projektion. + +Legen Sie ihn an das CRM-Beispiel an. Löschen Sie `src/views/opportunity.view.ts`, und das Opportunity-Objekt, seine Felder, seine Lookups und sein Berechtigungsset sind alle noch da; der REST-Endpunkt antwortet weiter; das MCP-Tool existiert weiter. Löschen Sie `src/objects/opportunity.object.ts`, und die View hat nichts mehr zu projizieren, der Flow nichts mehr anzulegen, und das Berechtigungsset benennt ein Objekt, das nicht mehr existiert. Das eine war eine Projektion; das andere war die Software. + +Warum die Unterscheidung einen Namen verdient, steht auf derselben Seite: „Die Unterscheidung begrenzt, was ein Agent halten muss, um über das Geschäft nachzudenken: den Kern, nicht jeden Bildschirm.“ Ein Agent, der eine Änderung an einer Genehmigungsschwelle prüft, braucht die Objekte, die Berechtigungen und den Flow vor sich. Er braucht nicht jedes Dashboard, das das Ergebnis zufällig in ein Diagramm zeichnet. + +## Drei Dinge, die sie nicht ist + +Das Wort *Ontologie* trägt Jahrzehnte an Bedeutung aus anderen Feldern, und die README verwendet einen guten Teil ihrer Definition darauf zu sagen, welche dieser Bedeutungen nicht gelten. Es gibt drei Verneinungen, und sie sind verschieden. + +| Sie ist nicht | Was dieses Ding ist | Warum das hier anders ist | +| :--- | :--- | :--- | +| **Eine Ontologie der Wissensrepräsentation** | OWL, RDF und Wissensgraphen *beschreiben* eine Domäne, tragen Klassenvererbung und Axiome und werden von einer Inferenzmaschine *durchdacht* | Keine Objektvererbung, keine Axiome, kein Reasoner. Die Definition wird validiert – durch Zod-Schemas und `os validate` – und dann ausgeführt. Der Schlüssel `abstract` wurde genau deshalb aus der Spezifikation entfernt (ADR-0049) | +| **Ein Semantic Layer über Ihren bestehenden Systemen** | Eine nur lesende Abbildung über Systeme, die Sie bereits betreiben: Sie beschreibt das Geschäft, führt es aber nicht aus | Die Ontologie *ist* das System: Datenbank, API, UI und Agent-Tools werden aus ihr abgeleitet. Die Föderation einer externen Datenquelle ist möglich, standardmäßig nur lesend und früh | +| **Eine formale Ontologie im philosophischen Sinn** | Eine Aussage darüber, was im Allgemeinen existiert | Sie macht keine solche Aussage. Sie ist die Anwendung eines Unternehmens, genau genug definiert, um zu laufen | + +![Drei Spalten: Eine Ontologie der Wissensrepräsentation wird beschrieben und durchdacht, ein Semantic Layer ist eine nur lesende Abbildung über bestehenden Systemen, und eine ausführbare Geschäftsontologie wird validiert und dann zu Datenbank, API, UI und Agent-Tools ausgeführt](./validated-not-reasoned-en.svg) + +Die erste Verneinung wird am häufigsten übersehen, weil sie eine Designentscheidung ist und keine Einschränkung. Eine Inferenzmaschine ist eine zweite Quelle der Wahrheit: Die Fakten, die sie zur Laufzeit ableitet, wurden nie irgendwo aufgeschrieben, wo ein Prüfer sie lesen könnte. Die Regel von ObjectStack, von der Konzeptseite, ist das Gegenteil: „Zur Laufzeit wird nichts durchdacht: Was läuft, ist, was validiert wurde.“ Wenn eine Berechtigung nicht im Diff steht, ist sie nicht im System. + +Die zweite Verneinung ist für jeden wichtig, der Plattformen bewertet, denn der Begriff *Semantic Layer* wird in denselben Gesprächen für zwei verschiedene Dinge verwendet. Ein Semantic Layer kann eine ausgezeichnete Definition dessen sein, was eine Zahl bedeutet – der [Vergleich mit Wissensgraphen](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/) verteidigt ihn fair. Er ist eine Substantivschicht. Eine ausführbare Geschäftsontologie trägt die Verben: Aktionen, Berechtigungen und Flows, die entscheiden, was getan werden darf, von wem, und was als Nächstes passiert. Die Dokumentation merkt außerdem an, dass sie *Semantic Layer* in einem engeren Sinn verwendet, für die Analyse-Dataset-Schicht, an die Berichte und Dashboards binden; dieser engere Sinn betrifft das Berichtswesen und ist nicht, was der Satz meint. + +Die dritte Verneinung ist die kürzeste und die ehrlichste. Das Format macht keine Aussage über die Welt. Es behauptet, dass die Anwendung eines Unternehmens genau genug formuliert werden kann, damit eine Runtime sie ausführt und ein Prüfer sie als Ganzes liest. + +## Wohin Code geht + +Eine Definition, die ausgeführt und nicht durchdacht wird, muss trotzdem Dinge ausdrücken, die kein Schema ausdrückt: eine Wahrscheinlichkeit, die auf 100 % springt, wenn ein Deal abschließt, ein Button, der verschwindet, sobald ein Lead konvertiert ist, eine Landingpage mit einem Layout. Der Satz der README lautet „Code verschwindet nicht: er wandert in die Runtime, als Hooks, Aktionsrümpfe, CEL und eingeschränktes JSX“, und die Konzeptseite gibt den Grund: Alle vier „laufen innerhalb des Berechtigungs- und Audit-Zauns der Runtime“, und „nichts davon ist über eine Codebasis verstreut, die die Ontologie dann beschreiben müsste.“ + +Ein Hook aus dem CRM-Beispiel ist eine Funktion, die am Lebenszyklus eines Objekts hängt: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL ist die Ausdruckssprache für Formeln, Prädikate und Richtlinien. Dasselbe Beispiel nutzt sie für ein berechnetes Feld und für die Sichtbarkeit einer Aktion: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +Eingeschränktes JSX ist die Art, eine eigene Seite zu schreiben, und die Einschränkung ist der Punkt: Es „wird in einen servergesteuerten UI-Baum geparst, niemals als Code ausgeführt.“ Das Paket, das dies tut, ist in der README als Compiler aufgeführt, der parst und nie ausführt. Aktionsrümpfe sind der vierte Ort, für die serverseitige Arbeit, die eine Aktion beim Aufruf verrichtet. + +Das Muster über alle vier ist dasselbe. Code ist erlaubt; Code ist nicht der Ort, an dem das Geschäft lebt. Das Geschäft lebt in den sechs Arten, und der Code läuft innerhalb des Zauns, den diese sechs Arten definieren, unter denselben Berechtigungen und demselben Audit wie ein Klick oder ein Tool-Aufruf. + +## Validiert, dann ausgeführt + +Zwei Verben beschreiben, was die Runtime mit dem Artefakt tut, und die Konzeptseite verwendet beide bewusst. Sie **leitet ab**: „das Datenbankschema auf einem austauschbaren Treiber, eine generierte REST- und Realtime-API, servergesteuerte UI für die Console und einen MCP-Server“. Und sie **setzt durch**: „RBAC, Sicherheit auf Zeilen- und Feldebene, Freigaben und Audit gelten für eine REST-Anfrage, einen Klick in der Console und den Tool-Aufruf eines Agents gleichermaßen.“ + +Die README zeigt das erste Verb mit einem Objekt. Deklarieren Sie ein Ticket: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +und „die REST-API existiert in dem Moment, in dem das Objekt existiert – keine Controller zu schreiben“: + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +Beachten Sie `sharingModel: 'private'` mit seinem Kommentar. Die Sicherheitshaltung ist Teil des Objekts, und das Gate verlangt sie: `os validate` „lehnt Metadaten ab, die den Typcheck bestehen, aber zur Laufzeit still scheitern würden: hängende Bindungen, fehlerhafte CEL-Prädikate, fehlende Sicherheitshaltung.“ Das ist die validierte Hälfte. Die ausgeführte Hälfte ist, dass jede aus dem Objekt abgeleitete Oberfläche – die Tabelle, der Endpunkt, das Console-Raster, das MCP-Tool – bei jedem Aufruf gegen dieselben Berechtigungen geprüft wird, sodass ein Agent, der das Ticket über MCP erreicht, genau das sieht, was ein Mensch mit denselben Berechtigungen sehen würde. + +## Die vier Versprechen, angeführt von „Ausführbar“ + +Die README reduziert die Zeile auf vier Worte: „Ausführbar · Von AI schreibbar · Von Agents bedienbar · Sie besitzen sie“. Jedes ist eine Behauptung mit einem Mechanismus dahinter, und die Reihenfolge ist keine Dekoration – das erste ist das, worauf die anderen drei stehen. + +**Ausführbar.** Die Definition ist das, was läuft. Alles in den beiden Abschnitten oben ist dieses Versprechen: ein Artefakt, abgeleitete Oberflächen, Durchsetzung bei jedem Aufruf, nichts, das zur Laufzeit durchdacht wird. Ohne es würde „von AI schreibbar“ bedeuten, dass eine AI Dokumentation schreibt. + +**Von AI schreibbar.** „Der Agent schreibt typisierte Metadaten – keine Codebasis. Das Gate lehnt ab, was zur Laufzeit still scheitern würde, und der Agent behebt es, bevor Sie es je zu sehen bekommen.“ Der Scaffolder „installiert das AI-Skills-Bündel und schreibt eine `AGENTS.md`, sodass Ihr Agent mit den bereits geladenen Regeln des Protokolls startet“. Die vier Gates der README – typisiert, validiert, geprüft, kontrolliert – sind der Grund, warum die Fehler des Agents nicht ausgeliefert werden, und der genannte Grund, warum das überhaupt funktioniert: „Die Fehler eines Agents werden zu lokalisiertem, korrigierendem Text, den er lesen und selbst beheben kann.“ + +**Von Agents bedienbar.** „Objekte sind Tools, Aktionen sind Tools, Berechtigungen entscheiden, was ein Agent aufrufen darf, und das Audit zeichnet auf, was er getan hat.“ Die laufende App „stellt sie als MCP-Server unter `/api/v1/mcp` bereit – standardmäßig eingeschaltet“, und ein Agent bedient sie „unter denselben Berechtigungen und derselben RLS wie ein Mensch“. Dieselbe Ontologie, die von einem Agent geschrieben wurde, wird dann von einem benutzt, durch denselben Zaun. + +**Sie besitzen sie.** „Alles in diesem Repo ist der offene Stack – Protokoll, Mikrokernel, SDK, CLI und die Produktions-Runtime, Apache-2.0 ohne Open-Core-Sternchen.“ Und die Definition selbst „liegt als gewöhnliches TypeScript in Ihrem Repository, versioniert in Ihrer VCS und als Diff prüfbar – kein Graph, der im System eines Anbieters gehalten wird.“ Die Ontologie ist eine Datei, die Sie halten, nicht ein Mandant, den Sie mieten. + +## Was das nicht löst + +Ein Definitionsbeitrag sollte sagen, wo die Definition endet. + +Sie macht das Modellieren nicht leicht. Zu entscheiden, was eine Opportunity ist, wer eine abschließen darf und was ein Rabatt über 30 % braucht, ist nach wie vor die langsame, umstrittene, menschliche Arbeit, die es immer war; das Format entscheidet nur, ob das Ergebnis klein genug ist, um es als Ganzes zu lesen, und streng genug, um Fehler schon beim Verfassen abzulehnen. + +Sie schlussfolgert nicht. Wenn Ihr Problem wirklich Inferenz über ein Domänenmodell braucht – Klassenhierarchien, abgeleitete Fakten, eine Maschine, die Konsequenzen entdeckt –, dann ist das die Ontologie der Wissensrepräsentation, die diese ausdrücklich nicht ist. Wählen Sie dieses Werkzeug, und erwarten Sie nicht, dass diesem ein Reasoner wächst. + +Sie ist keine Schicht über dem, was Sie bereits betreiben. Die Föderation einer externen Datenquelle ist standardmäßig nur lesend und als früh beschrieben, sodass eine ausführbare Ontologie über einer Altdatenbank als Lesemodell beginnt und sich Schreibzugriffe Objekt für Objekt verdient. [Ein internes System in Metadaten überführen](/en/blog/how-to-move-an-internal-system-to-metadata/) ist die ehrliche Version dieses Wegs, einschließlich der Fälle, in denen die Antwort lautet, gar nicht zu migrieren. + +Und neuartige Logik bleibt Code. Hooks und Aktionsrümpfe sind ihr Ort, innerhalb des Zauns, und sie sind weiterhin Code, den ein Mensch prüfen muss. Die Ontologie macht diesen Code kleiner und begrenzt; sie lässt ihn nicht verschwinden, und die README sagt das mit genau diesen Worten. + +## Gehostet, oder auf Ihrer eigenen Infrastruktur + +Die README beschreibt die Schleife oben – ein Coding-Agent schreibt die Metadaten in Ihr Repo und bedient die laufende App über MCP – und sagt dann: „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), und in beiden Editionen können Sie Ihre Ontologie exportieren und auf der quelloffenen ObjectStack Runtime ausführen. Das ist das vierte Versprechen, als Produktgrenze formuliert: Die Ontologie gehört Ihnen auf jedem Host, auf dem sie läuft. + +## Beginnen Sie mit dem Agent-Pfad + +Nichts oben verlangt, die Definition auf Treu und Glauben zu nehmen; der Fünf-Minuten-Pfad der README selbst erzeugt eine, die Sie lesen können. + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +Öffnen Sie das Projekt in Ihrem Coding-Agent und beschreiben Sie die Anforderung. Das Beispiel der README ist ein Support-Desk: ein `ticket`-Objekt mit Betreff, Beschreibung, einer Prioritätsauswahl und einer Statusauswahl; eine Resolve-Aktion, die nur auf Tickets erscheint, die noch nicht gelöst sind; eine Listenansicht „Open tickets“ und eine Support-Navigationsgruppe; `npm run validate`, wenn es fertig ist. Dann die Vorschau: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +Sehen Sie sich den Diff an, den der Agent erzeugt hat, bevor Sie in die Console schauen. Das Objekt, die Aktion und ihr Sichtbarkeitsprädikat, die View – Sie werden jede Zeile mit dem Entfernungstest in Kern oder Projektion einsortieren können, und das Gate wird die Zeilen, die nicht laufen würden, bereits abgelehnt haben. Wenn der Agent Steuerung braucht, deckt [die Konfiguration für ObjectStack](/en/blog/how-to-point-your-agent-at-objectstack/) die Regeldatei, das Skills-Bündel und den Tool-Server ab. Die Definition, die er schreibt, ist die Software, und sie liegt in Ihrem Repository. diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx new file mode 100644 index 0000000..81c089b --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx @@ -0,0 +1,197 @@ +--- +title: "¿Qué significa «La ontología es el software»? Una ontología de negocio ejecutable, definida" +description: "La frase es el posicionamiento de ObjectStack; esto es su definición: un núcleo de seis tipos, cuatro proyecciones, tres cosas que no es y una prueba para distinguir núcleo de proyección." +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontología + - Ontología de negocio ejecutable +--- + +**La versión corta.** «La ontología es el software.» es la primera frase del README de ObjectStack, y la línea de debajo es la definición: «Una ontología de negocio ejecutable. La AI la escribe, el runtime la ejecuta, los agentes la operan, tú la posees.» Una ontología de negocio ejecutable es la definición de tu negocio que un runtime ejecuta directamente. Su núcleo son seis tipos de cosas —objetos y campos, relaciones, acciones, permisos, flujos, y definiciones de agentes y herramientas— compilados en un artefacto, del que se derivan la base de datos, la API, la UI y las herramientas de agente. Las vistas, los paneles, las apps y las traducciones son proyecciones suyas, no parte de ella. No es una ontología de representación del conocimiento, no es una capa semántica sobre tus sistemas existentes y no es una ontología formal en el sentido filosófico. El código no desaparece; se traslada al runtime, como hooks, cuerpos de acción, CEL y JSX restringido. + +Esa es toda la afirmación. El resto de este artículo la desmonta: por qué la frase dice *es* y no *describe*, los seis tipos, las proyecciones y una prueba para distinguirlas, las tres negaciones, adónde va el código, y las cuatro promesas a las que se reduce la línea. Las fuentes son el README y la página de conceptos que enlaza, citadas en lugar de parafraseadas, más el ejemplo de CRM que viene en el mismo repositorio, medido en lugar de descrito. + +## Por qué la frase dice «es» + +La mayoría de las ontologías empresariales describen software. Están junto a los sistemas que de verdad hacen funcionar el negocio —un modelo del cliente, un grafo de qué se conecta con qué, una definición gobernada de qué significa ingreso— y los sistemas siguen funcionando debajo, sin enterarse. La descripción es útil, y también es opcional: bórrala y nada se detiene. + +La frase de ObjectStack está formulada como una identidad porque la relación es la contraria. La definición es la cosa que se ejecuta. Borra la ontología y no queda ninguna aplicación: ni tablas, ni endpoints, ni pantallas, ni herramientas que un agente pueda llamar. La página de conceptos pone la tubería en una línea: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +Escribes metadatos tipados en `objectstack.config.ts`; se compilan en el artefacto `objectstack.json`; el runtime lee el artefacto y deriva el resto. El pie de foto del README para la misma imagen es aún más corto: «Una definición tipada → base de datos · REST API · SDK de cliente · UI · herramientas MCP.» No hay un segundo artefacto que *sea* el software mientras la ontología se limita a hablar de él. Eso es lo que significa «es» aquí, y todo lo que sigue se desprende de ello. + +## El núcleo de seis tipos + +La página de conceptos define el núcleo como seis tipos de cosas. La tabla siguiente conserva su redacción en la columna central y, en la columna derecha, señala dónde vive cada tipo en el ejemplo de CRM incluido (`examples/app-crm` en el repositorio de ObjectStack), para que la definición pueda comprobarse contra una definición real y no contra un eslogan. + +| Núcleo | Qué declara | En el ejemplo de CRM incluido | +| :--- | :--- | :--- | +| **Objetos y campos** | Las entidades del negocio y sus atributos tipados: validación, fórmulas, valores por defecto, archivos | `src/objects/`: cuenta, contacto, lead, oportunidad, línea de oportunidad, actividad | +| **Relaciones** | Lookups y enlaces maestro-detalle entre objetos | `Field.lookup('crm_account', …)` en la oportunidad; la línea es su hijo maestro-detalle | +| **Acciones** | Las operaciones que un usuario o un agente puede invocar sobre un registro, y las condiciones bajo las que aparecen | `src/actions/convert-lead.action.ts`, visible solo mientras el lead aún no está convertido | +| **Permisos** | Quién puede leer, crear, editar y borrar qué: RBAC, seguridad a nivel de fila y de campo, compartición | `src/security/sales-positions.ts`: un conjunto `crm_sales_user` y un perfil de invitado que solo puede insertar leads | +| **Flujos** | Los procesos de negocio: flujos DAG, disparadores de registro, tareas programadas, aprobaciones, workflows | `src/flows/convert-lead.flow.ts`, un flujo de pantalla que crea la cuenta y la oportunidad y luego marca el lead como convertido | +| **Definiciones de agentes y herramientas** | Los agentes de AI que la app incluye, las herramientas que pueden llamar y las skills que los moldean | Ninguna declarada en este ejemplo. Cada objeto ya es una herramienta MCP; las acciones se apuntan con `ai: { exposed: true }` | + +Una medición, tomada con el comando que el propio README imprime bajo «measure it yourself»: en el commit `9f9510f2`, la definición del ejemplo de CRM son **1.917 líneas de TypeScript en 31 archivos**. Esos archivos contienen seis objetos, tres vistas, un panel, una app, un flujo de conversión de leads, una acción, un hook, dos conjuntos de permisos, tres posiciones, un paquete de traducción, una página y los datos semilla: la aplicación entera, como un diff que puedes leer. La gracia del número no es que sea pequeño en abstracto. Es que un agente puede sostener la definición completa de una vez y responder «¿qué se rompe si cambio esto?» desde la propia definición, lo que el README llama apps lo bastante pequeñas para que la AI las abarque enteras. + +El núcleo es también, por construcción, la parte que es tuya. La página de conceptos: «Es una definición, abierta y versionada. Vive en tu repositorio como TypeScript ordinario, es revisable como un diff, y está especificada y licenciada bajo Apache-2.0: no un grafo guardado dentro del sistema de un proveedor y accesible solo a través de la consola de ese proveedor.» + +## Proyecciones, y la prueba de eliminación + +Las vistas, los paneles, las apps y las traducciones se escriben en los mismos metadatos y se validan en la misma puerta, pero no forman parte de la ontología. La página de conceptos las llama proyecciones y explica cada una en una cláusula: «Una vista de lista proyecta los campos y permisos de un objeto sobre una cuadrícula; un panel proyecta consultas sobre objetos en gráficos; una app agrupa objetos, vistas y acciones en un producto navegable; una traducción proyecta cada etiqueta a un idioma.» + +![El núcleo de seis tipos se compila a través del runtime de ObjectStack en la base de datos, la API, la UI de la Console y las herramientas MCP; las vistas, los paneles, las apps y las traducciones quedan fuera de la caja de la ontología como proyecciones, y los hooks, los cuerpos de acción, CEL y el JSX restringido se ejecutan dentro de la valla del runtime](./six-kind-core-en.svg) + +La frase que hace operativa la distinción es esta: «Cambia el núcleo y las proyecciones lo siguen; elimina una proyección y el negocio que mostraba no cambia.» Conviértela en una prueba y podrás clasificar cualquier definición de un proyecto de un solo movimiento. Llamémosla la **prueba de eliminación**: + +> Borra la definición. Si el esquema de la base de datos, la API, o lo que un usuario o agente puede hacer ha cambiado, era núcleo. Si solo ha desaparecido una pantalla, un gráfico, un menú o una etiqueta, era una proyección. + +Léela contra el ejemplo de CRM. Borra `src/views/opportunity.view.ts` y el objeto oportunidad, sus campos, sus lookups y su conjunto de permisos siguen todos ahí; el endpoint REST sigue respondiendo; la herramienta MCP sigue existiendo. Borra `src/objects/opportunity.object.ts` y la vista no tiene nada que proyectar, el flujo no tiene nada que crear, y el conjunto de permisos nombra un objeto que ya no existe. Uno era una proyección; el otro era el software. + +Por qué la distinción merece un nombre lo dice la misma página: «La distinción acota lo que un agente debe sostener para razonar sobre el negocio: el núcleo, no cada pantalla.» Un agente que revisa un cambio en un umbral de aprobación necesita tener delante los objetos, los permisos y el flujo. No necesita cada panel que casualmente grafica el resultado. + +## Tres cosas que no es + +La palabra *ontología* arrastra décadas de significado de otros campos, y el README dedica buena parte de su definición a decir cuáles de esos significados no aplican. Hay tres negaciones, y son distintas. + +| No es | Qué es esa cosa | Por qué esto es diferente | +| :--- | :--- | :--- | +| **Una ontología de representación del conocimiento** | OWL, RDF y los grafos de conocimiento *describen* un dominio, llevan herencia de clases y axiomas, y un motor de inferencia *razona* sobre ellos | Sin herencia de objetos, sin axiomas, sin razonador. La definición se valida —con esquemas Zod y `os validate`— y después se ejecuta. La clave `abstract` se eliminó de la especificación exactamente por esa razón (ADR-0049) | +| **Una capa semántica sobre tus sistemas existentes** | Un mapeo de solo lectura sobre sistemas que ya operas: describe el negocio pero no lo ejecuta | La ontología *es* el sistema: la base de datos, la API, la UI y las herramientas de agente se derivan de ella. Federar una fuente de datos externa es posible, de solo lectura por defecto, y temprano | +| **Una ontología formal en el sentido filosófico** | Una afirmación sobre qué existe en general | No hace tal afirmación. Es la aplicación de un negocio, definida con la precisión suficiente para ejecutarse | + +![Tres columnas: una ontología de representación del conocimiento se describe y se razona, una capa semántica es un mapeo de solo lectura sobre sistemas existentes, y una ontología de negocio ejecutable se valida y luego se ejecuta en una base de datos, una API, una UI y herramientas de agente](./validated-not-reasoned-en.svg) + +La primera negación es la que más se pasa por alto, porque es una decisión de diseño y no una limitación. Un motor de inferencia es una segunda fuente de verdad: los hechos que deriva en tiempo de ejecución nunca se escribieron en ningún sitio que un revisor pudiera leer. La regla de ObjectStack, de la página de conceptos, es la contraria: «Nada se razona en tiempo de ejecución: lo que se ejecuta es lo que se validó.» Si un permiso no está en el diff, no está en el sistema. + +La segunda negación importa a cualquiera que evalúe plataformas, porque el término *capa semántica* se está usando para dos cosas distintas en las mismas conversaciones. Una capa semántica puede ser una excelente definición de qué significa un número; la [comparación con los grafos de conocimiento](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/) la defiende con justicia. Es una capa de sustantivos. Una ontología de negocio ejecutable lleva los verbos: acciones, permisos y flujos que deciden qué puede hacerse, quién lo hace y qué ocurre después. La documentación también señala que usa *capa semántica* en un sentido más estrecho, para la capa de datasets analíticos a la que se enlazan informes y paneles; ese sentido estrecho trata de informes, y no es lo que la frase significa. + +La tercera negación es la más corta y la más honesta. El formato no afirma nada sobre el mundo. Afirma que la aplicación de un negocio puede enunciarse con la precisión suficiente para que un runtime la ejecute y un revisor la lea entera. + +## Adónde va el código + +Una definición que se ejecuta en lugar de razonarse sigue teniendo que expresar cosas que ningún esquema expresa: una probabilidad que salta al 100 % cuando se cierra un trato, un botón que desaparece una vez convertido un lead, una página de aterrizaje con un diseño. La frase del README es «el código no desaparece: se traslada al runtime, como hooks, cuerpos de acción, CEL y JSX restringido», y la página de conceptos da la razón: los cuatro «se ejecutan dentro de la valla de permisos y auditoría del runtime», y «nada de ello queda disperso por una base de código que la ontología tendría entonces que describir.» + +Un hook, del ejemplo de CRM, es una función enganchada al ciclo de vida de un objeto: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL es el lenguaje de expresiones para fórmulas, predicados y políticas. El mismo ejemplo lo usa para un campo calculado y para la visibilidad de una acción: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +El JSX restringido es la forma de escribir una página personalizada, y la restricción es lo importante: «se parsea en un árbol de UI dirigido por el servidor, nunca se ejecuta como código.» El paquete que lo hace figura en el README como un compilador que parsea y nunca ejecuta. Los cuerpos de acción son el cuarto lugar, para el trabajo del lado del servidor que una acción realiza cuando se invoca. + +El patrón en los cuatro es el mismo. El código está permitido; el código no es donde vive el negocio. El negocio vive en los seis tipos, y el código se ejecuta dentro de la valla que esos seis tipos definen, bajo los mismos permisos y la misma auditoría que un clic o una llamada a una herramienta. + +## Validada, y después ejecutada + +Dos verbos describen lo que el runtime hace con el artefacto, y la página de conceptos usa ambos a propósito. **Deriva**: «el esquema de la base de datos sobre un driver intercambiable, una API REST y en tiempo real generada, UI dirigida por el servidor para la Console, y un servidor MCP». Y **aplica**: «RBAC, seguridad a nivel de fila y de campo, compartición y auditoría se aplican por igual a una petición REST, a un clic en la Console y a la llamada a una herramienta de un agente.» + +El README muestra el primer verbo con un objeto. Declara un ticket: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +y «la API REST existe en el momento en que existe el objeto: no hay controladores que escribir»: + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +Fíjate en `sharingModel: 'private'` con su comentario. La postura de seguridad forma parte del objeto, y la puerta la exige: `os validate` «rechaza metadatos que pasan la comprobación de tipos pero fallarían en silencio en tiempo de ejecución: enlaces colgantes, predicados CEL incorrectos, postura de seguridad ausente.» Esa es la mitad validada. La mitad ejecutada es que cada superficie derivada del objeto —la tabla, el endpoint, la cuadrícula de la Console, la herramienta MCP— se comprueba contra los mismos permisos en cada llamada, de modo que un agente que llega al ticket por MCP ve exactamente lo que vería un humano con los mismos permisos. + +## Las cuatro promesas, encabezadas por «Ejecutable» + +El README reduce la línea a cuatro palabras: «Ejecutable · Escribible por AI · Operable por agentes · Tú la posees». Cada una es una afirmación con un mecanismo detrás, y el orden no es decorativo: la primera es sobre la que se apoyan las otras tres. + +**Ejecutable.** La definición es la cosa que se ejecuta. Todo lo de las dos secciones anteriores es esta promesa: un artefacto, superficies derivadas, aplicación en cada llamada, nada razonado en tiempo de ejecución. Sin ella, «escribible por AI» significaría una AI escribiendo documentación. + +**Escribible por AI.** «El agente escribe metadatos tipados, no una base de código. La puerta rechaza lo que fallaría en silencio en tiempo de ejecución, y el agente lo corrige antes de que tú lo veas.» El scaffolder «instala el paquete de skills de AI y escribe un `AGENTS.md`, de modo que tu agente empieza con las reglas del protocolo ya cargadas». Las cuatro puertas del README —tipado, validado, revisado, gobernado— son la razón de que los errores del agente no lleguen a producción, y la razón declarada de que funcione en absoluto: «los errores de un agente se convierten en texto localizado y correctivo que puede leer y corregir por sí mismo». + +**Operable por agentes.** «Los objetos son herramientas, las acciones son herramientas, los permisos deciden qué puede llamar un agente, y la auditoría registra lo que hizo.» La app en ejecución «la sirve como servidor MCP en `/api/v1/mcp`, activo por defecto», y un agente la opera «bajo los mismos permisos y RLS que un humano». La misma ontología que un agente escribió es después usada por uno, a través de la misma valla. + +**Tú la posees.** «Todo en este repositorio es el stack abierto: protocolo, microkernel, SDK, CLI y el runtime de producción, Apache-2.0 sin asteriscos de open core.» Y la propia definición «vive en tu repositorio como TypeScript ordinario, versionada en tu VCS y revisable como un diff: no un grafo guardado dentro del sistema de un proveedor.» La ontología es un archivo que tienes en la mano, no una tenencia que alquilas. + +## Lo que esto no resuelve + +Un artículo de definición debería decir dónde se detiene la definición. + +No hace fácil el modelado. Decidir qué es una oportunidad, quién puede cerrarla y qué necesita un descuento superior al 30 % sigue siendo el trabajo lento, discutido y humano que siempre fue; el formato solo decide si el resultado es lo bastante pequeño para leerse entero y lo bastante estricto para rechazar errores al escribirlo. + +No razona. Si tu problema de verdad necesita inferencia sobre un modelo de dominio —jerarquías de clases, hechos derivados, un motor que descubra consecuencias—, esa es la ontología de representación del conocimiento que esta explícitamente no es. Elige esa herramienta, y no esperes que a esta le crezca un razonador. + +No es una capa sobre lo que ya operas. Federar una fuente de datos externa es de solo lectura por defecto y se describe como temprano, así que una ontología ejecutable sobre una base de datos heredada empieza como un modelo de lectura y se gana las escrituras objeto a objeto. [Mover un sistema interno a metadatos](/en/blog/how-to-move-an-internal-system-to-metadata/) es la versión honesta de ese camino, incluidos los casos en que la respuesta es no migrar en absoluto. + +Y la lógica novedosa sigue siendo código. Los hooks y los cuerpos de acción son su sitio, dentro de la valla, y siguen siendo código que un humano tiene que revisar. La ontología hace ese código más pequeño y acotado; no lo hace desaparecer, y el README lo dice con esas mismas palabras. + +## Alojada, o en tu propia infraestructura + +El README describe el ciclo de arriba —un agente de código escribe los metadatos en tu repositorio y opera la app en ejecución por MCP— y después dice: «¿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), y en cualquiera de las dos ediciones puedes exportar tu ontología y ejecutarla en el runtime open source de ObjectStack. Que es la cuarta promesa, enunciada como un límite de producto: la ontología es tuya en cualquier host donde se ejecute. + +## Empieza por el camino del agente + +Nada de lo anterior te obliga a aceptar la definición por fe; el propio camino de cinco minutos del README produce una que puedes leer. + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +Abre el proyecto en tu agente de código y describe el requisito. El ejemplo del README es un servicio de soporte: un objeto `ticket` con asunto, descripción, un selector de prioridad y un selector de estado; una acción Resolve que solo aparece en los tickets que aún no están resueltos; una vista de lista «Open tickets» y un grupo de navegación Support; `npm run validate` cuando termine. Después previsualiza el resultado: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +Mira el diff que produjo el agente antes de mirar la Console. El objeto, la acción y su predicado de visibilidad, la vista: podrás clasificar cada línea en núcleo o proyección con la prueba de eliminación, y la puerta ya habrá rechazado las líneas que no se ejecutarían. Si el agente necesita guía, [cómo configurarlo para ObjectStack](/en/blog/how-to-point-your-agent-at-objectstack/) cubre el archivo de reglas, el paquete de skills y el servidor de herramientas. La definición que escribe es el software, y está en tu repositorio. diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx new file mode 100644 index 0000000..ee1894d --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx @@ -0,0 +1,197 @@ +--- +title: "Que signifie « L’ontologie est le logiciel » ? Une ontologie métier exécutable, définie" +description: "La phrase est le positionnement d’ObjectStack ; voici sa définition : un noyau de six sortes, quatre projections, trois choses qu’elle n’est pas, et un test pour distinguer le noyau de la projection." +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontologie + - Ontologie métier exécutable +--- + +**En bref.** « L’ontologie est le logiciel. » est la première phrase du README d’ObjectStack, et la ligne en dessous en est la définition : « Une ontologie métier exécutable. L’AI l’écrit, le runtime l’exécute, les agents l’opèrent, vous la possédez. » Une ontologie métier exécutable est la définition de votre métier qu’un runtime exécute directement. Son noyau est fait de six sortes de choses — objets et champs, relations, actions, permissions, flux, et définitions d’agents et d’outils — compilées en un artefact, dont dérivent la base de données, l’API, l’UI et les outils d’agent. Les vues, les tableaux de bord, les apps et les traductions en sont des projections, pas des parties. Ce n’est pas une ontologie de représentation des connaissances, pas une couche sémantique posée sur vos systèmes existants, et pas une ontologie formelle au sens philosophique. Le code ne disparaît pas ; il se déplace dans le runtime, sous forme de hooks, de corps d’action, de CEL et de JSX contraint. + +C’est toute l’affirmation. La suite de ce billet la démonte : pourquoi la phrase dit *est* et non *décrit*, les six sortes, les projections et un test pour les distinguer, les trois négations, où va le code, et les quatre promesses auxquelles la ligne se réduit. Les sources sont le README et la page de concepts qu’il lie, citées plutôt que paraphrasées, ainsi que l’exemple de CRM livré dans le même dépôt, mesuré plutôt que décrit. + +## Pourquoi la phrase dit « est » + +La plupart des ontologies d’entreprise décrivent du logiciel. Elles se tiennent à côté des systèmes qui font réellement tourner le métier — un modèle du client, un graphe de ce qui se relie à quoi, une définition gouvernée de ce que signifie le chiffre d’affaires — et les systèmes continuent dessous, sans le savoir. La description est utile, et elle est aussi facultative : supprimez-la et rien ne s’arrête. + +La phrase d’ObjectStack est formulée comme une identité parce que la relation est inversée. La définition est la chose qui tourne. Supprimez l’ontologie et il ne reste aucune application : pas de tables, pas d’endpoints, pas d’écrans, pas d’outils qu’un agent puisse appeler. La page de concepts met le pipeline sur une ligne : + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +Vous rédigez des métadonnées typées dans `objectstack.config.ts` ; elles se compilent en l’artefact `objectstack.json` ; le runtime lit l’artefact et dérive le reste. La légende du README pour la même image est plus courte encore : « Une définition typée → base de données · REST API · SDK client · UI · outils MCP. » Il n’y a pas de second artefact qui *soit* le logiciel pendant que l’ontologie se contente d’en parler. Voilà ce que « est » veut dire ici, et tout ce qui suit en découle. + +## Le noyau de six sortes + +La page de concepts définit le noyau comme six sortes de choses. Le tableau ci-dessous garde sa formulation dans la colonne centrale et, dans la colonne de droite, indique où chaque sorte vit dans l’exemple de CRM livré (`examples/app-crm` dans le dépôt ObjectStack), pour que la définition puisse être vérifiée contre une définition réelle plutôt que contre un slogan. + +| Noyau | Ce qu’elle déclare | Dans l’exemple de CRM livré | +| :--- | :--- | :--- | +| **Objets et champs** | Les entités du métier et leurs attributs typés — validation, formules, valeurs par défaut, fichiers | `src/objects/` : compte, contact, lead, opportunité, ligne d’opportunité, activité | +| **Relations** | Les lookups et les liens maître-détail entre objets | `Field.lookup('crm_account', …)` sur l’opportunité ; la ligne en est l’enfant maître-détail | +| **Actions** | Les opérations qu’un utilisateur ou un agent peut invoquer sur un enregistrement, et les conditions sous lesquelles elles apparaissent | `src/actions/convert-lead.action.ts`, visible seulement tant que le lead n’est pas encore converti | +| **Permissions** | Qui peut lire, créer, modifier et supprimer quoi — RBAC, sécurité au niveau ligne et champ, partage | `src/security/sales-positions.ts` : un ensemble `crm_sales_user`, et un profil invité qui ne peut qu’insérer des leads | +| **Flux** | Les processus métier — flux DAG, déclencheurs d’enregistrement, tâches planifiées, approbations, workflows | `src/flows/convert-lead.flow.ts`, un flux d’écran qui crée le compte et l’opportunité, puis marque le lead comme converti | +| **Définitions d’agents et d’outils** | Les agents d’AI livrés avec l’app, les outils qu’ils peuvent appeler, et les skills qui les façonnent | Aucune déclarée dans cet exemple. Chaque objet est déjà un outil MCP ; les actions s’inscrivent avec `ai: { exposed: true }` | + +Une mesure, prise avec la commande que le README imprime lui-même sous « measure it yourself » : au commit `9f9510f2`, la définition de l’exemple de CRM fait **1 917 lignes de TypeScript réparties sur 31 fichiers**. Ces fichiers contiennent six objets, trois vues, un tableau de bord, une app, un flux de conversion de lead, une action, un hook, deux ensembles de permissions, trois positions, un paquet de traduction, une page et les données d’amorçage — l’application entière, sous forme d’un diff que vous pouvez lire. L’intérêt du nombre n’est pas qu’il soit petit dans l’absolu. C’est qu’un agent peut tenir toute la définition d’un coup et répondre « qu’est-ce qui casse si je change ceci ? » à partir de la définition elle-même, ce que le README appelle des apps assez petites pour que l’AI les tienne entières. + +Le noyau est aussi, par construction, la partie qui est à vous. La page de concepts : « C’est une seule définition, ouverte et versionnée. Elle vit dans votre dépôt comme du TypeScript ordinaire, se relit comme un diff, et est spécifiée et licenciée sous Apache-2.0 — pas un graphe détenu dans le système d’un fournisseur et accessible uniquement par la console de ce fournisseur. » + +## Les projections, et le test de suppression + +Les vues, les tableaux de bord, les apps et les traductions sont rédigés dans les mêmes métadonnées et validés par la même porte, mais ils ne font pas partie de l’ontologie. La page de concepts les appelle des projections et explique chacune en une proposition : « Une vue liste projette les champs et les permissions d’un objet sur une grille ; un tableau de bord projette des requêtes sur des objets en graphiques ; une app regroupe des objets, des vues et des actions en un produit navigable ; une traduction projette chaque libellé dans une langue. » + +![Le noyau de six sortes se compile, à travers le runtime ObjectStack, en base de données, API, UI de la Console et outils MCP ; les vues, les tableaux de bord, les apps et les traductions se tiennent hors de la boîte de l’ontologie comme projections, et les hooks, les corps d’action, CEL et le JSX contraint s’exécutent à l’intérieur de la clôture du runtime](./six-kind-core-en.svg) + +La phrase qui rend la distinction opérationnelle est celle-ci : « Changez le noyau et les projections suivent ; retirez une projection et le métier qu’elle montrait est inchangé. » Transformez-la en test et vous pourrez classer n’importe quelle définition d’un projet en un seul geste. Appelons-le le **test de suppression** : + +> Supprimez la définition. Si le schéma de base de données, l’API, ou ce qu’un utilisateur ou un agent peut faire a changé, c’était du noyau. Si seul un écran, un graphique, un menu ou un libellé a disparu, c’était une projection. + +Lisez-le contre l’exemple de CRM. Supprimez `src/views/opportunity.view.ts` et l’objet opportunité, ses champs, ses lookups et son ensemble de permissions sont tous encore là ; l’endpoint REST répond toujours ; l’outil MCP existe toujours. Supprimez `src/objects/opportunity.object.ts` et la vue n’a plus rien à projeter, le flux n’a plus rien à créer, et l’ensemble de permissions nomme un objet qui n’existe plus. L’un était une projection ; l’autre était le logiciel. + +Pourquoi la distinction mérite un nom est dit sur la même page : « La distinction borne ce qu’un agent doit tenir pour raisonner sur le métier : le noyau, pas chaque écran. » Un agent qui relit une modification d’un seuil d’approbation a besoin d’avoir devant lui les objets, les permissions et le flux. Il n’a pas besoin de chaque tableau de bord qui se trouve représenter le résultat. + +## Trois choses qu’elle n’est pas + +Le mot *ontologie* porte des décennies de sens venus d’autres domaines, et le README consacre une bonne part de sa définition à dire lesquels de ces sens ne s’appliquent pas. Il y a trois négations, et elles sont différentes. + +| Elle n’est pas | Ce qu’est cette chose | Pourquoi c’est différent ici | +| :--- | :--- | :--- | +| **Une ontologie de représentation des connaissances** | OWL, RDF et les graphes de connaissances *décrivent* un domaine, portent l’héritage de classes et des axiomes, et sont *raisonnés* par un moteur d’inférence | Pas d’héritage d’objets, pas d’axiomes, pas de raisonneur. La définition est validée — par des schémas Zod et `os validate` — puis exécutée. La clé `abstract` a été retirée de la spécification exactement pour cette raison (ADR-0049) | +| **Une couche sémantique sur vos systèmes existants** | Une correspondance en lecture seule au-dessus de systèmes que vous exploitez déjà : elle décrit le métier mais ne l’exécute pas | L’ontologie *est* le système : la base de données, l’API, l’UI et les outils d’agent en dérivent. Fédérer une source de données externe est possible, en lecture seule par défaut, et encore précoce | +| **Une ontologie formelle au sens philosophique** | Une affirmation sur ce qui existe en général | Elle ne fait aucune affirmation de ce genre. C’est l’application d’un seul métier, définie assez précisément pour tourner | + +![Trois colonnes : une ontologie de représentation des connaissances est décrite et raisonnée, une couche sémantique est une correspondance en lecture seule sur des systèmes existants, et une ontologie métier exécutable est validée puis exécutée en une base de données, une API, une UI et des outils d’agent](./validated-not-reasoned-en.svg) + +La première négation est celle qu’on manque le plus souvent, parce que c’est un choix de conception et non une limite. Un moteur d’inférence est une seconde source de vérité : les faits qu’il dérive à l’exécution n’ont jamais été écrits nulle part où un relecteur pourrait les lire. La règle d’ObjectStack, sur la page de concepts, est l’inverse : « Rien n’est raisonné à l’exécution : ce qui tourne est ce qui a été validé. » Si une permission n’est pas dans le diff, elle n’est pas dans le système. + +La seconde négation compte pour quiconque évalue des plateformes, parce que le terme *couche sémantique* sert à désigner deux choses différentes dans les mêmes conversations. Une couche sémantique peut être une excellente définition de ce que signifie un nombre — la [comparaison avec les graphes de connaissances](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/) la défend loyalement. C’est une couche de noms. Une ontologie métier exécutable porte les verbes : des actions, des permissions et des flux qui décident ce qui peut être fait, par qui, et ce qui se passe ensuite. La documentation note aussi qu’elle emploie *couche sémantique* dans un sens plus étroit, pour la couche de datasets analytiques à laquelle rapports et tableaux de bord se lient ; ce sens étroit concerne le reporting, et ce n’est pas ce que la phrase veut dire. + +La troisième négation est la plus courte et la plus honnête. Le format ne prétend rien sur le monde. Il affirme que l’application d’un seul métier peut être énoncée assez précisément pour qu’un runtime l’exécute et qu’un relecteur la lise en entier. + +## Où va le code + +Une définition qui est exécutée plutôt que raisonnée doit encore exprimer des choses qu’aucun schéma n’exprime : une probabilité qui passe à 100 % quand une affaire est conclue, un bouton qui disparaît une fois un lead converti, une page d’accueil avec une mise en page. La phrase du README est « le code ne disparaît pas : il se déplace dans le runtime, sous forme de hooks, de corps d’action, de CEL et de JSX contraint », et la page de concepts en donne la raison : les quatre « s’exécutent à l’intérieur de la clôture de permissions et d’audit du runtime », et « rien de tout cela n’est dispersé dans une base de code que l’ontologie devrait ensuite décrire. » + +Un hook, tiré de l’exemple de CRM, est une fonction attachée au cycle de vie d’un objet : + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL est le langage d’expressions des formules, des prédicats et des politiques. Le même exemple l’utilise pour un champ calculé et pour la visibilité d’une action : + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +Le JSX contraint est la façon d’écrire une page personnalisée, et la contrainte est tout l’enjeu : il « est analysé en un arbre d’UI piloté par le serveur, jamais exécuté comme du code. » Le paquet qui s’en charge est listé dans le README comme un compilateur qui analyse et n’exécute jamais. Les corps d’action sont le quatrième lieu, pour le travail côté serveur qu’une action effectue quand elle est invoquée. + +Le motif, pour les quatre, est le même. Le code est permis ; le code n’est pas là où vit le métier. Le métier vit dans les six sortes, et le code s’exécute à l’intérieur de la clôture que ces six sortes définissent, sous les mêmes permissions et le même audit qu’un clic ou un appel d’outil. + +## Validée, puis exécutée + +Deux verbes décrivent ce que le runtime fait de l’artefact, et la page de concepts les emploie tous deux à dessein. Il **dérive** : « le schéma de base de données sur un pilote interchangeable, une API REST et temps réel générée, une UI pilotée par le serveur pour la Console, et un serveur MCP ». Et il **applique** : « RBAC, sécurité au niveau ligne et champ, partage et audit s’appliquent de la même façon à une requête REST, à un clic dans la Console et à l’appel d’outil d’un agent. » + +Le README montre le premier verbe avec un seul objet. Déclarez un ticket : + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +et « l’API REST existe dès l’instant où l’objet existe — aucun contrôleur à écrire » : + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +Remarquez `sharingModel: 'private'` et son commentaire. La posture de sécurité fait partie de l’objet, et la porte l’exige : `os validate` « rejette les métadonnées qui passent la vérification de types mais échoueraient silencieusement à l’exécution : liaisons pendantes, prédicats CEL erronés, posture de sécurité manquante. » C’est la moitié validée. La moitié exécutée, c’est que chaque surface dérivée de l’objet — la table, l’endpoint, la grille de la Console, l’outil MCP — est vérifiée contre les mêmes permissions à chaque appel, de sorte qu’un agent qui atteint le ticket par MCP voit exactement ce qu’un humain avec les mêmes permissions verrait. + +## Les quatre promesses, menées par « Exécutable » + +Le README réduit la ligne à quatre mots : « Exécutable · Rédigeable par l’AI · Opérable par les agents · Vous la possédez ». Chacun est une affirmation avec un mécanisme derrière, et l’ordre n’est pas décoratif — le premier est ce sur quoi les trois autres reposent. + +**Exécutable.** La définition est la chose qui tourne. Tout ce qui précède dans les deux sections ci-dessus est cette promesse : un artefact, des surfaces dérivées, l’application à chaque appel, rien de raisonné à l’exécution. Sans elle, « rédigeable par l’AI » voudrait dire une AI qui rédige de la documentation. + +**Rédigeable par l’AI.** « L’agent écrit des métadonnées typées — pas une base de code. La porte rejette ce qui échouerait silencieusement à l’exécution, et l’agent le corrige avant même que vous le voyiez. » Le scaffolder « installe le paquet de skills d’AI et écrit un `AGENTS.md`, de sorte que votre agent démarre avec les règles du protocole déjà chargées ». Les quatre portes du README — typée, validée, relue, gouvernée — sont la raison pour laquelle les erreurs de l’agent ne sont pas livrées, et la raison énoncée pour laquelle cela fonctionne tout court : « les erreurs d’un agent deviennent un texte localisé et correctif qu’il peut lire et corriger lui-même ». + +**Opérable par les agents.** « Les objets sont des outils, les actions sont des outils, les permissions décident ce qu’un agent peut appeler, et l’audit enregistre ce qu’il a fait. » L’app en cours d’exécution « la sert comme serveur MCP sur `/api/v1/mcp` — activé par défaut », et un agent l’opère « sous les mêmes permissions et la même RLS qu’un humain ». La même ontologie qu’un agent a écrite est ensuite utilisée par un agent, à travers la même clôture. + +**Vous la possédez.** « Tout dans ce dépôt est la pile ouverte — protocole, micro-noyau, SDK, CLI et le runtime de production, sous Apache-2.0 sans astérisque open-core. » Et la définition elle-même « vit dans votre dépôt comme du TypeScript ordinaire, versionnée dans votre VCS et relisible comme un diff — pas un graphe détenu dans le système d’un fournisseur. » L’ontologie est un fichier que vous tenez, pas une location que vous payez. + +## Ce que cela ne résout pas + +Un billet de définition devrait dire où la définition s’arrête. + +Elle ne rend pas la modélisation facile. Décider ce qu’est une opportunité, qui peut la conclure et ce qu’exige une remise au-delà de 30 % reste le travail lent, disputé et humain que cela a toujours été ; le format décide seulement si le résultat est assez petit pour être lu en entier et assez strict pour rejeter les erreurs au moment de la rédaction. + +Elle ne raisonne pas. Si votre problème a réellement besoin d’inférence sur un modèle de domaine — hiérarchies de classes, faits dérivés, un moteur qui découvre des conséquences — c’est l’ontologie de représentation des connaissances que celle-ci n’est explicitement pas. Choisissez cet outil-là, et n’attendez pas de celui-ci qu’il se dote d’un raisonneur. + +Elle n’est pas une couche sur ce que vous exploitez déjà. Fédérer une source de données externe est en lecture seule par défaut et décrit comme précoce, si bien qu’une ontologie exécutable sur une base héritée commence comme un modèle de lecture et gagne les écritures objet par objet. [Migrer un système interne vers des métadonnées](/en/blog/how-to-move-an-internal-system-to-metadata/) est la version honnête de ce chemin, y compris les cas où la réponse est de ne pas migrer du tout. + +Et la logique inédite reste du code. Les hooks et les corps d’action sont son lieu, à l’intérieur de la clôture, et c’est encore du code qu’un humain doit relire. L’ontologie rend ce code plus petit et borné ; elle ne le fait pas disparaître, et le README le dit en ces termes. + +## Hébergée, ou sur votre propre infrastructure + +Le README décrit la boucle ci-dessus — un agent de code écrit les métadonnées dans votre dépôt et opère l’app en cours d’exécution via MCP — puis dit : « 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) ; dans les deux éditions, vous pouvez exporter votre ontologie et l’exécuter sur le runtime open source ObjectStack. C’est la quatrième promesse, énoncée comme une frontière de produit : l’ontologie est à vous sur chaque hôte où elle tourne. + +## Commencez par le chemin de l’agent + +Rien de ce qui précède ne vous demande de prendre la définition sur parole ; le chemin de cinq minutes du README lui-même en produit une que vous pouvez lire. + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +Ouvrez le projet dans votre agent de code et décrivez le besoin. L’exemple du README est un centre de support : un objet `ticket` avec un sujet, une description, un sélecteur de priorité et un sélecteur de statut ; une action Resolve qui n’apparaît que sur les tickets pas encore résolus ; une vue liste « Open tickets » et un groupe de navigation Support ; `npm run validate` quand c’est terminé. Puis prévisualisez le résultat : + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +Regardez le diff produit par l’agent avant de regarder la Console. L’objet, l’action et son prédicat de visibilité, la vue — vous pourrez classer chaque ligne en noyau ou projection avec le test de suppression, et la porte aura déjà refusé les lignes qui ne tourneraient pas. Si l’agent a besoin d’être guidé, [comment le configurer pour ObjectStack](/en/blog/how-to-point-your-agent-at-objectstack/) couvre le fichier de règles, le paquet de skills et le serveur d’outils. La définition qu’il écrit est le logiciel, et elle est dans votre dépôt. diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx new file mode 100644 index 0000000..3d74745 --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx @@ -0,0 +1,197 @@ +--- +title: "「オントロジーがソフトウェアである」とはどういう意味か?実行可能なビジネスオントロジーの定義" +description: "この一文は ObjectStack のポジショニングであり、本稿はその定義だ。実行可能なビジネスオントロジーには六種の中核、四つの投影、三つの「ではないもの」があり、中核と投影を見分けるテストがある。" +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - オントロジー + - 実行可能なビジネスオントロジー +--- + +**要点**:「オントロジーがソフトウェアである。」は ObjectStack README の最初の一文で、その下の行が定義だ:「実行可能なビジネスオントロジーをひとつ。AI が書き、ランタイムが動かし、エージェントが操作し、あなたが所有する。」実行可能なビジネスオントロジーとは、ランタイムが直接実行する、あなたのビジネスの定義である。その中核は六種のもの——オブジェクトとフィールド、リレーション、アクション、パーミッション、フロー、エージェントとツールの定義——で、ひとつのアーティファクトにコンパイルされ、そこからデータベース、API、UI、エージェントツールが導出される。ビュー、ダッシュボード、アプリ、翻訳はその投影であって、その一部ではない。知識表現のオントロジーではなく、既存システムの上に載せるセマンティックレイヤーでもなく、哲学的な意味での形式オントロジーでもない。コードは消えない。フック、アクション本体、CEL、制約付き JSX として、ランタイムの中へ移る。 + +主張はこれで全部だ。本稿はそれを分解する:なぜこの文は「である」と言い、「記述する」と言わないのか。六種の中核。投影と、両者を見分けるテスト。三つの否定。コードの行き先。そして、この一文が帰着する四つの約束。出典は README とそこからリンクされた概念ページで、言い換えずに引用する。同じリポジトリに同梱された CRM サンプルは、説明ではなく計測で示す。 + +## なぜ「である」と言うのか + +企業向けオントロジーの多くは、ソフトウェアを記述する。実際にビジネスを動かしているシステムの脇に置かれ——顧客のモデル、何と何がつながるかのグラフ、売上の意味を統制した定義——その下でシステムは何も知らずに動き続ける。記述は有用だが、任意でもある。消しても何も止まらない。 + +ObjectStack の一文が同一性の形で書かれているのは、関係が逆だからだ。定義そのものが、動いているものである。オントロジーを消せばアプリケーションは残らない。テーブルも、エンドポイントも、画面も、エージェントが呼べるツールもない。概念ページはパイプラインを一行で示す: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +型付きメタデータを `objectstack.config.ts` に書く。それが `objectstack.json` アーティファクトにコンパイルされる。ランタイムがアーティファクトを読み、残りを導出する。README の同じ図のキャプションはさらに短い:「型付き定義ひとつ → データベース · REST API · クライアント SDK · UI · MCP ツール。」オントロジーが語るだけで、別のアーティファクトがソフトウェア*である*、ということはない。ここでの「である」はそういう意味で、以下のすべてはそこから導かれる。 + +## 六種の中核 + +概念ページは中核を六種のものとして定義する。下の表は中央の列にその文言をそのまま残し、右の列で各種が同梱 CRM サンプル(ObjectStack リポジトリの `examples/app-crm`)のどこにあるかを指す。定義をスローガンではなく、実際の定義と照らして検証できるようにするためだ。 + +| 中核 | 宣言するもの | 同梱 CRM サンプルでは | +| :--- | :--- | :--- | +| **オブジェクトとフィールド** | ビジネスの実体と、その型付き属性——バリデーション、数式、デフォルト値、ファイル | `src/objects/`:取引先、担当者、リード、商談、商談明細、活動 | +| **リレーション** | オブジェクト間のルックアップと主従(master-detail)のリンク | 商談の `Field.lookup('crm_account', …)`。商談明細はその主従の子 | +| **アクション** | ユーザーまたはエージェントがレコードに対して起動できる操作と、それが表示される条件 | `src/actions/convert-lead.action.ts`。リードが未転換の間だけ表示される | +| **パーミッション** | 誰が何を読み、作成し、編集し、削除できるか——RBAC、行レベルとフィールドレベルのセキュリティ、共有 | `src/security/sales-positions.ts`:`crm_sales_user` セットと、リードの追加しかできないゲストプロファイル | +| **フロー** | ビジネスプロセス——DAG フロー、レコードトリガー、スケジュールジョブ、承認、ワークフロー | `src/flows/convert-lead.flow.ts`。取引先と商談を作成し、リードを転換済みにする画面フロー | +| **エージェントとツールの定義** | アプリに同梱される AI エージェント、それらが呼べるツール、それらを形作るスキル | このサンプルでは未宣言。すべてのオブジェクトは既に MCP ツールであり、アクションは `ai: { exposed: true }` でオプトインする | + +計測をひとつ。README 自身が「自分で測ってみよう」の下に印字しているコマンドで測った。コミット `9f9510f2` において、CRM サンプルの定義は **31 ファイル、1,917 行の TypeScript** だ。その中に、六つのオブジェクト、三つのビュー、ひとつのダッシュボード、ひとつのアプリ、リード転換フロー、ひとつのアクション、ひとつのフック、二つのパーミッションセット、三つのポジション、ひとつの翻訳バンドル、ひとつのページ、そしてシードデータが入っている——アプリケーション全体が、読み切れる diff として。この数字の意味は、抽象的に小さいことではない。エージェントが定義全体を一度に保持し、「ここを変えたら何が壊れるか」を定義そのものから答えられることにある。README はそれを「AI が丸ごと保持できる大きさのアプリ」と呼ぶ。 + +中核は同時に、構造上、あなたのものである部分だ。概念ページはこう書く:「それはひとつの定義であり、オープンでバージョン管理される。通常の TypeScript としてあなたのリポジトリに置かれ、diff としてレビューでき、Apache-2.0 で仕様化・ライセンスされる——ベンダーのシステム内に保持され、そのベンダーのコンソールからしか届かないグラフではない。」 + +## 投影と、削除テスト + +ビュー、ダッシュボード、アプリ、翻訳は同じメタデータで書かれ、同じゲートで検証されるが、オントロジーの一部ではない。概念ページはこれらを投影と呼び、それぞれを一節で説明する:「リストビューはオブジェクトのフィールドとパーミッションをグリッドに投影する。ダッシュボードはオブジェクトへのクエリをチャートに投影する。アプリはオブジェクト、ビュー、アクションをナビゲートできる製品にまとめる。翻訳はすべてのラベルをロケールに投影する。」 + +![六種の中核が ObjectStack ランタイムを通じてデータベース、API、Console UI、MCP ツールにコンパイルされる。ビュー、ダッシュボード、アプリ、翻訳は投影としてオントロジーの枠の外にあり、フック、アクション本体、CEL、制約付き JSX はランタイムのフェンスの内側で動く](./six-kind-core-en.svg) + +区別を運用可能にするのはこの一文だ:「中核を変えれば投影は追従する。投影を取り除いても、それが示していたビジネスは変わらない。」これをテストにすれば、プロジェクト内のどんな定義も一手で分類できる。**削除テスト**と呼ぼう: + +> その定義を削除する。データベーススキーマ、API、あるいはユーザーやエージェントにできることが変わったなら、それは中核だった。画面、チャート、メニュー、ラベルが消えただけなら、それは投影だった。 + +CRM サンプルに当ててみる。`src/views/opportunity.view.ts` を削除しても、商談オブジェクト、そのフィールド、ルックアップ、パーミッションセットはすべて残る。REST エンドポイントは応答し続け、MCP ツールも存在し続ける。`src/objects/opportunity.object.ts` を削除すると、ビューには投影するものがなく、フローには作成するものがなく、パーミッションセットはもう存在しないオブジェクトを指す。前者は投影で、後者がソフトウェアだった。 + +この区別に名前を付ける価値がある理由も同じページに書いてある:「この区別は、エージェントがビジネスについて推論するために保持すべきものを限定する:中核であって、すべての画面ではない。」承認しきい値の変更をレビューするエージェントに必要なのは、オブジェクト、パーミッション、フローだ。結果をたまたまチャートにしているダッシュボード全部ではない。 + +## それではない三つのもの + +「オントロジー」という語は他分野から何十年分もの意味を背負っており、README の定義のかなりの部分は、そのうちどの意味が当てはまらないかを述べることに費やされている。否定は三つあり、それぞれ異なる。 + +| それではないもの | そのものとは何か | なぜ違うのか | +| :--- | :--- | :--- | +| **知識表現のオントロジー** | OWL、RDF、ナレッジグラフは領域を*記述*し、クラス継承と公理を持ち、推論エンジンによって*推論される* | オブジェクト継承なし、公理なし、推論器なし。定義は検証され——Zod スキーマと `os validate` によって——そして実行される。`abstract` キーがまさにその理由で仕様から取り除かれた(ADR-0049) | +| **既存システムの上に載せるセマンティックレイヤー** | 既に運用しているシステムの上の読み取り専用のマッピング。ビジネスを記述するが、動かしはしない | オントロジーがシステム*である*。データベース、API、UI、エージェントツールはそこから導出される。外部データソースのフェデレーションは可能で、デフォルトで読み取り専用、まだ初期段階 | +| **哲学的な意味での形式オントロジー** | 一般に何が存在するかについての主張 | そうした主張はしない。ひとつの企業のアプリケーションを、実行できるだけ正確に定義したものだ | + +![三列の対比:知識表現オントロジーは記述され推論される。セマンティックレイヤーは既存システムの上の読み取り専用マッピング。実行可能なビジネスオントロジーは検証されてから実行され、データベース、API、UI、エージェントツールになる](./validated-not-reasoned-en.svg) + +最初の否定が最も見落とされやすい。制限ではなく設計上の選択だからだ。推論エンジンは第二の真実の源である。実行時に導出される事実は、レビュアーが読める場所にはどこにも書かれていない。概念ページにある ObjectStack のルールはその逆だ:「実行時には何も推論されない。動くのは、検証されたものである。」パーミッションが diff にないなら、それはシステムにない。 + +二つ目の否定は、プラットフォームを評価する人にとって重要だ。「セマンティックレイヤー」という語が、同じ会話の中で二つの異なるものに使われているからである。セマンティックレイヤーは、ある数字が何を意味するかの優れた定義でありうる——[ナレッジグラフとの比較](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/)は公平にその擁護をしている。それは名詞のレイヤーだ。実行可能なビジネスオントロジーは動詞を運ぶ。何をしてよいか、誰が、次に何が起きるかを決める、アクション、パーミッション、フローである。ドキュメントは、レポートとダッシュボードがバインドする分析データセット層を指して「セマンティックレイヤー」をより狭い意味でも使うと注記している。その狭い意味はレポーティングの話であって、この一文の意味ではない。 + +三つ目の否定は最も短く、最も正直だ。このフォーマットは世界について主張しない。主張するのは、ひとつの企業のアプリケーションを、ランタイムが実行でき、レビュアーが丸ごと読めるだけ正確に記述できる、ということだ。 + +## コードの行き先 + +推論されるのではなく実行される定義にも、スキーマでは表せないものを表現する必要はある。案件成立時に 100% に固定される確度、リード転換後に消えるボタン、レイアウトを持つランディングページ。README の一文は「コードは消えない。フック、アクション本体、CEL、制約付き JSX として、ランタイムの中へ移る」で、概念ページがその理由を述べる。四つすべてが「ランタイムのパーミッションと監査のフェンスの内側で動く」のであり、「そのどれも、オントロジーが後から記述しなければならないコードベースに散らばってはいない」。 + +CRM サンプルのフックは、オブジェクトのライフサイクルに取り付けられた関数だ: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL は数式、述語、ポリシーのための式言語だ。同じサンプルは、計算フィールドとアクションの表示条件の両方にそれを使う: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +制約付き JSX はカスタムページの書き方で、制約こそが要点だ。それは「サーバー駆動の UI ツリーに解析され、コードとして実行されることは決してない」。それを行うパッケージは README に、解析はするが決して実行しないコンパイラとして列挙されている。アクション本体が四つ目の行き先で、アクションが起動されたときにサーバー側で行う作業を担う。 + +四つに共通するパターンは同じだ。コードは許される。だがコードはビジネスの居場所ではない。ビジネスは六種の中核に住み、コードはその六種が定めるフェンスの内側で、クリックやツール呼び出しと同じパーミッションと監査のもとで動く。 + +## 検証し、そして実行する + +ランタイムがアーティファクトに対して行うことは二つの動詞で表され、概念ページは意図的に両方を使う。それは**導出する**:「交換可能なドライバー上のデータベーススキーマ、生成された REST とリアルタイムの API、Console 向けのサーバー駆動 UI、そして MCP サーバー」。そして**強制する**:「RBAC、行レベルとフィールドレベルのセキュリティ、共有、監査は、REST リクエストにも、Console のクリックにも、エージェントのツール呼び出しにも等しく適用される。」 + +README は最初の動詞をオブジェクトひとつで示す。チケットを宣言する: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +すると「REST API はオブジェクトが存在した瞬間に存在する——書くべきコントローラーはない」: + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +コメント付きの `sharingModel: 'private'` に注目してほしい。セキュリティの姿勢はオブジェクトの一部であり、ゲートがそれを要求する。`os validate` は「型チェックは通るが実行時に静かに失敗するメタデータを拒否する:宙に浮いたバインディング、誤った CEL 述語、欠けたセキュリティ姿勢」。これが検証の半分だ。実行の半分は、このオブジェクトから導出されたすべての面——テーブル、エンドポイント、Console のグリッド、MCP ツール——が、呼び出しのたびに同じパーミッションに照らして検査されることにある。だから MCP 経由でチケットに届いたエージェントが見るのは、同じパーミッションを持つ人間が見るものと正確に同じだ。 + +## 四つの約束、先頭は「実行可能」 + +README はこの一文を四つの語に圧縮する:「実行可能 · AI が書ける · エージェントが操作できる · あなたが所有する」。それぞれが仕組みに裏付けられた主張で、順序は飾りではない。最初のひとつが、残り三つの足場だ。 + +**実行可能。** 定義が、動いているものそのものである。上の二つの節の内容すべてがこの約束だ。ひとつのアーティファクト、導出される面、呼び出しごとの強制、実行時に推論されるものは何もない。これがなければ、「AI が書ける」は AI がドキュメントを書くという意味になってしまう。 + +**AI が書ける。**「エージェントは型付きメタデータを書く——コードベースではない。ゲートは実行時に静かに失敗するものを拒否し、エージェントはあなたの目に入る前にそれを直す。」スキャフォルダーは「AI スキルバンドルをインストールして `AGENTS.md` を書くので、あなたのエージェントはプロトコルのルールを最初から読み込んだ状態で始まる」。README の四つのゲート——型付き、検証済み、レビュー済み、統制済み——がエージェントの誤りを出荷させない理由であり、そもそもこれが機能する理由として述べられているのが「エージェントの誤りは、位置が特定された、訂正のためのテキストになり、エージェント自身がそれを読んで直せる」だ。 + +**エージェントが操作できる。**「オブジェクトはツールであり、アクションはツールであり、パーミッションがエージェントに何を呼べるかを決め、監査が何をしたかを記録する。」動いているアプリは「`/api/v1/mcp` で MCP サーバーとして提供される——デフォルトでオン」であり、エージェントは「人間と同じパーミッションと RLS のもとで」それを操作する。エージェントが書いた同じオントロジーが、同じフェンスを通って、次はエージェントに使われる。 + +**あなたが所有する。**「このリポジトリにあるものはすべてオープンスタックだ——プロトコル、マイクロカーネル、SDK、CLI、そして本番ランタイム。Apache-2.0 で、オープンコアの但し書きはない。」そして定義そのものは「通常の TypeScript としてあなたのリポジトリに置かれ、あなたの VCS でバージョン管理され、diff としてレビューできる——ベンダーのシステム内に保持されるグラフではない」。オントロジーはあなたが手に持つファイルであって、借りているテナントではない。 + +## これが解決しないこと + +定義の記事は、定義がどこで止まるかを言うべきだ。 + +モデリングを簡単にはしない。商談とは何か、誰がそれを成約にできるか、30% を超える値引きに何が必要か——それを決めるのは、相変わらず遅く、議論を呼び、人の手による仕事だ。フォーマットが決めるのは、結果が丸ごと読めるほど小さいか、作成時に誤りを拒めるほど厳格か、だけである。 + +推論はしない。あなたの問題が本当に領域モデル上の推論——クラス階層、導出される事実、帰結を発見するエンジン——を必要とするなら、それは本稿が明確に「ではない」と言った知識表現のオントロジーだ。そちらのツールを選ぶべきで、こちらに推論器が生えてくることを期待してはいけない。 + +既に運用しているものの上に載せる層ではない。外部データソースのフェデレーションはデフォルトで読み取り専用で、初期段階と記述されている。だからレガシーデータベースの上の実行可能オントロジーは読み取りモデルから始まり、オブジェクトごとに書き込みを獲得していく。[社内システムをメタデータへ移す](/en/blog/how-to-move-an-internal-system-to-metadata/)はその道筋の正直な版で、まったく移さないのが答えになる場合も含む。 + +そして、新規性のあるロジックはコードのままだ。その行き先がフックとアクション本体で、フェンスの内側にあり、それでも人間がレビューすべきコードである。オントロジーはそのコードを小さく、境界のあるものにする。消しはしない。README はそのとおりの言葉でそう言っている。 + +## ホスト型で、あるいは自社インフラで + +README は上のループ——コーディングエージェントがあなたのリポジトリにメタデータを書き、動いているアプリを MCP 越しに操作する——を記述したあと、こう言う:「同じループをホスト型で、ブラウザだけで、何もインストールせずに使いたい?それが ObjectOS — ObjectStack の上に構築された商用ランタイム環境です。」ObjectOS は、ブラウザ上でホストされインストール不要の ObjectOS Cloud としても、自社インフラでセルフマネージドの ObjectOS Enterprise としても動作します。どちらのエディションでも、オントロジーをエクスポートしてオープンソースの ObjectStack ランタイムで実行できます。これは四つ目の約束を製品の境界として述べたものだ。どのホストで動いていても、オントロジーはあなたのものである。 + +## エージェントの道筋から始める + +上のどれも、定義を信じて受け取る必要はない。README 自身の五分間の道筋が、読める定義をひとつ作ってくれる。 + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +プロジェクトをコーディングエージェントで開き、要件を説明する。README の例はサポートデスクだ。件名、説明、優先度の選択、ステータスの選択を持つ `ticket` オブジェクト。まだ解決していないチケットにだけ表示される Resolve アクション。「未解決チケット」のリストビューと Support ナビゲーショングループ。終わったら `npm run validate`。それから結果をプレビューする: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +Console を見る前に、エージェントが生成した diff を見てほしい。オブジェクト、アクションとその表示述語、ビュー——削除テストで一行ずつ中核か投影かに仕分けでき、動かない行はゲートが既に拒んでいる。エージェントに舵取りが必要なら、[ObjectStack 向けに設定する方法](/en/blog/how-to-point-your-agent-at-objectstack/)がルールファイル、スキルバンドル、ツールサーバーを扱っている。エージェントが書くその定義がソフトウェアであり、それはあなたのリポジトリにある。 diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx new file mode 100644 index 0000000..eda1244 --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx @@ -0,0 +1,197 @@ +--- +title: "「온톨로지가 곧 소프트웨어다」는 무슨 뜻인가? 실행 가능한 비즈니스 온톨로지의 정의" +description: "이 문장은 ObjectStack의 포지셔닝이고, 이 글은 그 정의다. 실행 가능한 비즈니스 온톨로지는 여섯 종류의 핵심, 네 가지 투영, 그것이 아닌 세 가지로 이루어지며, 핵심과 투영을 가르는 테스트가 있다." +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - 온톨로지 + - 실행 가능한 비즈니스 온톨로지 +--- + +**짧은 버전.** 「온톨로지가 곧 소프트웨어다.」는 ObjectStack README의 첫 문장이고, 그 아래 줄이 정의다: 「실행 가능한 비즈니스 온톨로지 하나. AI가 쓰고, 런타임이 실행하고, 에이전트가 운영하고, 당신이 소유합니다.」 실행 가능한 비즈니스 온톨로지란 런타임이 직접 실행하는, 당신의 비즈니스에 대한 정의다. 그 핵심은 여섯 종류의 것 — 객체와 필드, 관계, 액션, 권한, 플로우, 에이전트와 도구 정의 — 이며, 하나의 아티팩트로 컴파일되어 거기서 데이터베이스, API, UI, 에이전트 도구가 도출된다. 뷰, 대시보드, 앱, 번역은 그것의 투영이지 그것의 일부가 아니다. 지식 표현 온톨로지가 아니고, 기존 시스템 위에 얹는 시맨틱 레이어가 아니며, 철학적 의미의 형식 온톨로지도 아니다. 코드는 사라지지 않는다. 훅, 액션 본문, CEL, 제한된 JSX로서 런타임 안으로 옮겨 간다. + +주장은 이것이 전부다. 이 글의 나머지는 그것을 분해한다: 왜 문장이 *기술한다*가 아니라 *이다*라고 말하는지, 여섯 종류, 투영과 둘을 가르는 테스트, 세 가지 부정, 코드가 가는 곳, 그리고 이 한 줄이 귀결되는 네 가지 약속. 출처는 README와 거기서 링크하는 개념 페이지이며, 바꿔 말하지 않고 인용한다. 같은 저장소에 함께 실린 CRM 예제는 설명하는 대신 측정한다. + +## 왜 문장이 「이다」라고 말하는가 + +대부분의 기업 온톨로지는 소프트웨어를 기술한다. 실제로 비즈니스를 돌리는 시스템 옆에 놓여 있고 — 고객의 모델, 무엇이 무엇과 연결되는지의 그래프, 매출이 무엇을 뜻하는지에 대한 통제된 정의 — 그 아래의 시스템은 아무것도 모른 채 계속 돈다. 그 기술은 유용하고, 동시에 선택 사항이다. 지워도 아무것도 멈추지 않는다. + +ObjectStack의 문장이 동일성의 형태로 쓰인 것은 관계가 정반대이기 때문이다. 정의가 곧 실행되는 그것이다. 온톨로지를 지우면 애플리케이션이 남지 않는다. 테이블도, 엔드포인트도, 화면도, 에이전트가 호출할 도구도 없다. 개념 페이지는 파이프라인을 한 줄로 적는다: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +타입이 있는 메타데이터를 `objectstack.config.ts`에 작성한다. 그것이 `objectstack.json` 아티팩트로 컴파일된다. 런타임이 아티팩트를 읽고 나머지를 도출한다. 같은 그림에 대한 README의 캡션은 더 짧다: 「타입이 있는 정의 하나 → 데이터베이스 · REST API · 클라이언트 SDK · UI · MCP 도구.」 온톨로지는 말만 하고 다른 아티팩트가 소프트웨어*인* 경우는 없다. 여기서 「이다」는 그런 뜻이고, 아래의 모든 것은 거기서 따라 나온다. + +## 여섯 종류의 핵심 + +개념 페이지는 핵심을 여섯 종류의 것으로 정의한다. 아래 표는 가운데 열에 그 문구를 그대로 두고, 오른쪽 열에서 각 종류가 함께 실린 CRM 예제(ObjectStack 저장소의 `examples/app-crm`) 어디에 있는지 가리킨다. 정의를 구호가 아니라 실제 정의에 대조해 확인할 수 있도록 하기 위해서다. + +| 핵심 | 무엇을 선언하는가 | 함께 실린 CRM 예제에서 | +| :--- | :--- | :--- | +| **객체와 필드** | 비즈니스의 엔티티와 그 타입이 있는 속성 — 검증, 수식, 기본값, 파일 | `src/objects/`: 계정, 연락처, 리드, 기회, 기회 라인 아이템, 활동 | +| **관계** | 객체 사이의 lookup과 master-detail 연결 | 기회의 `Field.lookup('crm_account', …)`; 라인 아이템은 그것의 master-detail 자식 | +| **액션** | 사용자나 에이전트가 레코드에 대해 호출할 수 있는 작업과, 그것이 표시되는 조건 | `src/actions/convert-lead.action.ts`, 리드가 아직 전환되지 않은 동안에만 표시 | +| **권한** | 누가 무엇을 읽고, 만들고, 고치고, 지울 수 있는가 — RBAC, 행 수준·필드 수준 보안, 공유 | `src/security/sales-positions.ts`: `crm_sales_user` 세트와, 리드 삽입만 가능한 게스트 프로필 | +| **플로우** | 비즈니스 프로세스 — DAG 플로우, 레코드 트리거, 예약 작업, 승인, 워크플로우 | `src/flows/convert-lead.flow.ts`, 계정과 기회를 만든 뒤 리드를 전환됨으로 표시하는 화면 플로우 | +| **에이전트와 도구 정의** | 앱에 함께 실리는 AI 에이전트, 그들이 호출할 수 있는 도구, 그들을 빚는 스킬 | 이 예제에는 선언된 것이 없음. 모든 객체는 이미 MCP 도구이고, 액션은 `ai: { exposed: true }`로 옵트인 | + +측정 하나. README가 「직접 재 보라」 아래에 스스로 인쇄한 명령으로 쟀다. 커밋 `9f9510f2`에서 CRM 예제의 정의는 **31개 파일, TypeScript 1,917줄**이다. 그 파일들에는 객체 여섯 개, 뷰 세 개, 대시보드 하나, 앱 하나, 리드 전환 플로우 하나, 액션 하나, 훅 하나, 권한 세트 두 개, 포지션 세 개, 번역 번들 하나, 페이지 하나, 그리고 시드 데이터가 들어 있다 — 애플리케이션 전체가, 읽을 수 있는 diff로. 이 숫자의 요점은 추상적으로 작다는 것이 아니다. 에이전트가 정의 전체를 한 번에 붙들고, 「여기를 바꾸면 무엇이 깨지는가?」에 정의 자체로부터 답할 수 있다는 것이다. README는 이를 AI가 통째로 붙들 수 있을 만큼 작은 앱이라고 부른다. + +핵심은 또한, 구조적으로, 당신의 것인 부분이다. 개념 페이지: 「그것은 하나의 정의이며, 열려 있고 버전이 관리된다. 평범한 TypeScript로 당신의 저장소에 살고, diff로 검토할 수 있으며, Apache-2.0으로 명세되고 라이선스된다 — 벤더의 시스템 안에 보관되어 그 벤더의 콘솔로만 닿을 수 있는 그래프가 아니다.」 + +## 투영, 그리고 제거 테스트 + +뷰, 대시보드, 앱, 번역은 같은 메타데이터로 작성되고 같은 게이트로 검증되지만, 온톨로지의 일부가 아니다. 개념 페이지는 이를 투영이라 부르고 각각을 한 절로 설명한다: 「리스트 뷰는 객체의 필드와 권한을 그리드에 투영하고, 대시보드는 객체에 대한 쿼리를 차트에 투영하며, 앱은 객체·뷰·액션을 탐색 가능한 제품으로 묶고, 번역은 모든 레이블을 하나의 로케일에 투영한다.」 + +![여섯 종류의 핵심이 ObjectStack 런타임을 거쳐 데이터베이스, API, Console UI, MCP 도구로 컴파일된다. 뷰, 대시보드, 앱, 번역은 투영으로서 온톨로지 상자 바깥에 있고, 훅, 액션 본문, CEL, 제한된 JSX는 런타임의 울타리 안에서 실행된다](./six-kind-core-en.svg) + +이 구분을 작동하게 만드는 문장은 이것이다: 「핵심을 바꾸면 투영이 따라온다. 투영을 제거해도 그것이 보여 주던 비즈니스는 바뀌지 않는다.」 이를 테스트로 바꾸면 프로젝트 안의 어떤 정의든 한 번에 분류할 수 있다. **제거 테스트**라 부르자: + +> 그 정의를 지워라. 데이터베이스 스키마, API, 또는 사용자나 에이전트가 할 수 있는 일이 바뀌었다면 그것은 핵심이었다. 화면, 차트, 메뉴, 레이블만 사라졌다면 그것은 투영이었다. + +CRM 예제에 대조해 읽어 보자. `src/views/opportunity.view.ts`를 지워도 기회 객체, 그 필드, lookup, 권한 세트는 모두 그대로다. REST 엔드포인트는 여전히 응답하고, MCP 도구도 여전히 존재한다. `src/objects/opportunity.object.ts`를 지우면 뷰는 투영할 것이 없고, 플로우는 만들 것이 없으며, 권한 세트는 더 이상 존재하지 않는 객체를 가리킨다. 하나는 투영이었고, 다른 하나는 소프트웨어였다. + +이 구분에 이름을 붙일 가치가 있는 이유도 같은 페이지에 적혀 있다: 「이 구분은 에이전트가 비즈니스에 대해 추론하기 위해 붙들어야 할 범위를 한정한다: 모든 화면이 아니라 핵심이다.」 승인 임계값 변경을 검토하는 에이전트는 객체, 권한, 플로우를 눈앞에 두어야 한다. 결과를 우연히 차트로 그리는 대시보드 전부가 필요하지는 않다. + +## 그것이 아닌 세 가지 + +*온톨로지*라는 말은 다른 분야에서 수십 년의 의미를 짊어지고 왔고, README는 정의의 상당 부분을 그 의미 중 어느 것이 여기에 해당하지 않는지 말하는 데 쓴다. 부정은 세 가지이고, 서로 다르다. + +| 그것이 아닌 것 | 그것은 무엇인가 | 왜 여기서는 다른가 | +| :--- | :--- | :--- | +| **지식 표현 온톨로지** | OWL, RDF, 지식 그래프는 도메인을 *기술*하고, 클래스 상속과 공리를 지니며, 추론 엔진이 그 위에서 *추론*한다 | 객체 상속 없음, 공리 없음, 추론기 없음. 정의는 검증되고 — Zod 스키마와 `os validate`로 — 그다음 실행된다. `abstract` 키는 바로 그 이유로 명세에서 제거되었다 (ADR-0049) | +| **기존 시스템 위의 시맨틱 레이어** | 이미 운영 중인 시스템 위의 읽기 전용 매핑: 비즈니스를 기술하지만 실행하지는 않는다 | 온톨로지가 곧 시스템*이다*: 데이터베이스, API, UI, 에이전트 도구가 거기서 도출된다. 외부 데이터소스 페더레이션은 가능하고, 기본값은 읽기 전용이며, 아직 초기 단계다 | +| **철학적 의미의 형식 온톨로지** | 일반적으로 무엇이 존재하는가에 대한 주장 | 그런 주장을 하지 않는다. 한 비즈니스의 애플리케이션을, 실행될 만큼 정확하게 정의한 것이다 | + +![세 열의 대비: 지식 표현 온톨로지는 기술되고 추론되며, 시맨틱 레이어는 기존 시스템 위의 읽기 전용 매핑이고, 실행 가능한 비즈니스 온톨로지는 검증된 뒤 데이터베이스, API, UI, 에이전트 도구로 실행된다](./validated-not-reasoned-en.svg) + +첫 번째 부정이 가장 자주 놓친다. 제약이 아니라 설계상의 선택이기 때문이다. 추론 엔진은 두 번째 진실의 원천이다. 그것이 런타임에 도출하는 사실은 검토자가 읽을 수 있는 어디에도 적힌 적이 없다. 개념 페이지에 있는 ObjectStack의 규칙은 그 반대다: 「런타임에서는 아무것도 추론되지 않는다. 실행되는 것은 검증된 것이다.」 권한이 diff에 없다면 시스템에도 없다. + +두 번째 부정은 플랫폼을 평가하는 모든 사람에게 중요하다. *시맨틱 레이어*라는 용어가 같은 대화 안에서 서로 다른 두 가지를 가리키는 데 쓰이고 있기 때문이다. 시맨틱 레이어는 어떤 숫자가 무엇을 뜻하는지에 대한 훌륭한 정의일 수 있다 — [지식 그래프와의 비교](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/)는 그것을 공정하게 변호한다. 그것은 명사의 레이어다. 실행 가능한 비즈니스 온톨로지는 동사를 지닌다: 무엇을 해도 되는지, 누가, 그다음 무엇이 일어나는지를 결정하는 액션, 권한, 플로우다. 문서는 또한 보고서와 대시보드가 바인딩하는 분석 데이터셋 레이어를 가리켜 *시맨틱 레이어*를 더 좁은 의미로도 쓴다고 적는다. 그 좁은 의미는 보고에 관한 것이고, 이 문장이 뜻하는 바가 아니다. + +세 번째 부정은 가장 짧고 가장 정직하다. 이 형식은 세계에 대해 아무 주장도 하지 않는다. 한 비즈니스의 애플리케이션을 런타임이 실행할 수 있고 검토자가 통째로 읽을 수 있을 만큼 정확하게 서술할 수 있다고 주장할 뿐이다. + +## 코드는 어디로 가는가 + +추론되는 대신 실행되는 정의라도 스키마로는 표현할 수 없는 것을 여전히 표현해야 한다. 거래가 성사되면 100%로 고정되는 확률, 리드가 전환되면 사라지는 버튼, 레이아웃이 있는 랜딩 페이지. README의 문장은 「코드는 사라지지 않는다: 훅, 액션 본문, CEL, 제한된 JSX로서 런타임 안으로 옮겨 간다」이고, 개념 페이지가 그 이유를 든다. 네 가지 모두 「런타임의 권한과 감사 울타리 안에서 실행」되며, 「그중 어느 것도 온톨로지가 나중에 기술해야 할 코드베이스에 흩어져 있지 않다.」 + +CRM 예제의 훅은 객체의 생명주기에 붙은 함수다: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL은 수식, 술어, 정책을 위한 표현식 언어다. 같은 예제는 계산 필드와 액션의 표시 조건 양쪽에 그것을 쓴다: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +제한된 JSX는 사용자 정의 페이지를 쓰는 방법이고, 제한이 바로 요점이다. 그것은 「서버 주도 UI 트리로 파싱되며, 결코 코드로 실행되지 않는다.」 이를 수행하는 패키지는 README에 파싱만 하고 결코 실행하지 않는 컴파일러로 등재되어 있다. 액션 본문은 네 번째 자리로, 액션이 호출될 때 서버 측에서 수행하는 작업을 맡는다. + +네 가지에 걸친 패턴은 같다. 코드는 허용된다. 그러나 코드는 비즈니스가 사는 곳이 아니다. 비즈니스는 여섯 종류의 핵심에 살고, 코드는 그 여섯 종류가 정하는 울타리 안에서, 클릭이나 도구 호출과 같은 권한과 감사 아래 실행된다. + +## 검증하고, 그다음 실행한다 + +런타임이 아티팩트로 하는 일은 두 동사로 설명되며, 개념 페이지는 둘 다 의도적으로 쓴다. 그것은 **도출한다**: 「교체 가능한 드라이버 위의 데이터베이스 스키마, 생성된 REST와 실시간 API, Console을 위한 서버 주도 UI, 그리고 MCP 서버」. 그리고 **강제한다**: 「RBAC, 행 수준·필드 수준 보안, 공유, 감사는 REST 요청에도, Console 클릭에도, 에이전트의 도구 호출에도 똑같이 적용된다.」 + +README는 첫 번째 동사를 객체 하나로 보여 준다. 티켓을 선언한다: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +그러면 「REST API는 객체가 존재하는 순간 존재한다 — 작성할 컨트롤러가 없다」: + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +주석이 달린 `sharingModel: 'private'`에 주목하라. 보안 태세는 객체의 일부이고, 게이트는 그것을 요구한다. `os validate`는 「타입 검사는 통과하지만 런타임에서 조용히 실패할 메타데이터를 거부한다: 매달린 바인딩, 잘못된 CEL 술어, 빠진 보안 태세」. 이것이 검증의 절반이다. 실행의 절반은, 이 객체에서 도출된 모든 표면 — 테이블, 엔드포인트, Console 그리드, MCP 도구 — 이 호출할 때마다 같은 권한에 대조해 검사된다는 것이다. 그래서 MCP로 티켓에 닿은 에이전트는 같은 권한을 가진 사람이 보는 것과 정확히 같은 것을 본다. + +## 네 가지 약속, 「실행 가능」이 앞장선다 + +README는 이 줄을 네 단어로 줄인다: 「실행 가능 · AI가 작성 · 에이전트가 운영 · 당신이 소유」. 각각은 메커니즘이 뒷받침하는 주장이고, 순서는 장식이 아니다. 첫 번째가 나머지 셋이 딛고 선 것이다. + +**실행 가능.** 정의가 곧 실행되는 그것이다. 위의 두 절에 있는 모든 것이 이 약속이다. 아티팩트 하나, 도출된 표면들, 호출마다의 강제, 런타임에서 추론되는 것은 없음. 이것이 없으면 「AI가 작성」은 AI가 문서를 쓴다는 뜻이 될 것이다. + +**AI가 작성.** 「에이전트는 타입이 있는 메타데이터를 쓴다 — 코드베이스가 아니다. 게이트는 런타임에서 조용히 실패할 것을 거부하고, 에이전트는 당신이 보기도 전에 그것을 고친다.」 스캐폴더는 「AI 스킬 번들을 설치하고 `AGENTS.md`를 써 주므로, 당신의 에이전트는 프로토콜의 규칙이 이미 로드된 채로 시작한다」. README의 네 게이트 — 타입 검사, 검증, 검토, 거버넌스 — 가 에이전트의 실수가 출시되지 않는 이유이고, 이것이 애초에 작동하는 이유로 적힌 것은 「에이전트의 오류가 위치가 특정된, 교정용 텍스트가 되어 에이전트 스스로 읽고 고칠 수 있다」는 것이다. + +**에이전트가 운영.** 「객체는 도구이고, 액션은 도구이며, 권한이 에이전트가 무엇을 호출할 수 있는지 결정하고, 감사가 무엇을 했는지 기록한다.」 실행 중인 앱은 「`/api/v1/mcp`에서 MCP 서버로 제공된다 — 기본으로 켜져 있다」, 그리고 에이전트는 「사람과 같은 권한과 RLS 아래」 그것을 운영한다. 에이전트가 쓴 같은 온톨로지를, 같은 울타리를 통해, 다음에는 에이전트가 쓴다. + +**당신이 소유.** 「이 저장소의 모든 것은 열린 스택이다 — 프로토콜, 마이크로커널, SDK, CLI, 그리고 프로덕션 런타임, Apache-2.0에 오픈코어 별표 없음.」 그리고 정의 자체는 「평범한 TypeScript로 당신의 저장소에 살고, 당신의 VCS에서 버전이 관리되며, diff로 검토할 수 있다 — 벤더의 시스템 안에 보관된 그래프가 아니다.」 온톨로지는 당신이 손에 쥔 파일이지, 빌려 쓰는 테넌시가 아니다. + +## 이것이 해결하지 않는 것 + +정의를 다루는 글은 정의가 어디서 멈추는지 말해야 한다. + +모델링을 쉽게 만들지 않는다. 기회가 무엇인지, 누가 그것을 종결할 수 있는지, 30%를 넘는 할인에 무엇이 필요한지 정하는 일은 여전히 늘 그랬듯 느리고, 다툼이 있고, 사람이 하는 일이다. 형식이 정하는 것은 결과가 통째로 읽을 만큼 작은지, 작성 시점에 실수를 거부할 만큼 엄격한지뿐이다. + +추론하지 않는다. 당신의 문제가 정말로 도메인 모델 위의 추론 — 클래스 계층, 도출된 사실, 결과를 발견하는 엔진 — 을 필요로 한다면, 그것은 이 글이 명시적으로 아니라고 말한 지식 표현 온톨로지다. 그 도구를 고르고, 이쪽에 추론기가 자라나기를 기대하지 말라. + +이미 운영 중인 것 위의 레이어가 아니다. 외부 데이터소스 페더레이션은 기본값이 읽기 전용이고 초기 단계로 기술되어 있으므로, 레거시 데이터베이스 위의 실행 가능한 온톨로지는 읽기 모델로 시작해 객체 하나씩 쓰기를 얻어 간다. [내부 시스템을 메타데이터로 옮기기](/en/blog/how-to-move-an-internal-system-to-metadata/)는 그 길의 정직한 버전이며, 아예 옮기지 않는 것이 답인 경우까지 포함한다. + +그리고 새로운 로직은 코드로 남는다. 훅과 액션 본문이 그것이 가는 자리이고, 울타리 안에 있으며, 여전히 사람이 검토해야 하는 코드다. 온톨로지는 그 코드를 더 작고 경계가 있게 만든다. 사라지게 하지는 않으며, README도 꼭 그 말로 그렇게 적는다. + +## 호스팅으로, 또는 자체 인프라에서 + +README는 위의 루프 — 코딩 에이전트가 당신의 저장소에 메타데이터를 쓰고, 실행 중인 앱을 MCP로 운영한다 — 를 설명한 뒤 이렇게 말한다: 「같은 루프를 호스팅으로, 브라우저에서, 아무것도 설치하지 않고 쓰고 싶다면? 그것이 ObjectOS, ObjectStack 위에 구축된 상용 런타임 환경입니다.」 ObjectOS는 설치 없이 브라우저에서 호스팅되는 ObjectOS Cloud로도, 자체 인프라에서 직접 운영하는 ObjectOS Enterprise로도 실행되며, 어느 에디션에서든 온톨로지를 내보내 오픈소스 ObjectStack 런타임에서 실행할 수 있습니다. 이것은 네 번째 약속을 제품의 경계로 말한 것이다. 어느 호스트에서 돌든, 온톨로지는 당신의 것이다. + +## 에이전트 경로로 시작하라 + +위의 어느 것도 정의를 믿음으로 받아들이라고 요구하지 않는다. README 자신의 5분 경로가 읽을 수 있는 정의 하나를 만들어 준다. + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +프로젝트를 코딩 에이전트에서 열고 요구사항을 설명하라. README의 예제는 지원 데스크다. 제목, 설명, 우선순위 선택, 상태 선택이 있는 `ticket` 객체; 아직 해결되지 않은 티켓에만 표시되는 Resolve 액션; 「미해결 티켓」 리스트 뷰와 Support 내비게이션 그룹; 끝나면 `npm run validate`. 그다음 결과를 미리 보라: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +Console을 보기 전에 에이전트가 만든 diff를 보라. 객체, 액션과 그 표시 술어, 뷰 — 제거 테스트로 모든 줄을 핵심 또는 투영으로 분류할 수 있을 것이고, 실행되지 않을 줄은 게이트가 이미 거부했을 것이다. 에이전트에게 방향 잡기가 필요하다면, [ObjectStack용으로 설정하는 방법](/en/blog/how-to-point-your-agent-at-objectstack/)이 규칙 파일, 스킬 번들, 도구 서버를 다룬다. 에이전트가 쓰는 그 정의가 소프트웨어이고, 그것은 당신의 저장소에 있다. diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx new file mode 100644 index 0000000..2b1932e --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx @@ -0,0 +1,197 @@ +--- +title: "What Does \"The Ontology Is the Software\" Mean? An Executable Business Ontology, Defined" +description: "The line is ObjectStack's positioning; this is its definition. An executable business ontology has a six-kind core, four projections and three things it is not — and a test for telling core from projection." +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover-en.svg +tags: + - Ontology + - Executable business ontology +--- + +**The short version.** "The ontology is the software." is the first sentence of the ObjectStack README, and the line under it is the definition: "One executable business ontology. AI writes it, the runtime runs it, agents operate it, you own it." An executable business ontology is the definition of your business that a runtime executes directly. Its core is six kinds of thing — objects and fields, relations, actions, permissions, flows, and agent and tool definitions — compiled into one artifact, from which the database, the API, the UI and the agent tools are derived. Views, dashboards, apps and translations are projections of it, not part of it. It is not a knowledge-representation ontology, not a semantic layer over your existing systems, and not a formal ontology in the philosophical sense. Code does not disappear; it moves into the runtime, as hooks, action bodies, CEL and constrained JSX. + +That is the whole claim. The rest of this post takes it apart: why the sentence says *is* and not *describes*, the six kinds, the projections and a test for telling the two apart, the three negations, where code goes, and the four promises the line reduces to. The sources are the README and the concepts page it links to, quoted rather than paraphrased, plus the CRM example that ships in the same repository, measured rather than described. + +## Why the sentence says "is" + +Most enterprise ontologies describe software. They sit beside the systems that actually run the business — a model of the customer, a graph of what connects to what, a governed definition of what revenue means — and the systems carry on underneath, unaware. The description is useful, and it is also optional: delete it and nothing stops. + +The ObjectStack sentence is phrased as an identity because the relationship is the other way round. The definition is the thing that runs. Delete the ontology and there is no application left: no tables, no endpoints, no screens, no tools for an agent to call. The concepts page puts the pipeline in one line: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +You author typed metadata in `objectstack.config.ts`; it compiles into the `objectstack.json` artifact; the runtime reads the artifact and derives the rest. The README's caption for the same picture is shorter still: "One typed definition → database · REST API · client SDK · UI · MCP tools." There is no second artifact that *is* the software while the ontology merely talks about it. That is what "is" means here, and everything below follows from it. + +## The six-kind core + +The concepts page defines the core as six kinds of thing. The table below keeps its wording in the middle column and, in the right column, points at where each kind lives in the bundled CRM example (`examples/app-crm` in the ObjectStack repository), so the definition can be checked against a real definition rather than against a slogan. + +| Core | What it declares | In the bundled CRM example | +| :--- | :--- | :--- | +| **Objects and fields** | The entities of the business and their typed attributes — validation, formulas, defaults, files | `src/objects/`: account, contact, lead, opportunity, opportunity line item, activity | +| **Relations** | Lookups and master-detail links between objects | `Field.lookup('crm_account', …)` on the opportunity; the line item is a master-detail child of it | +| **Actions** | The operations a user or an agent may invoke on a record, and the conditions under which they show | `src/actions/convert-lead.action.ts`, visible only while the lead is not yet converted | +| **Permissions** | Who may read, create, edit and delete what — RBAC, row-level and field-level security, sharing | `src/security/sales-positions.ts`: a `crm_sales_user` set, and a guest profile that may only insert leads | +| **Flows** | The business processes — DAG flows, record triggers, scheduled jobs, approvals, workflows | `src/flows/convert-lead.flow.ts`, a screen flow that creates the account and opportunity, then marks the lead converted | +| **Agent and tool definitions** | The AI agents the app ships with, the tools they may call, and the skills that shape them | None declared in this example. Every object is already an MCP tool; actions opt in with `ai: { exposed: true }` | + +One measurement, taken with the command the README itself prints under "measure it yourself": at commit `9f9510f2`, the CRM example's definition is **1,917 lines of TypeScript across 31 files**. Those files hold six objects, three views, a dashboard, an app, a lead-conversion flow, an action, a hook, two permission sets, three positions, a translation bundle, a page and the seed data — the whole application, as a diff you can read. The point of the number is not that it is small in the abstract. It is that an agent can hold the whole definition at once and answer "what breaks if I change this?" from the definition itself, which the README calls apps small enough for AI to hold whole. + +The core is also, by construction, the part that is yours. The concepts page: "It is one definition, open and versioned. It lives in your repository as ordinary TypeScript, is reviewable as a diff, and is specified and licensed Apache-2.0 — not a graph held inside a vendor's system and reachable only through that vendor's console." + +## Projections, and the removal test + +Views, dashboards, apps and translations are authored in the same metadata and validated by the same gate, but they are not part of the ontology. The concepts page calls them projections and explains each in a clause: "A list view projects an object's fields and permissions onto a grid; a dashboard projects queries over objects onto charts; an app groups objects, views and actions into a navigable product; a translation projects every label into a locale." + +![The six-kind core compiles through the ObjectStack runtime into the database, the API, the Console UI and MCP tools; views, dashboards, apps and translations sit outside the ontology box as projections, and hooks, action bodies, CEL and constrained JSX run inside the runtime's fence](./six-kind-core-en.svg) + +The sentence that makes the distinction operational is this one: "Change the core and the projections follow; remove a projection and the business it showed is unchanged." Turn it into a test and you can classify any definition in a project in one move. Call it the **removal test**: + +> Delete the definition. If the database schema, the API, or what a user or agent may do has changed, it was core. If only a screen, a chart, a menu or a label has gone, it was a projection. + +Read it against the CRM example. Delete `src/views/opportunity.view.ts` and the opportunity object, its fields, its lookups and its permission set are all still there; the REST endpoint still answers; the MCP tool still exists. Delete `src/objects/opportunity.object.ts` and the view has nothing to project, the flow has nothing to create, and the permission set names an object that no longer exists. One was a projection; the other was the software. + +Why the distinction is worth a name is stated on the same page: "The distinction bounds what an agent must hold to reason about the business: the core, not every screen." An agent reviewing a change to an approval threshold needs the objects, the permissions and the flow in front of it. It does not need every dashboard that happens to chart the result. + +## Three things it is not + +The word *ontology* carries decades of meaning from other fields, and the README spends a good part of its definition saying which of those meanings do not apply. There are three negations, and they are different. + +| It is not | What that thing is | Why this is different | +| :--- | :--- | :--- | +| **A knowledge-representation ontology** | OWL, RDF and knowledge graphs *describe* a domain, carry class inheritance and axioms, and are *reasoned over* by an inference engine | No object inheritance, no axioms, no reasoner. The definition is validated — by Zod schemas and `os validate` — and then run. The `abstract` key was removed from the spec for exactly that reason (ADR-0049) | +| **A semantic layer over your existing systems** | A read-only mapping over systems you already run: it describes the business but does not run it | The ontology *is* the system: the database, API, UI and agent tools are derived from it. Federating an external datasource is possible, read-only by default, and early | +| **A formal ontology in the philosophical sense** | A claim about what exists in general | It makes no such claim. It is one business's application, defined precisely enough to run | + +![Three columns: a knowledge-representation ontology is described and reasoned over, a semantic layer is a read-only mapping over existing systems, and an executable business ontology is validated and then run into a database, an API, a UI and agent tools](./validated-not-reasoned-en.svg) + +The first negation is the one most often missed, because it is a design choice and not a limitation. An inference engine is a second source of truth: the facts it derives at runtime were never written down anywhere a reviewer could read them. ObjectStack's rule, from the concepts page, is the opposite: "Nothing is reasoned over at runtime: what runs is what was validated." If a permission is not in the diff, it is not in the system. + +The second negation matters for anyone evaluating platforms, because the term *semantic layer* is being used for two different things in the same conversations. A semantic layer can be an excellent definition of what a number means — the [comparison with knowledge graphs](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/) gives it a fair defence. It is a noun layer. An executable business ontology carries the verbs: actions, permissions and flows that decide what may be done, by whom, and what happens next. The docs also note that they use *semantic layer* in a narrower sense, for the analytics dataset layer that reports and dashboards bind to; that narrower sense is about reporting, and it is not what the sentence means. + +The third negation is the shortest and the most honest. The format makes no claim about the world. It claims that one business's application can be stated precisely enough for a runtime to execute it and for a reviewer to read it whole. + +## Where code goes + +A definition that is executed rather than reasoned over still has to express things no schema expresses: a probability that snaps to 100% when a deal closes, a button that disappears once a lead is converted, a landing page with a layout. The README's sentence is "code does not disappear: it moves into the runtime, as hooks, action bodies, CEL, and constrained JSX", and the concepts page gives the reason: all four "run inside the runtime's permission and audit fence", and "none of it is scattered across a codebase the ontology would then have to describe." + +A hook, from the CRM example, is a function attached to an object's lifecycle: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL is the expression language for formulas, predicates and policies. The same example uses it for a computed field and for the visibility of an action: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +Constrained JSX is how a custom page is written, and the constraint is the point: it "is parsed into a server-driven UI tree, never executed as code." The package that does it is listed in the README as a compiler that parses and never executes. Action bodies are the fourth place, for the server-side work an action performs when invoked. + +The pattern across all four is the same. Code is allowed; code is not where the business lives. The business lives in the six kinds, and the code runs inside the fence those six kinds define, under the same permissions and audit as a click or a tool call. + +## Validated, then run + +Two verbs describe what the runtime does with the artifact, and the concepts page uses both deliberately. It **derives**: "the database schema on an interchangeable driver, a generated REST and realtime API, server-driven UI for the Console, and an MCP server". And it **enforces**: "RBAC, row-level and field-level security, sharing, and audit apply to a REST request, a Console click and an agent's tool call alike." + +The README shows the first verb with one object. Declare a ticket: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +and "the REST API exists the moment the object does — no controllers to write": + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +Notice `sharingModel: 'private'` with its comment. The security posture is part of the object, and the gate requires it: `os validate` "rejects metadata that type-checks but would fail silently at runtime: dangling bindings, bad CEL predicates, missing security posture." That is the validated half. The run half is that every surface derived from the object — the table, the endpoint, the Console grid, the MCP tool — is checked against the same permissions on every call, so an agent that reaches the ticket through MCP sees exactly what a human with the same permissions would see. + +## The four promises, led by Executable + +The README reduces the line to four words: "Executable · AI-writable · Agent-operable · You own it". Each is a claim with a mechanism behind it, and the order is not decorative — the first one is what the other three stand on. + +**Executable.** The definition is the thing that runs. Everything in the two sections above is this promise: one artifact, derived surfaces, enforcement on every call, nothing reasoned over at runtime. Without it, "AI-writable" would mean an AI writing documentation. + +**AI-writable.** "The agent writes typed metadata — not a codebase. The gate rejects what would fail silently at runtime, and the agent fixes it before you ever see it." The scaffolder "installs the AI skills bundle and writes an `AGENTS.md`, so your agent starts with the protocol's rules already loaded". The README's four gates — typed, validated, reviewed, governed — are the reason the agent's mistakes do not ship, and the stated reason it works at all: "an agent's errors become located, corrective text it can read and fix itself". + +**Agent-operable.** "Objects are tools, actions are tools, permissions decide what an agent may call, and audit records what it did." The running app "serves it as an MCP server at `/api/v1/mcp` — on by default", and an agent operates it "under the same permissions and RLS as a human". The same ontology that was written by an agent is then used by one, through the same fence. + +**You own it.** "Everything in this repo is the open stack — protocol, microkernel, SDK, CLI, and the production runtime, Apache-2.0 with no open-core asterisks." And the definition itself "lives in your repository as ordinary TypeScript, versioned in your VCS and reviewable as a diff — not a graph held inside a vendor's system." The ontology is a file you hold, not a tenancy you rent. + +## What this does not solve + +A definition post should say where the definition stops. + +It does not make modelling easy. Deciding what an opportunity is, who may close one and what a discount above 30% needs is still the slow, contested, human work it always was; the format decides only whether the result is small enough to read whole and strict enough to reject mistakes at authoring time. + +It does not reason. If your problem genuinely needs inference over a domain model — class hierarchies, derived facts, an engine that discovers consequences — that is the knowledge-representation ontology this one is explicitly not. Pick that tool, and do not expect this one to grow a reasoner. + +It is not a layer over what you already run. Federating an external datasource is read-only by default and described as early, so an executable ontology over a legacy database starts as a read model and earns writes one object at a time. [Moving an internal system to metadata](/en/blog/how-to-move-an-internal-system-to-metadata/) is the honest version of that path, including the cases where the answer is not to move at all. + +And novel logic stays code. Hooks and action bodies are where it goes, inside the fence, and they are still code a human has to review. The ontology makes that code smaller and bounded; it does not make it disappear, and the README says so in as many words. + +## Hosted, or on your own infrastructure + +The README describes the loop above — a coding agent writes the metadata in your repo and operates the running app over MCP — and then says: "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. Which is the fourth promise, stated as a product boundary: the ontology is yours on every host it runs on. + +## Start with the agent path + +Nothing above requires you to take the definition on faith; the README's own five-minute path produces one you can read. + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +Open the project in your coding agent and describe the requirement. The README's example is a support desk: a `ticket` object with subject, description, a priority select and a status select; a Resolve action that only shows on tickets that are not already resolved; an "Open tickets" list view and a Support nav group; `npm run validate` when it is done. Then preview the result: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +Look at the diff the agent produced before you look at the Console. The object, the action and its visibility predicate, the view — you will be able to sort every line into core or projection with the removal test, and the gate will already have refused the lines that would not run. If the agent needs steering, [how to configure it for ObjectStack](/en/blog/how-to-point-your-agent-at-objectstack/) covers the rule file, the skills bundle and the tool server. The definition it writes is the software, and it is in your repository. diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx new file mode 100644 index 0000000..33d9617 --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx @@ -0,0 +1,197 @@ +--- +title: 「本体即软件」到底是什么意思?可执行业务本体的完整定义 +description: 这句话是 ObjectStack 的定位,这篇是它的定义:可执行业务本体由六类核心构成,有四种投影、三个「不是」,还有一条分辨核心与投影的测试。 +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover.svg +tags: + - 本体 + - 可执行业务本体 +--- + +**先给结论**:「本体即软件。」是 ObjectStack README 的第一句话,紧跟其后的那一行就是定义:「一份可执行的业务本体。AI 编写它,运行时运行它,Agent 操作它,你拥有它。」可执行业务本体,是你的业务里那份由运行时直接执行的定义。它的核心是六类东西——对象与字段、关系、动作、权限、流程、Agent 与工具定义——编译成一个工件,数据库、API、UI 和 Agent 工具都从这个工件推导出来。视图、仪表盘、应用和翻译是它的投影,不是它的一部分。它不是知识表示意义上的本体,不是架在你现有系统之上的语义层,也不是哲学意义上的形式本体。代码不会消失,它搬进了运行时:钩子、动作体、CEL 和受限 JSX。 + +这就是全部主张。本文接下来把它拆开:这句话为什么说「即」而不说「描述」;六类核心;投影,以及一条区分二者的测试;三个「不是」;代码去了哪里;以及这句话最终归结成的四个承诺。来源是 README 和它链接的概念页,引用而非转述;再加上同一仓库里自带的 CRM 示例,测量而非描述。 + +## 这句话为什么说「即」 + +大多数企业本体是在描述软件。它们挨着真正跑业务的系统——一个客户模型、一张「什么连着什么」的图、一份对「营收」的受控定义——而底下的系统照常运转,毫不知情。这种描述有用,但也是可选的:删掉它,什么都不会停。 + +ObjectStack 这句话写成一个等式,是因为关系正好反过来:定义本身就是在跑的那个东西。删掉本体,应用就没有了——没有表、没有接口、没有界面、没有可供 Agent 调用的工具。概念页用一行写出整条流水线: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +你在 `objectstack.config.ts` 里编写类型化元数据;它编译成 `objectstack.json` 工件;运行时读取工件,推导出其余一切。README 给同一张图的说明更短:「一份类型化定义 → 数据库 · REST API · 客户端 SDK · UI · MCP 工具。」这里没有第二个工件在「当」软件、而本体只是谈论它。这就是「即」的含义,下面的一切都由此推出。 + +## 六类核心 + +概念页把核心定义为六类东西。下表中间一列保留它的原话,右边一列指向每一类在自带 CRM 示例(ObjectStack 仓库里的 `examples/app-crm`)中的位置,这样定义可以对照一份真实的定义来核验,而不是对照一句口号。 + +| 核心 | 它声明什么 | 在自带 CRM 示例中 | +| :--- | :--- | :--- | +| **对象与字段** | 业务中的实体及其类型化属性——校验、公式、默认值、文件 | `src/objects/`:客户、联系人、线索、商机、商机明细、活动 | +| **关系** | 对象之间的查找(lookup)与主从(master-detail)链接 | 商机上的 `Field.lookup('crm_account', …)`;商机明细是它的主从子对象 | +| **动作** | 用户或 Agent 可以对一条记录发起的操作,以及它们显示的条件 | `src/actions/convert-lead.action.ts`,只在线索尚未转化时可见 | +| **权限** | 谁可以读、建、改、删什么——RBAC、行级与字段级安全、共享 | `src/security/sales-positions.ts`:一个 `crm_sales_user` 权限集,以及一个只能新增线索的访客档案 | +| **流程** | 业务过程——DAG 流程、记录触发器、定时任务、审批、工作流 | `src/flows/convert-lead.flow.ts`,一个屏幕流程:创建客户与商机,再把线索标记为已转化 | +| **Agent 与工具定义** | 应用自带的 AI Agent、它们可以调用的工具,以及塑造它们的技能 | 本示例未声明。每个对象本来就是一个 MCP 工具;动作用 `ai: { exposed: true }` 主动开放 | + +一个测量,用的是 README 自己在「自己量一量」下面印出来的那条命令:在提交 `9f9510f2`,CRM 示例的定义是 **31 个文件、1,917 行 TypeScript**。这些文件装着六个对象、三个视图、一个仪表盘、一个应用、一条线索转化流程、一个动作、一个钩子、两个权限集、三个岗位、一个翻译包、一个页面和种子数据——整个应用,是一份你读得完的 diff。这个数字的意义不在于它抽象地「小」,而在于一个 Agent 能把整份定义一次装下,并从定义本身回答「我改了这里会弄坏什么?」——README 把这叫作「小到 AI 能整个装下的应用」。 + +核心同时也是——按构造就是——属于你的那部分。概念页写道:「它是一份定义,开放且有版本。它以普通 TypeScript 的形式住在你的仓库里,可以作为 diff 审阅,按 Apache-2.0 规范并授权——不是一张锁在厂商系统里、只能通过该厂商控制台触达的图。」 + +## 投影,以及「删除测试」 + +视图、仪表盘、应用和翻译用同样的元数据编写、由同一道门禁校验,但它们不是本体的一部分。概念页称之为投影,并逐一用一个分句解释:「列表视图把一个对象的字段和权限投影到一张网格上;仪表盘把对象上的查询投影到图表上;应用把对象、视图和动作组合成一个可导航的产品;翻译把每一个标签投影到一种语言。」 + +![六类核心经 ObjectStack 运行时编译成数据库、API、控制台 UI 和 MCP 工具;视图、仪表盘、应用和翻译作为投影位于本体框之外;钩子、动作体、CEL 和受限 JSX 在运行时的围栏内运行](./six-kind-core.svg) + +让这个区分可操作的是这一句:「改动核心,投影跟着变;删掉一个投影,它所展示的业务不变。」把它变成一条测试,你就能一步给项目里任何一份定义归类。姑且叫它**删除测试**: + +> 删掉这份定义。如果数据库结构、API,或者用户与 Agent 能做的事变了,它就是核心。如果消失的只是一个页面、一张图、一个菜单或一个标签,它就是投影。 + +拿 CRM 示例来对照。删掉 `src/views/opportunity.view.ts`,商机对象、它的字段、它的查找关系和它的权限集都还在;REST 接口照常应答;MCP 工具照样存在。删掉 `src/objects/opportunity.object.ts`,视图没了可投影的东西,流程没了可创建的东西,权限集点名的对象也不复存在。前者是投影,后者才是软件。 + +这个区分为什么值得起个名字,同一页也说了:「这个区分划定了 Agent 为了推理业务必须装下的范围:是核心,而不是每一个屏幕。」一个在审阅审批阈值改动的 Agent,需要把对象、权限和流程摆在眼前;它不需要恰好把结果画成图表的每一个仪表盘。 + +## 它不是的三样东西 + +「本体」这个词从其他领域带来了几十年的含义,README 的定义有相当一部分篇幅是在说哪些含义在这里不适用。否定有三个,而且各不相同。 + +| 它不是 | 那样东西是什么 | 为什么这里不同 | +| :--- | :--- | :--- | +| **知识表示意义上的本体** | OWL、RDF 和知识图谱*描述*一个领域,带有类继承和公理,并由推理引擎*推理* | 没有对象继承、没有公理、没有推理器。定义先被校验——由 Zod schema 和 `os validate`——然后被运行。`abstract` 这个键正是因此从规范中移除(ADR-0049) | +| **架在你现有系统之上的语义层** | 在你已经运行的系统之上的一层只读映射:它描述业务,但不运行业务 | 本体*就是*系统:数据库、API、UI 和 Agent 工具都从它推导出来。联邦接入外部数据源是可以的,默认只读,而且还处于早期 | +| **哲学意义上的形式本体** | 对「一般而言什么存在」的主张 | 它不作此类主张。它是一家企业的应用,被定义到足以运行的精确程度 | + +![三列对比:知识表示本体被描述并被推理;语义层是现有系统之上的只读映射;可执行业务本体先被校验、再被运行成数据库、API、UI 和 Agent 工具](./validated-not-reasoned.svg) + +第一个否定最容易被忽略,因为它是一个设计选择,不是一个局限。推理引擎是第二个真相来源:它在运行时推导出的事实,从未写在任何审阅者读得到的地方。ObjectStack 的规则来自概念页,正好相反:「运行时不做任何推理:跑的就是被校验过的东西。」如果一条权限不在 diff 里,它就不在系统里。 + +第二个否定对任何在评估平台的人都重要,因为「语义层」这个词正在同一场对话里被用来指两件不同的事。语义层可以是对「一个数字意味着什么」的出色定义——[与知识图谱的比较](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/)给了它公允的辩护。它是一个名词层。可执行业务本体带着动词:动作、权限和流程决定什么可以被做、由谁来做、接下来发生什么。文档也注明,他们在一个更窄的意义上使用「语义层」,指报表与仪表盘绑定的分析数据集层;那个窄义讲的是报表,不是这句话的意思。 + +第三个否定最短,也最诚实。这个格式不对世界作主张。它主张的是:一家企业的应用可以被陈述到足够精确,让运行时能执行它、让审阅者能整个读完它。 + +## 代码去了哪里 + +一份被执行而非被推理的定义,仍然要表达 schema 表达不了的东西:成交时自动跳到 100% 的概率,线索转化后消失的按钮,带版式的落地页。README 的句子是「代码不会消失:它搬进了运行时,成为钩子、动作体、CEL 和受限 JSX」,概念页给出了理由:这四样都「在运行时的权限与审计围栏内运行」,而且「没有一样散落在一个本体随后还得去描述的代码库里」。 + +CRM 示例里的一个钩子,是挂在对象生命周期上的一个函数: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL 是公式、谓词和策略的表达式语言。同一个示例用它写一个计算字段,也用它写一个动作的可见性: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +受限 JSX 是自定义页面的写法,而「受限」正是重点:它「被解析成一棵服务端驱动的 UI 树,永远不会作为代码执行」。做这件事的包在 README 里被列为一个只解析、从不执行的编译器。动作体是第四个去处,承载一个动作被调用时在服务端做的工作。 + +四处的模式是同一个:允许代码,但代码不是业务所在的地方。业务住在六类核心里,代码在这六类所划定的围栏内运行,接受与一次点击或一次工具调用相同的权限和审计。 + +## 先校验,再运行 + +运行时对工件做的事可以用两个动词描述,概念页是有意同时使用它们的。它**推导**:「可互换驱动之上的数据库结构、一套生成的 REST 与实时 API、控制台的服务端驱动 UI,以及一个 MCP 服务器」。它也**强制执行**:「RBAC、行级与字段级安全、共享和审计,对一次 REST 请求、一次控制台点击和一次 Agent 工具调用一视同仁。」 + +README 用一个对象演示第一个动词。声明一张工单: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +然后「REST API 在对象存在的那一刻就存在了——没有控制器要写」: + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +注意带注释的那行 `sharingModel: 'private'`。安全姿态是对象的一部分,门禁要求它必须在:`os validate`「拒绝那些能通过类型检查、却会在运行时静默失败的元数据:悬空绑定、错误的 CEL 谓词、缺失的安全姿态」。这是校验的那一半。运行的那一半是,从这个对象推导出的每一个表面——表、接口、控制台网格、MCP 工具——在每一次调用时都对照同一套权限检查,所以一个通过 MCP 触达这张工单的 Agent,看到的正是拥有相同权限的人会看到的东西。 + +## 四个承诺,以「可执行」领头 + +README 把这句话压缩成四个词:「可执行 · AI 可编写 · Agent 可操作 · 归你所有」。每一个都是一个有机制支撑的主张,顺序也不是装饰——第一个是其余三个的立足点。 + +**可执行。** 定义就是在跑的那个东西。上面两节的全部内容都是这个承诺:一个工件、推导出的表面、每次调用都强制执行、运行时不做推理。没有它,「AI 可编写」就只是 AI 在写文档。 + +**AI 可编写。**「Agent 写的是类型化元数据——不是一个代码库。门禁拒绝那些会在运行时静默失败的东西,Agent 在你看到之前就把它改好了。」脚手架「安装 AI 技能包并写入一份 `AGENTS.md`,所以你的 Agent 一开始就装载了协议规则」。README 的四道门——类型化、已校验、已审阅、受治理——是 Agent 的错误到不了生产的原因,也是它说明这件事能成立的理由:「Agent 的错误变成了有定位的、可纠正的文本,它自己能读、能改」。 + +**Agent 可操作。**「对象是工具,动作是工具,权限决定 Agent 可以调用什么,审计记录它做了什么。」运行中的应用「以 MCP 服务器的形式在 `/api/v1/mcp` 提供它——默认开启」,Agent「在与人相同的权限和 RLS 之下」操作它。由 Agent 写出的同一份本体,随后由 Agent 使用,经过同一道围栏。 + +**归你所有。**「这个仓库里的一切都是开放栈——协议、微内核、SDK、CLI 和生产运行时,Apache-2.0,没有开放核心的小字。」而定义本身「以普通 TypeScript 的形式住在你的仓库里,在你的版本控制里有版本,可以作为 diff 审阅——不是一张锁在厂商系统里的图」。本体是你握着的一个文件,不是你租来的一个租户。 + +## 它解决不了什么 + +一篇定义文章应该说清定义在哪里止步。 + +它不会让建模变容易。决定商机是什么、谁可以关闭一个商机、超过 30% 的折扣需要什么,仍然是一如既往缓慢、有争议、靠人的工作;格式只决定结果是否小到能整个读完、是否严到能在编写时就拒绝错误。 + +它不推理。如果你的问题确实需要对领域模型做推断——类层级、推导出的事实、一个能发现后果的引擎——那是本文明确说「不是」的那种知识表示本体。去选那个工具,别指望这一个长出推理器来。 + +它不是架在你现有系统之上的一层。联邦接入外部数据源默认只读,并被描述为早期阶段,所以一份架在遗留数据库之上的可执行本体,从只读模型开始,然后一个对象一个对象地争取写入。[把内部系统迁到元数据](/en/blog/how-to-move-an-internal-system-to-metadata/)是这条路的诚实版本,包括那些答案是「根本不要迁」的情形。 + +还有,新颖的逻辑仍然是代码。钩子和动作体是它的去处,在围栏之内,而它们仍然是需要人审阅的代码。本体让这些代码更小、有边界;它不会让代码消失,README 也原话这么说。 + +## 托管,或在你自己的基础设施上 + +README 描述了上面的循环——编码 Agent 在你的仓库里写元数据,并通过 MCP 操作运行中的应用——然后说:「想把同一个循环托管起来、在浏览器里用、什么都不用安装?那就是 ObjectOS——建立在 ObjectStack 之上的商业运行环境。」ObjectOS 既可托管在浏览器中、无需安装(ObjectOS Cloud),也可自管部署在你自己的基础设施上(ObjectOS Enterprise);无论哪个版本,你都可以导出本体,拿到开源的 ObjectStack 运行时上运行。这正是第四个承诺以产品边界的形式说出来:无论本体跑在哪个宿主上,它都是你的。 + +## 从 Agent 路径开始 + +上面没有一件事需要你凭信仰接受定义;README 自己的五分钟路径就能产出一份你读得懂的定义。 + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +在你的编码 Agent 里打开项目,描述需求。README 的例子是一个支持台:一个 `ticket` 对象,带主题、描述、一个优先级选择和一个状态选择;一个只在尚未解决的工单上显示的 Resolve 动作;一个「未解决工单」列表视图和一个 Support 导航组;做完以后跑 `npm run validate`。然后预览结果: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +先看 Agent 产出的 diff,再看控制台。对象、动作和它的可见性谓词、视图——你可以用删除测试把每一行归入核心或投影,而门禁已经拒掉了那些跑不起来的行。如果 Agent 需要引导,[如何为 ObjectStack 配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆盖了规则文件、技能包和工具服务器。它写出的那份定义就是软件,而且就在你的仓库里。 diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx new file mode 100644 index 0000000..3420908 --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx @@ -0,0 +1,198 @@ +--- +# @generated zh-Hant from zh-Hans (s2twp) — edit the zh-Hans file; delete this line to hand-maintain. +title: 「本體即軟體」到底是什麼意思?可執行業務本體的完整定義 +description: 這句話是 ObjectStack 的定位,這篇是它的定義:可執行業務本體由六類核心構成,有四種投影、三個「不是」,還有一條分辨核心與投影的測試。 +author: ObjectStack Team +date: 2026-10-08 +status: published +topic: app-building +audience: developer +solutions: [] +industries: [] +cover: ./cover.svg +tags: + - 本體 + - 可執行業務本體 +--- + +**先給結論**:「本體即軟體。」是 ObjectStack README 的第一句話,緊跟其後的那一行就是定義:「一份可執行的業務本體。AI 編寫它,執行時執行它,Agent 操作它,你擁有它。」可執行業務本體,是你的業務裡那份由執行時直接執行的定義。它的核心是六類東西——物件與欄位、關係、動作、權限、流程、Agent 與工具定義——編譯成一個工件,資料庫、API、UI 和 Agent 工具都從這個工件推匯出來。檢視、儀表盤、應用和翻譯是它的投影,不是它的一部分。它不是知識表示意義上的本體,不是架在你現有系統之上的語義層,也不是哲學意義上的形式本體。程式碼不會消失,它搬進了執行時:鉤子、動作體、CEL 和受限 JSX。 + +這就是全部主張。本文接下來把它拆開:這句話為什麼說「即」而不說「描述」;六類核心;投影,以及一條區分二者的測試;三個「不是」;程式碼去了哪裡;以及這句話最終歸結成的四個承諾。來源是 README 和它連結的概念頁,引用而非轉述;再加上同一倉庫裡自帶的 CRM 示例,測量而非描述。 + +## 這句話為什麼說「即」 + +大多數企業本體是在描述軟體。它們挨著真正跑業務的系統——一個客戶模型、一張「什麼連著什麼」的圖、一份對「營收」的受控定義——而底下的系統照常運轉,毫不知情。這種描述有用,但也是可選的:刪掉它,什麼都不會停。 + +ObjectStack 這句話寫成一個等式,是因為關係正好反過來:定義本身就是在跑的那個東西。刪掉本體,應用就沒有了——沒有表、沒有介面、沒有介面、沒有可供 Agent 呼叫的工具。概念頁用一行寫出整條流水線: + +```text +objectstack.config.ts -> objectstack.json -> ObjectStack runtime -> database · REST / realtime API · UI · MCP server +``` + +你在 `objectstack.config.ts` 裡編寫型別化後設資料;它編譯成 `objectstack.json` 工件;執行時讀取工件,推匯出其餘一切。README 給同一張圖的說明更短:「一份型別化定義 → 資料庫 · REST API · 客戶端 SDK · UI · MCP 工具。」這裡沒有第二個工件在「當」軟體、而本體只是談論它。這就是「即」的含義,下面的一切都由此推出。 + +## 六類核心 + +概念頁把核心定義為六類東西。下表中間一列保留它的原話,右邊一列指向每一類在自帶 CRM 示例(ObjectStack 倉庫裡的 `examples/app-crm`)中的位置,這樣定義可以對照一份真實的定義來核驗,而不是對照一句口號。 + +| 核心 | 它宣告什麼 | 在自帶 CRM 示例中 | +| :--- | :--- | :--- | +| **物件與欄位** | 業務中的實體及其型別化屬性——校驗、公式、預設值、檔案 | `src/objects/`:客戶、聯絡人、線索、商機、商機明細、活動 | +| **關係** | 物件之間的查詢(lookup)與主從(master-detail)連結 | 商機上的 `Field.lookup('crm_account', …)`;商機明細是它的主從子物件 | +| **動作** | 使用者或 Agent 可以對一條記錄發起的操作,以及它們顯示的條件 | `src/actions/convert-lead.action.ts`,只在線索尚未轉化時可見 | +| **權限** | 誰可以讀、建、改、刪什麼——RBAC、行級與欄位級安全、共享 | `src/security/sales-positions.ts`:一個 `crm_sales_user` 權限集,以及一個只能新增線索的訪客檔案 | +| **流程** | 業務過程——DAG 流程、記錄觸發器、定時任務、審批、工作流 | `src/flows/convert-lead.flow.ts`,一個螢幕流程:建立客戶與商機,再把線索標記為已轉化 | +| **Agent 與工具定義** | 應用自帶的 AI Agent、它們可以呼叫的工具,以及塑造它們的技能 | 本示例未宣告。每個物件本來就是一個 MCP 工具;動作用 `ai: { exposed: true }` 主動開放 | + +一個測量,用的是 README 自己在「自己量一量」下面印出來的那條命令:在提交 `9f9510f2`,CRM 示例的定義是 **31 個檔案、1,917 行 TypeScript**。這些檔案裝著六個物件、三個檢視、一個儀表盤、一個應用、一條線索轉化流程、一個動作、一個鉤子、兩個權限集、三個崗位、一個翻譯包、一個頁面和種子資料——整個應用,是一份你讀得完的 diff。這個數字的意義不在於它抽象地「小」,而在於一個 Agent 能把整份定義一次裝下,並從定義本身回答「我改了這裡會弄壞什麼?」——README 把這叫作「小到 AI 能整個裝下的應用」。 + +核心同時也是——按構造就是——屬於你的那部分。概念頁寫道:「它是一份定義,開放且有版本。它以普通 TypeScript 的形式住在你的倉庫裡,可以作為 diff 審閱,按 Apache-2.0 規範並授權——不是一張鎖在廠商系統裡、只能通過該廠商控制台觸達的圖。」 + +## 投影,以及「刪除測試」 + +檢視、儀表盤、應用和翻譯用同樣的後設資料編寫、由同一道門禁校驗,但它們不是本體的一部分。概念頁稱之為投影,並逐一用一個分句解釋:「列表檢視把一個物件的欄位和權限投影到一張網格上;儀表盤把物件上的查詢投影到圖表上;應用把物件、檢視和動作組合成一個可導航的產品;翻譯把每一個標籤投影到一種語言。」 + +![六類核心經 ObjectStack 執行時編譯成資料庫、API、控制台 UI 和 MCP 工具;檢視、儀表盤、應用和翻譯作為投影位於本體框之外;鉤子、動作體、CEL 和受限 JSX 在執行時的圍欄內執行](./six-kind-core.svg) + +讓這個區分可操作的是這一句:「改動核心,投影跟著變;刪掉一個投影,它所展示的業務不變。」把它變成一條測試,你就能一步給專案裡任何一份定義歸類。姑且叫它**刪除測試**: + +> 刪掉這份定義。如果資料庫結構、API,或者使用者與 Agent 能做的事變了,它就是核心。如果消失的只是一個頁面、一張圖、一個選單或一個標籤,它就是投影。 + +拿 CRM 示例來對照。刪掉 `src/views/opportunity.view.ts`,商機物件、它的欄位、它的查詢關係和它的權限集都還在;REST 介面照常應答;MCP 工具照樣存在。刪掉 `src/objects/opportunity.object.ts`,檢視沒了可投影的東西,流程沒了可建立的東西,權限集點名的物件也不復存在。前者是投影,後者才是軟體。 + +這個區分為什麼值得起個名字,同一頁也說了:「這個區分劃定了 Agent 為了推理業務必須裝下的範圍:是核心,而不是每一個螢幕。」一個在審閱審批閾值改動的 Agent,需要把物件、權限和流程擺在眼前;它不需要恰好把結果畫成圖表的每一個儀表盤。 + +## 它不是的三樣東西 + +「本體」這個詞從其他領域帶來了幾十年的含義,README 的定義有相當一部分篇幅是在說哪些含義在這裡不適用。否定有三個,而且各不相同。 + +| 它不是 | 那樣東西是什麼 | 為什麼這裡不同 | +| :--- | :--- | :--- | +| **知識表示意義上的本體** | OWL、RDF 和知識圖譜*描述*一個領域,帶有類繼承和公理,並由推理引擎*推理* | 沒有物件繼承、沒有公理、沒有推理器。定義先被校驗——由 Zod schema 和 `os validate`——然後被執行。`abstract` 這個鍵正是因此從規範中移除(ADR-0049) | +| **架在你現有系統之上的語義層** | 在你已經執行的系統之上的一層只讀對映:它描述業務,但不執行業務 | 本體*就是*系統:資料庫、API、UI 和 Agent 工具都從它推匯出來。聯邦接入外部資料來源是可以的,預設只讀,而且還處於早期 | +| **哲學意義上的形式本體** | 對「一般而言什麼存在」的主張 | 它不作此類主張。它是一家企業的應用,被定義到足以執行的精確程度 | + +![三列對比:知識表示本體被描述並被推理;語義層是現有系統之上的只讀對映;可執行業務本體先被校驗、再被執行成資料庫、API、UI 和 Agent 工具](./validated-not-reasoned.svg) + +第一個否定最容易被忽略,因為它是一個設計選擇,不是一個侷限。推理引擎是第二個真相來源:它在執行時推匯出的事實,從未寫在任何審閱者讀得到的地方。ObjectStack 的規則來自概念頁,正好相反:「執行時不做任何推理:跑的就是被校驗過的東西。」如果一條權限不在 diff 裡,它就不在系統裡。 + +第二個否定對任何在評估平臺的人都重要,因為「語義層」這個詞正在同一場對話裡被用來指兩件不同的事。語義層可以是對「一個數字意味著什麼」的出色定義——[與知識圖譜的比較](/en/blog/ontology-vs-semantic-layer-vs-knowledge-graph/)給了它公允的辯護。它是一個名詞層。可執行業務本體帶著動詞:動作、權限和流程決定什麼可以被做、由誰來做、接下來發生什麼。文件也註明,他們在一個更窄的意義上使用「語義層」,指報表與儀表盤繫結的分析資料集層;那個窄義講的是報表,不是這句話的意思。 + +第三個否定最短,也最誠實。這個格式不對世界作主張。它主張的是:一家企業的應用可以被陳述到足夠精確,讓執行時能執行它、讓審閱者能整個讀完它。 + +## 程式碼去了哪裡 + +一份被執行而非被推理的定義,仍然要表達 schema 表達不了的東西:成交時自動跳到 100% 的機率,線索轉化後消失的按鈕,帶版式的落地頁。README 的句子是「程式碼不會消失:它搬進了執行時,成為鉤子、動作體、CEL 和受限 JSX」,概念頁給出了理由:這四樣都「在執行時的權限與審計圍欄內執行」,而且「沒有一樣散落在一個本體隨後還得去描述的程式碼庫裡」。 + +CRM 示例裡的一個鉤子,是掛在物件生命週期上的一個函式: + +```ts +export const OpportunityStageHook = defineHook({ + name: 'opportunity_stage_probability', + object: 'crm_opportunity', + events: ['beforeInsert', 'beforeUpdate'], + priority: 100, + handler: async (ctx: HookContext) => { + const input = ctx.input as { stage?: string; probability?: number }; + if (input.stage === 'closed_won') input.probability = 100; + if (input.stage === 'closed_lost') input.probability = 0; + }, +}); +``` + +CEL 是公式、謂詞和策略的表示式語言。同一個示例用它寫一個計算欄位,也用它寫一個動作的可見性: + +```ts +expected_revenue: Field.formula({ + label: 'Expected Revenue', + expression: cel`(record.amount == null ? 0 : record.amount) * (record.probability == null ? 0 : record.probability) / 100`, +}), +``` + +```ts +visible: 'has(record.status) && record.status != "converted"', +``` + +受限 JSX 是自定義頁面的寫法,而「受限」正是重點:它「被解析成一棵服務端驅動的 UI 樹,永遠不會作為程式碼執行」。做這件事的包在 README 裡被列為一個只解析、從不執行的編譯器。動作體是第四個去處,承載一個動作被呼叫時在服務端做的工作。 + +四處的模式是同一個:允許程式碼,但程式碼不是業務所在的地方。業務住在六類核心裡,程式碼在這六類所劃定的圍欄內執行,接受與一次點選或一次工具呼叫相同的權限和審計。 + +## 先校驗,再執行 + +執行時對工件做的事可以用兩個動詞描述,概念頁是有意同時使用它們的。它**推導**:「可互換驅動之上的資料庫結構、一套生成的 REST 與即時 API、控制台的服務端驅動 UI,以及一個 MCP 伺服器」。它也**強制執行**:「RBAC、行級與欄位級安全、共享和審計,對一次 REST 請求、一次控制台點選和一次 Agent 工具呼叫一視同仁。」 + +README 用一個物件演示第一個動詞。宣告一張工單: + +```ts +import { ObjectSchema, Field } from '@objectstack/spec/data'; + +export const Ticket = ObjectSchema.create({ + name: 'support_desk_ticket', + label: 'Ticket', + sharingModel: 'private', // org-wide default — the security gate requires it + fields: { + subject: Field.text({ label: 'Subject', required: true, searchable: true }), + status: Field.select({ + label: 'Status', + required: true, + options: [ + { label: 'Open', value: 'open', color: '#3B82F6', default: true }, + { label: 'Resolved', value: 'resolved', color: '#10B981' }, + ], + }), + due_date: Field.date({ label: 'Due Date' }), + }, +}); +``` + +然後「REST API 在物件存在的那一刻就存在了——沒有控制器要寫」: + +```bash +curl http://localhost:3000/api/v1/data/support_desk_ticket +``` + +注意帶註釋的那行 `sharingModel: 'private'`。安全姿態是物件的一部分,門禁要求它必須在:`os validate`「拒絕那些能通過型別檢查、卻會在執行時靜默失敗的後設資料:懸空繫結、錯誤的 CEL 謂詞、缺失的安全姿態」。這是校驗的那一半。執行的那一半是,從這個物件推匯出的每一個表面——表、介面、控制台網格、MCP 工具——在每一次呼叫時都對照同一套權限檢查,所以一個通過 MCP 觸達這張工單的 Agent,看到的正是擁有相同權限的人會看到的東西。 + +## 四個承諾,以「可執行」領頭 + +README 把這句話壓縮成四個詞:「可執行 · AI 可編寫 · Agent 可操作 · 歸你所有」。每一個都是一個有機制支撐的主張,順序也不是裝飾——第一個是其餘三個的立足點。 + +**可執行。** 定義就是在跑的那個東西。上面兩節的全部內容都是這個承諾:一個工件、推匯出的表面、每次呼叫都強制執行、執行時不做推理。沒有它,「AI 可編寫」就只是 AI 在寫文件。 + +**AI 可編寫。**「Agent 寫的是型別化後設資料——不是一個程式碼庫。門禁拒絕那些會在執行時靜默失敗的東西,Agent 在你看到之前就把它改好了。」腳手架「安裝 AI 技能包並寫入一份 `AGENTS.md`,所以你的 Agent 一開始就裝載了協議規則」。README 的四道門——型別化、已校驗、已審閱、受治理——是 Agent 的錯誤到不了生產的原因,也是它說明這件事能成立的理由:「Agent 的錯誤變成了有定位的、可糾正的文本,它自己能讀、能改」。 + +**Agent 可操作。**「物件是工具,動作是工具,權限決定 Agent 可以呼叫什麼,審計記錄它做了什麼。」執行中的應用「以 MCP 伺服器的形式在 `/api/v1/mcp` 提供它——預設開啟」,Agent「在與人相同的權限和 RLS 之下」操作它。由 Agent 寫出的同一份本體,隨後由 Agent 使用,經過同一道圍欄。 + +**歸你所有。**「這個倉庫裡的一切都是開放棧——協議、微核心、SDK、CLI 和生產執行時,Apache-2.0,沒有開放核心的小字。」而定義本身「以普通 TypeScript 的形式住在你的倉庫裡,在你的版本控制裡有版本,可以作為 diff 審閱——不是一張鎖在廠商系統裡的圖」。本體是你握著的一個檔案,不是你租來的一個租戶。 + +## 它解決不了什麼 + +一篇定義文章應該說清定義在哪裡止步。 + +它不會讓建模變容易。決定商機是什麼、誰可以關閉一個商機、超過 30% 的折扣需要什麼,仍然是一如既往緩慢、有爭議、靠人的工作;格式只決定結果是否小到能整個讀完、是否嚴到能在編寫時就拒絕錯誤。 + +它不推理。如果你的問題確實需要對領域模型做推斷——類層級、推匯出的事實、一個能發現後果的引擎——那是本文明確說「不是」的那種知識表示本體。去選那個工具,別指望這一個長出推理器來。 + +它不是架在你現有系統之上的一層。聯邦接入外部資料來源預設只讀,並被描述為早期階段,所以一份架在遺留資料庫之上的可執行本體,從只讀模型開始,然後一個物件一個物件地爭取寫入。[把內部系統遷到後設資料](/en/blog/how-to-move-an-internal-system-to-metadata/)是這條路的誠實版本,包括那些答案是「根本不要遷」的情形。 + +還有,新穎的邏輯仍然是程式碼。鉤子和動作體是它的去處,在圍欄之內,而它們仍然是需要人審閱的程式碼。本體讓這些程式碼更小、有邊界;它不會讓程式碼消失,README 也原話這麼說。 + +## 託管,或在你自己的基礎設施上 + +README 描述了上面的迴圈——編碼 Agent 在你的倉庫裡寫後設資料,並通過 MCP 操作執行中的應用——然後說:「想把同一個迴圈託管起來、在瀏覽器裡用、什麼都不用安裝?那就是 ObjectOS——建立在 ObjectStack 之上的商業執行環境。」ObjectOS 既可託管在瀏覽器中、無需安裝(ObjectOS Cloud),也可自管部署在你自己的基礎設施上(ObjectOS Enterprise);無論哪個版本,你都可以匯出本體,拿到開源的 ObjectStack 執行時上執行。這正是第四個承諾以產品邊界的形式說出來:無論本體跑在哪個宿主上,它都是你的。 + +## 從 Agent 路徑開始 + +上面沒有一件事需要你憑信仰接受定義;README 自己的五分鐘路徑就能產出一份你讀得懂的定義。 + +```bash +npm create objectstack@latest my-app && cd my-app +``` + +在你的編碼 Agent 裡開啟專案,描述需求。README 的例子是一個支援臺:一個 `ticket` 物件,帶主題、描述、一個優先順序選擇和一個狀態選擇;一個只在尚未解決的工單上顯示的 Resolve 動作;一個「未解決工單」列表檢視和一個 Support 導航組;做完以後跑 `npm run validate`。然後預覽結果: + +```bash +npx os dev --ui # → http://localhost:3000/_console/ +``` + +先看 Agent 產出的 diff,再看控制台。物件、動作和它的可見性謂詞、檢視——你可以用刪除測試把每一行歸入核心或投影,而門禁已經拒掉了那些跑不起來的行。如果 Agent 需要引導,[如何為 ObjectStack 配置它](/en/blog/how-to-point-your-agent-at-objectstack/)覆蓋了規則檔案、技能包和工具伺服器。它寫出的那份定義就是軟體,而且就在你的倉庫裡。 diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core-en.svg b/content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core-en.svg new file mode 100644 index 0000000..4b7536f --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core-en.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + Six kinds in, four surfaces out — and what stays outside the box + The core is validated, then run. Projections use the same metadata and the same gate, and are not part of the ontology. + + + + + THE ONTOLOGY — SIX-KIND CORE + Objects and fields + Relations + Actions + Permissions + Flows + Agent & tool definitions + objectstack.config.ts + → objectstack.json + + + + validated + Zod · os validate + + then run + + + + + OBJECTSTACK RUNTIME + Derives, then enforces + on every call — REST request, + Console click, agent tool call alike + RBAC · RLS · FLS · sharing · audit + + + CODE, INSIDE THE FENCE + hooks · action bodies + CEL · constrained JSX + Runs under the same permissions + and audit as a click or a tool call + + Nothing is reasoned over at runtime: + what runs is what was validated. + objectstack.json → running app + + + + + + + + + + + Database schema + on an interchangeable driver + Postgres · MySQL · SQLite · Mongo + + + + REST / realtime API + generated — no controllers to write + /api/v1/data/<object> + + + + Console UI + server-driven: records, boards, + dashboards — rendered, not coded + + + + MCP tools + every object is a tool, on by default; + actions opt in — same permissions + + + + + projected onto screens, charts, menus and locales + + + + + PROJECTIONS — AUTHORED AS METADATA, NOT PART OF THE ONTOLOGY + Views + Dashboards + Apps + Translations + Validated by the same gate. Remove one and the business it showed is unchanged; change the core and they follow. + + + + WHY THE BOX MATTERS + What an agent must hold to reason + about the business is the core — + six kinds — not every screen. + Source: the ObjectStack README and + the Business Ontology concepts page. + diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core.svg b/content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core.svg new file mode 100644 index 0000000..fedb6e9 --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/six-kind-core.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + 六类进去,四个表面出来——以及什么留在框外 + 核心先校验、再运行。投影用同样的元数据、过同一道门禁,但不是本体的一部分。 + + + + + 本体——六类核心 + 对象与字段 + 关系 + 动作 + 权限 + 流程 + Agent 与工具定义 + objectstack.config.ts + → objectstack.json + + + + 先校验 + Zod · os validate + + 再运行 + + + + + OBJECTSTACK 运行时 + 先推导,再强制执行 + 每一次调用——REST 请求、 + 控制台点击、Agent 工具调用一视同仁 + RBAC · RLS · FLS · 共享 · 审计 + + + 代码在围栏内运行 + 钩子 · 动作体 + CEL · 受限 JSX + 接受与一次点击或一次工具调用 + 相同的权限与审计 + + 运行时不做任何推理: + 跑的就是被校验过的东西。 + objectstack.json → 运行中的应用 + + + + + + + + + + + 数据库结构 + 在可互换的驱动之上 + Postgres · MySQL · SQLite · Mongo + + + + REST / 实时 API + 自动生成——没有控制器要写 + /api/v1/data/<object> + + + + 控制台 UI + 服务端驱动:记录、看板、仪表盘 + 渲染出来的,不是写出来的 + + + + MCP 工具 + 每个对象都是工具,默认开启; + 动作主动开放——权限相同 + + + + + 投影到页面、图表、菜单和语言上 + + + + + 投影——用元数据编写,但不是本体的一部分 + 视图 + 仪表盘 + 应用 + 翻译 + 由同一道门禁校验。删掉一个投影,它所展示的业务不变;改动核心,投影跟着变。 + + + + 这个框为什么重要 + Agent 为了推理业务必须装下的, + 是核心——六类东西—— + 而不是每一个屏幕。 + 来源:ObjectStack README 与 + 「业务本体」概念页。 + diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned-en.svg b/content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned-en.svg new file mode 100644 index 0000000..cc44dfa --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned-en.svg @@ -0,0 +1,83 @@ + + + + + + + Validated, not reasoned over: three things the word can mean + The same word, three different objects. Only the third one is what "the ontology is the software" means. + + + + + 1 · KNOWLEDGE-REPRESENTATION + A knowledge ontology + OWL · RDF · knowledge graphs + + Describes a domain + Carries class inheritance and axioms + Is reasoned over by an inference engine + WHAT COMES OUT + Inferred facts, derived at runtime — + never written where a reviewer reads them + + + Not this. + No inheritance, no axioms, no reasoner. + + + + + + + 2 · SEMANTIC LAYER + A layer over systems + the ones you already run + + A read-only mapping over existing data + Describes the business + Does not run it + WHAT COMES OUT + Consistent metrics and queries + over data that lives somewhere else + + + Not this. + Here the ontology is the system itself. + + + + + + + 3 · EXECUTABLE BUSINESS ONTOLOGY + The definition that runs + one business's application, precisely + + Six kinds: objects and fields, relations, + actions, permissions, flows, agents and tools + Validated: Zod schemas and os validate + Then run — nothing reasoned over at runtime + WHAT COMES OUT + The database, the API, the UI + and the agent tools, derived from it + + + This. + What runs is what was validated. + + + + Nor a formal ontology in the philosophical sense: it makes no claim about what exists in general. It is one business's application, defined precisely enough to run. + Source: the ObjectStack README, "What we mean by ontology", and the Business Ontology concepts page. + diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned.svg b/content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned.svg new file mode 100644 index 0000000..951ec5d --- /dev/null +++ b/content/blog/what-does-the-ontology-is-the-software-mean/validated-not-reasoned.svg @@ -0,0 +1,83 @@ + + + + + + + 先校验、不推理:「本体」这个词的三种意思 + 同一个词,三样不同的东西。只有第三种才是「本体即软件」的意思。 + + + + + 1 · 知识表示 + 知识本体 + OWL · RDF · 知识图谱 + + 描述一个领域 + 带有类继承与公理 + 由推理引擎对它做推理 + 产出什么 + 运行时推导出的事实—— + 从未写在审阅者读得到的地方 + + + 不是这个。 + 没有继承、没有公理、没有推理器。 + + + + + + + 2 · 语义层 + 现有系统之上的一层 + 架在你已经在运行的系统上 + + 对既有数据的只读映射 + 描述业务 + 但不运行业务 + 产出什么 + 一致的指标与查询, + 数据却住在别处 + + + 不是这个。 + 在这里,本体本身就是系统。 + + + + + + + 3 · 可执行业务本体 + 被运行的那份定义 + 一家企业的应用,定义到足以运行 + + 六类:对象与字段、关系、动作、 + 权限、流程、Agent 与工具定义 + 先校验:Zod schema 与 os validate + 再运行——运行时不做任何推理 + 产出什么 + 数据库、API、UI + 和 Agent 工具,全部由它推导 + + + 就是这个。 + 跑的就是被校验过的东西。 + + + + 它也不是哲学意义上的形式本体:不对「一般而言什么存在」作主张。它是一家企业的应用,被定义到足以运行的精确程度。 + 来源:ObjectStack README「What we mean by ontology」一节,以及「业务本体」概念页。 + From ae4eabb96c18e8ec4b80489bf170b5b50af7158c Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 15 Oct 2026 11:27:49 +0000 Subject: [PATCH 02/24] content(blog): re-slot the pillar definition post to Thursday 2026-10-15 The post was written for the Thursday 2026-10-08 slot and dated for it, but it was never merged that day, so the slot passed with the post still unpublished. Under the publishing-cadence rules in AGENTS.md, a post's `date` is its planned slot and merge time is publish time; the forward-only rule protects the dates of posts that are already live, and this one is not. The seat therefore moves it to the next free slot, today, Thursday 2026-10-15, and merges it today. Only the `date` frontmatter line changes, in the seven hand-maintained locales (en, zh-Hans, ja, de, es, fr, ko), bare-date format kept and no `updated` added; zh-Hant is regenerated from zh-Hans with `pnpm gen:zh-hant`. No other file changes. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- .../what-does-the-ontology-is-the-software-mean/index.de.mdx | 2 +- .../what-does-the-ontology-is-the-software-mean/index.es.mdx | 2 +- .../what-does-the-ontology-is-the-software-mean/index.fr.mdx | 2 +- .../what-does-the-ontology-is-the-software-mean/index.ja.mdx | 2 +- .../what-does-the-ontology-is-the-software-mean/index.ko.mdx | 2 +- .../blog/what-does-the-ontology-is-the-software-mean/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/what-does-the-ontology-is-the-software-mean/index.de.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx index 8e64c4e..0ae1624 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.de.mdx @@ -2,7 +2,7 @@ title: "Was bedeutet „Die Ontologie ist die Software“? Eine ausführbare Geschäftsontologie, definiert" description: "Der Satz ist ObjectStacks Positionierung; dies ist seine Definition: ein Kern aus sechs Arten, vier Projektionen, drei Dinge, die sie nicht ist, und ein Test, der Kern von Projektion trennt." author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx index 81c089b..d451171 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.es.mdx @@ -2,7 +2,7 @@ title: "¿Qué significa «La ontología es el software»? Una ontología de negocio ejecutable, definida" description: "La frase es el posicionamiento de ObjectStack; esto es su definición: un núcleo de seis tipos, cuatro proyecciones, tres cosas que no es y una prueba para distinguir núcleo de proyección." author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx index ee1894d..88115fe 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.fr.mdx @@ -2,7 +2,7 @@ title: "Que signifie « L’ontologie est le logiciel » ? Une ontologie métier exécutable, définie" description: "La phrase est le positionnement d’ObjectStack ; voici sa définition : un noyau de six sortes, quatre projections, trois choses qu’elle n’est pas, et un test pour distinguer le noyau de la projection." author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx index 3d74745..9b662f4 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.ja.mdx @@ -2,7 +2,7 @@ title: "「オントロジーがソフトウェアである」とはどういう意味か?実行可能なビジネスオントロジーの定義" description: "この一文は ObjectStack のポジショニングであり、本稿はその定義だ。実行可能なビジネスオントロジーには六種の中核、四つの投影、三つの「ではないもの」があり、中核と投影を見分けるテストがある。" author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx index eda1244..515bc06 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.ko.mdx @@ -2,7 +2,7 @@ title: "「온톨로지가 곧 소프트웨어다」는 무슨 뜻인가? 실행 가능한 비즈니스 온톨로지의 정의" description: "이 문장은 ObjectStack의 포지셔닝이고, 이 글은 그 정의다. 실행 가능한 비즈니스 온톨로지는 여섯 종류의 핵심, 네 가지 투영, 그것이 아닌 세 가지로 이루어지며, 핵심과 투영을 가르는 테스트가 있다." author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx index 2b1932e..9f0615a 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.mdx @@ -2,7 +2,7 @@ title: "What Does \"The Ontology Is the Software\" Mean? An Executable Business Ontology, Defined" description: "The line is ObjectStack's positioning; this is its definition. An executable business ontology has a six-kind core, four projections and three things it is not — and a test for telling core from projection." author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx index 33d9617..d29519f 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hans.mdx @@ -2,7 +2,7 @@ title: 「本体即软件」到底是什么意思?可执行业务本体的完整定义 description: 这句话是 ObjectStack 的定位,这篇是它的定义:可执行业务本体由六类核心构成,有四种投影、三个「不是」,还有一条分辨核心与投影的测试。 author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer diff --git a/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx index 3420908..cae0759 100644 --- a/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx +++ b/content/blog/what-does-the-ontology-is-the-software-mean/index.zh-Hant.mdx @@ -3,7 +3,7 @@ title: 「本體即軟體」到底是什麼意思?可執行業務本體的完整定義 description: 這句話是 ObjectStack 的定位,這篇是它的定義:可執行業務本體由六類核心構成,有四種投影、三個「不是」,還有一條分辨核心與投影的測試。 author: ObjectStack Team -date: 2026-10-08 +date: 2026-10-15 status: published topic: app-building audience: developer From 9cce2db85ab8213eb46a761b73b2875e0175e5f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 06:52:21 +0000 Subject: [PATCH 03/24] site: correct seven claims that contradict the docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Certification, AI governance tiers, MCP setup, templates, Cloud data, Business Self-Managed, and two locale overstatements, each re-sourced from objectos and objectstack docs at origin/main: 1. Enterprise card: "SCIM + compliance (SOC 2 / ISO)" -> "SCIM, compliance attestations" (license.mdx:72-73; security.mdx:142 says ObjectOS itself is not certified). 2. AI page: governance claims written per edition (license.mdx:42-43): a taste on Free, in-app AI on Team, AI action audit / guardrails / permission scoping from Business. The "0 bypasses" tile, the Ask audit item, the "never how it is governed" item, the edition table and the two FAQs now say that. 3. MCP page: the npx/--url snippet (no bin, no such flag) is replaced by the documented `claude mcp add --transport http` command and the `{"type":"http","url":".../api/v1/mcp"}` client config (connect-mcp.mdx:26-32, 85-94). 4. Templates page: the real eight-template catalog (todo, contracts, procurement, compliance, helpdesk, content, hr, project; templates.mdx:19-28) replaces the six invented starters; the decision table now carries the documented install / fork / publish paths (templates.mdx:33-62). llms.txt follows the page description. 5. Trust center: the lead quotes E1 (Cloud / self-managed split), the "Self-hosted" tile becomes "2 editions", the data-residency and licence-check items follow index.mdx:63-77 and license.mdx:150-152, and the model-training FAQ says only what the Privacy Policy says (legal.ts:414-415). 6. Pricing: Business Self-Managed (single node, 10-25 AI seats, $540 / AI seat / yr, bring your own model; license.mdx:41, 64-69) is a labelled option under Business, the billing note no longer says self-managed is Enterprise-only, and the Business governance line names the AI governance row. Team/Business headline prices untouched (#214). 7. zh home tile "数据不出网" -> "数据驻留" (= en "Data residency"); ja security tile "0 既定の外部テレメトリ" -> the en "Agent inherits user identity" tile. zh-Hans changed with en; zh-Hant for marketing pages and pricing derives at build time (registry deepS2t / toHant), so no generated file changes. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- content/pages/en/ai.ts | 16 ++++--- content/pages/en/mcp.ts | 16 ++++--- content/pages/en/templates.ts | 63 +++++++++++++++------------ content/pages/en/trust-center.ts | 17 +++++--- content/pages/zh-Hans/ai.ts | 16 ++++--- content/pages/zh-Hans/mcp.ts | 16 ++++--- content/pages/zh-Hans/templates.ts | 63 +++++++++++++++------------ content/pages/zh-Hans/trust-center.ts | 17 +++++--- src/lib/home.ts | 2 +- src/lib/pricing.ts | 18 ++++---- src/lib/security.ts | 2 +- 11 files changed, 140 insertions(+), 106 deletions(-) diff --git a/content/pages/en/ai.ts b/content/pages/en/ai.ts index f559c83..64c2260 100644 --- a/content/pages/en/ai.ts +++ b/content/pages/en/ai.ts @@ -15,7 +15,7 @@ const page = { metrics: [ { value: '2 paths', label: 'Build with a coding agent in source, or by conversation in the cloud' }, { value: '<150k tokens', label: 'A complete CRM, including UI, held in one context window' }, - { value: '0 bypasses', label: 'Every AI action runs inside user permissions and audit' }, + { value: 'Business+', label: 'AI action audit, guardrails, and permission scoping start on ObjectOS Business' }, ], artifact: { eyebrow: 'The build loop', @@ -73,7 +73,7 @@ const page = { }, { title: 'Everything on the record', - body: 'Every Ask query and action lands in the audit log alongside human activity, so review works the same for both.', + body: 'On ObjectOS Business and Enterprise, Ask queries and actions land in the audit log alongside human activity, so review works the same for both.', }, ], }, @@ -93,8 +93,8 @@ const page = { body: 'The MCP server exposes your objects, queries, and actions to whatever model you run — hosted or fully local.', }, { - title: 'Same runtime, same rules', - body: 'Permissions, approvals, and audit apply identically. The edition changes who hosts the AI, never how it is governed.', + title: 'Same runtime, governance by edition', + body: 'On the open-source runtime, MCP tool calls run inside the caller’s permissions and land in the audit log. ObjectOS adds in-app AI by edition: a taste on Free, in-app AI Build and Ask on Team, and AI governance — action audit, guardrails, permission scoping — from Business.', }, ], }, @@ -103,7 +103,9 @@ const page = { columns: ['Edition', 'How AI builds', 'How AI answers'], rows: [ ['Open-source ObjectStack', 'Your coding agent edits metadata source files; review as git diffs', 'Any MCP client queries governed objects with your own model'], - ['ObjectOS Cloud Team & Business', 'In-app AI Build drafts changes; approvals gate structure', 'In-app AI Ask answers and runs approved actions'], + ['ObjectOS Cloud Free', 'A taste of in-app AI Build', 'A taste of in-app AI Ask'], + ['ObjectOS Cloud Team', 'In-app AI Build drafts changes; approvals gate structure', 'In-app AI Ask answers and runs approved actions'], + ['ObjectOS Business (Cloud or Self-Managed)', 'Same, plus AI governance: action audit, guardrails, permission scoping', 'Same, with Ask queries and actions audited and guardrailed'], ['ObjectOS Enterprise', 'Same, plus private deployment and bring-your-own-model', 'Same, plus local models and internal identity'], ], }, @@ -111,12 +113,12 @@ const page = { { question: 'What stops AI from breaking production?', answer: - 'The ObjectStack runtime enforces three guarantees: structural changes require human approval, every action runs inside the requesting user’s permissions, and everything is audited. ObjectOS makes that approval and audit loop available in the production product, so AI can draft quickly but cannot ship silently.', + 'The ObjectStack runtime enforces two guarantees for any agent working over MCP: structural changes require human approval, and every tool call runs inside the requesting user’s permissions and is audited. ObjectOS Business and Enterprise add AI governance for the in-app AI — action audit, guardrails, permission scoping — so AI can draft quickly but cannot ship silently.', }, { question: 'Which models does it use?', answer: - 'Cloud editions run managed models behind AI Build and Ask. The open-source ObjectStack brings your own — any MCP-compatible client or a local model. Enterprise adds bring-your-own-model for private deployments.', + 'Cloud editions run managed models behind AI Build and Ask. The open-source ObjectStack brings your own — any MCP-compatible client or a local model. Business Self-Managed and Enterprise private deployments bring your own model or key.', }, ], } satisfies MarketingPage; diff --git a/content/pages/en/mcp.ts b/content/pages/en/mcp.ts index 0ac7ea6..bbb92f4 100644 --- a/content/pages/en/mcp.ts +++ b/content/pages/en/mcp.ts @@ -21,16 +21,18 @@ const page = { eyebrow: 'Connect in minutes', title: 'Point any MCP client at your runtime.', body: - 'One entry in your client configuration connects the model to your governed objects. From there, every query and action is checked against the signed-in user’s permissions.', - code: `// Any MCP client configuration + 'Every ObjectStack deployment already serves MCP at /api/v1/mcp — on by default, nothing to install. One command, or one entry in your client configuration, connects the model to your governed objects. From there, every query and action is checked against the signed-in user’s permissions.', + code: `# Claude Code — the first tool call opens a browser login +claude mcp add --transport http my-app \\ + https://your-deployment.example.com/api/v1/mcp + +# Any MCP client (.mcp.json), with an API key { "mcpServers": { "objectstack": { - "command": "npx", - "args": [ - "@objectstack/mcp", - "--url", "https://runtime.your-company.com" - ] + "type": "http", + "url": "https://your-deployment.example.com/api/v1/mcp", + "headers": { "x-api-key": "osk_..." } } } }`, diff --git a/content/pages/en/templates.ts b/content/pages/en/templates.ts index 8133ebe..881171c 100644 --- a/content/pages/en/templates.ts +++ b/content/pages/en/templates.ts @@ -5,7 +5,7 @@ const page = { navLabel: 'Templates', title: 'ObjectStack Templates for Governed AI Applications', description: - 'Start from reviewable ObjectStack templates for helpdesk, contracts, procurement, CRM, manufacturing, and employee service workflows instead of asking AI to generate a full app codebase.', + 'Start from the eight forkable ObjectStack templates — todo, contracts, procurement, compliance, helpdesk, content, hr, and project — instead of asking AI to generate a full app codebase.', eyebrow: 'Templates', heroTitle: 'Start with an operating model, not a blank app.', lead: @@ -13,7 +13,7 @@ const page = { primary: { label: 'Open template source', href: 'https://github.com/objectstack-ai/templates' }, secondary: { label: 'See the product tour', href: '/en/product-tour/' }, metrics: [ - { value: '6 starters', label: 'Support, contracts, procurement, CRM, manufacturing, employee service' }, + { value: '8 templates', label: 'todo, contracts, procurement, compliance, helpdesk, content, hr, project' }, { value: 'One layer', label: 'Objects, UI, workflows, APIs, and AI tools from the same definition' }, { value: 'Reviewable', label: 'Each template changes as metadata diffs' }, ], @@ -33,39 +33,49 @@ const page = { { id: 'catalog', eyebrow: 'Catalog', - title: 'Starter applications for common enterprise workflows', + title: 'What ships in the default catalog', copy: - 'These are reference templates, not locked products. Your agent can fork one, change the metadata, and open a diff for review.', + 'Eight forkable starters, each a real, shaped app: install it from the Marketplace in one click, or clone it with the CLI to get a TypeScript codebase to extend. Apache-2.0 — fork without asking. Your agent can fork one, change the metadata, and open a diff for review.', items: [ { - title: 'Helpdesk', - body: 'Tickets, customers, SLAs, queues, summaries, suggested replies, knowledge retrieval, escalation approvals.', - meta: 'Support operations', + title: 'todo', + body: 'Forkable starter for any “things to do, grouped somehow” tool.', + meta: 'Tasks & projects', }, { - title: 'Contracts', - body: 'Counterparties, obligations, renewals, clause extraction, approval stages, redlines, audit trail.', - meta: 'Legal operations', + title: 'contracts', + body: 'Forkable starter for approvals + document AI.', + meta: 'CLM with AI clause extraction', }, { - title: 'Procurement', - body: 'Suppliers, requests, POs, receiving, three-way match, budget checks, exception approval.', - meta: 'Spend control', + title: 'procurement', + body: 'Forkable starter for approval chains + receiving.', + meta: 'Vendors, POs, 3-way match', }, { - title: 'CRM case management', - body: 'Accounts, opportunities, cases, activities, ownership, risk signals, user-scoped AI summaries.', - meta: 'Revenue and service', + title: 'compliance', + body: 'Forkable starter for evidence collection workflows.', + meta: 'SOC 2 / ISO 27001 controls', }, { - title: 'Manufacturing service', - body: 'Assets, work orders, parts, maintenance history, downtime reports, field-service recommendations.', - meta: 'Operations', + title: 'helpdesk', + body: 'Forkable starter for tickets + SLAs + AI co-pilot.', + meta: 'AI-first customer support', }, { - title: 'Employee service', - body: 'Requests, policies, approvals, routing, employee data boundaries, internal knowledge retrieval.', - meta: 'Internal apps', + title: 'content', + body: 'Forkable starter for lifecycle + metrics.', + meta: 'Editorial calendar + channel ROI', + }, + { + title: 'hr', + body: 'Forkable starter for people-centric apps.', + meta: 'Directory, org chart, time-off', + }, + { + title: 'project', + body: 'Forkable starter for PM tools.', + meta: 'Project / task / milestone tracking', }, ], }, @@ -92,12 +102,11 @@ const page = { }, ], table: { - columns: ['Template', 'Objects included', 'Agent-safe actions'], + columns: ['Path', 'What you do', 'What you get'], rows: [ - ['Helpdesk', 'Ticket, Customer, SLA, Knowledge Article', 'Summarize, classify, suggest reply, escalate for approval'], - ['Contracts', 'Contract, Counterparty, Clause, Obligation', 'Extract metadata, flag risk, draft renewal task'], - ['Procurement', 'Supplier, Purchase Request, PO, Receipt', 'Check policy, prepare approval, match exception'], - ['CRM', 'Account, Contact, Opportunity, Case', 'Summarize account, identify risk, propose next step'], + ['Install from the Marketplace', 'Open Marketplace, search for the template, click Install, refresh', 'The app in your nav — records, views, flows, permissions, all live'], + ['Fork as a codebase', 'pnpm dlx @objectstack/cli create my-app --template todo', 'A self-contained pnpm package: *.object.ts, *.view.ts, *.flow.ts files you can edit, lint, test, commit'], + ['Publish your fork', 'pnpm build, then os package publish', 'Your marketplace — customers installing your fork get your brand and your modifications'], ], }, checklistTitle: 'Template review checklist', diff --git a/content/pages/en/trust-center.ts b/content/pages/en/trust-center.ts index 1f41c1d..49a96de 100644 --- a/content/pages/en/trust-center.ts +++ b/content/pages/en/trust-center.ts @@ -1,4 +1,7 @@ import type { MarketingPage } from '../../../src/content-pages/types'; +import { positioning } from '../../../src/lib/positioning'; + +const p = positioning.en; const page = { slug: 'trust-center', @@ -9,11 +12,11 @@ const page = { eyebrow: 'Trust center', heroTitle: 'AI can write software. The harder question is whether you can approve it.', lead: - 'ObjectOS is designed for the procurement, security, and IT review that happens after a demo works. It keeps business data under customer control and makes agent authority explicit in metadata and runtime enforcement.', + `ObjectOS is designed for the procurement, security, and IT review that happens after a demo works. ${p.e1} On either edition, agent authority is explicit in metadata and runtime enforcement.`, primary: { label: 'Read the security model', href: '/en/security/' }, secondary: { label: 'See deployment paths', href: '/en/pricing/' }, metrics: [ - { value: 'Self-hosted', label: 'Run in your VPC, servers, or isolated network' }, + { value: '2 editions', label: 'Hosted on ObjectOS Cloud, or self-managed on your own infrastructure with ObjectOS Enterprise' }, { value: 'User scoped', label: 'Agents inherit the signed-in user permissions' }, { value: 'Auditable', label: 'Reads, writes, tool calls, approvals, and schema changes' }, ], @@ -35,15 +38,15 @@ const page = { eyebrow: 'Boundaries', title: 'Keep sensitive business data inside controlled infrastructure', copy: - 'ObjectOS can deploy and operate the ObjectStack runtime inside customer-controlled infrastructure. Unless you configure external providers, business records, prompts, files, audit logs, and credentials stay inside that boundary.', + 'On ObjectOS Enterprise, the runtime, your business data, user identities, audit log, files, and secrets all live in your own infrastructure; unless you configure external providers, they stay inside that boundary. Business Self-Managed runs its single-node license on your infrastructure too. ObjectOS Cloud is operated by us.', items: [ { title: 'Data residency', - body: 'Connect customer-controlled databases and storage. ObjectOS does not require application records to be copied into a vendor workspace.', + body: 'On ObjectOS Enterprise, connect your own databases and storage: business records, identities, audit logs, and files live there, not in a vendor workspace.', }, { - title: 'No required telemetry', - body: 'The open-source ObjectStack runtime does not need a license callback or product telemetry channel to operate.', + title: 'License check', + body: 'ObjectOS Self-Managed validates its license online (Enterprise air-gapped licenses are offline-validated). The open-source ObjectStack runtime has no telemetry, no license check, and no update ping.', }, { title: 'Model choice', @@ -95,7 +98,7 @@ const page = { { question: 'Does ObjectOS train models on customer data?', answer: - 'Self-hosted deployments are controlled by the customer. Cloud or Enterprise data usage should be governed by the relevant commercial agreement; the product direction is to keep customer data out of general model training unless a customer explicitly agrees.', + 'If you enable AI features or connect a model provider, prompts, context, Customer Data, and outputs may be sent to the selected provider as necessary to perform the request. We do not use Customer Data from paid Cloud Services to train general-purpose models unless the customer affirmatively agrees or a separate agreement expressly permits it. The Privacy Policy is the governing text.', }, { question: 'Can ObjectOS run without internet access?', diff --git a/content/pages/zh-Hans/ai.ts b/content/pages/zh-Hans/ai.ts index f3d0c63..8962b0f 100644 --- a/content/pages/zh-Hans/ai.ts +++ b/content/pages/zh-Hans/ai.ts @@ -15,7 +15,7 @@ const page = { metrics: [ { value: '2 条路径', label: '在源码里用编码 Agent,或在云端用对话' }, { value: '<150k tokens', label: '完整 CRM 全应用(含 UI),一个上下文窗口可容纳' }, - { value: '0 后门', label: '每次 AI 动作都在用户权限与审计之内' }, + { value: 'Business 起', label: 'AI 动作审计、护栏与权限限定从 ObjectOS Business 开始提供' }, ], artifact: { eyebrow: '构建闭环', @@ -73,7 +73,7 @@ const page = { }, { title: '一切留痕', - body: '每次 Ask 的查询与动作都和人的操作一样写入审计日志,审查方式完全相同。', + body: '在 ObjectOS Business 与企业版中,Ask 的查询与动作和人的操作一样写入审计日志,审查方式完全相同。', }, ], }, @@ -93,8 +93,8 @@ const page = { body: 'MCP 服务器把对象、查询和动作暴露给你运行的任何模型——托管的或完全本地的。', }, { - title: '同一运行时,同一规则', - body: '权限、审批和审计的执行方式完全一致。版本只改变谁来托管 AI,从不改变治理方式。', + title: '同一运行时,治理按版本', + body: '在开源运行时上,MCP 工具调用在调用者权限内执行并写入审计日志。ObjectOS 按版本提供产品内 AI:免费版可试用,团队版提供产品内 AI Build 与 Ask,AI 治理——动作审计、护栏、权限限定——从商业版起提供。', }, ], }, @@ -103,7 +103,9 @@ const page = { columns: ['版本', 'AI 如何构建', 'AI 如何回答'], rows: [ ['开源 ObjectStack', '编码 Agent 编辑元数据源码文件,以 git diff 审阅', '任意 MCP 客户端用你自己的模型查询受治理对象'], - ['ObjectOS 云端 Team 与 Business', '产品内 AI Build 起草变更,审批把守结构', '产品内 AI Ask 回答问题并执行已批准动作'], + ['ObjectOS 云端免费版', '产品内 AI Build 试用', '产品内 AI Ask 试用'], + ['ObjectOS 云端团队版', '产品内 AI Build 起草变更,审批把守结构', '产品内 AI Ask 回答问题并执行已批准动作'], + ['ObjectOS 商业版(云端或自管)', '同上,另加 AI 治理:动作审计、护栏、权限限定', '同上,Ask 的查询与动作受审计与护栏约束'], ['ObjectOS 企业版', '同上,另支持私有部署与自带模型', '同上,另支持本地模型与内部身份系统'], ], }, @@ -111,12 +113,12 @@ const page = { { question: '怎么防止 AI 弄坏生产环境?', answer: - 'ObjectStack 运行时强制三条保证:结构性变更必须经人审批;每个动作都在发起用户的权限之内执行;一切都被审计。ObjectOS 把这套审批与审计闭环提供在生产产品中,因此 AI 可以快速起草,却无法悄悄上线。', + 'ObjectStack 运行时对任何经 MCP 工作的 Agent 强制两条保证:结构性变更必须经人审批;每次工具调用都在发起用户的权限之内执行并被审计。ObjectOS 商业版与企业版为产品内 AI 另加 AI 治理——动作审计、护栏、权限限定——因此 AI 可以快速起草,却无法悄悄上线。', }, { question: '用的是什么模型?', answer: - '云端版本由托管模型驱动 AI Build 与 Ask。开源 ObjectStack 自带模型——任意兼容 MCP 的客户端或本地模型。企业版支持在私有部署中自带模型。', + '云端版本由托管模型驱动 AI Build 与 Ask。开源 ObjectStack 自带模型——任意兼容 MCP 的客户端或本地模型。商业版自管与企业版私有部署自带模型或密钥。', }, ], } satisfies MarketingPage; diff --git a/content/pages/zh-Hans/mcp.ts b/content/pages/zh-Hans/mcp.ts index 9340f98..b84139a 100644 --- a/content/pages/zh-Hans/mcp.ts +++ b/content/pages/zh-Hans/mcp.ts @@ -21,16 +21,18 @@ const page = { eyebrow: '几分钟接入', title: '把任意 MCP 客户端指向你的运行时。', body: - '在客户端配置里加一条,就把模型接到了你的受治理对象上。此后每一次查询和动作都会按登录用户的权限校验。', - code: `// 任意 MCP 客户端的配置 + '每个 ObjectStack 部署都已在 /api/v1/mcp 提供 MCP——默认开启,无需安装。一条命令,或在客户端配置里加一条,就把模型接到了你的受治理对象上。此后每一次查询和动作都会按登录用户的权限校验。', + code: `# Claude Code——首次工具调用会打开浏览器登录 +claude mcp add --transport http my-app \\ + https://your-deployment.example.com/api/v1/mcp + +# 任意 MCP 客户端(.mcp.json),使用 API key { "mcpServers": { "objectstack": { - "command": "npx", - "args": [ - "@objectstack/mcp", - "--url", "https://runtime.your-company.com" - ] + "type": "http", + "url": "https://your-deployment.example.com/api/v1/mcp", + "headers": { "x-api-key": "osk_..." } } } }`, diff --git a/content/pages/zh-Hans/templates.ts b/content/pages/zh-Hans/templates.ts index a214817..ac7acaa 100644 --- a/content/pages/zh-Hans/templates.ts +++ b/content/pages/zh-Hans/templates.ts @@ -5,7 +5,7 @@ const page = { navLabel: '模板', title: 'ObjectStack 模板:受治理 AI 应用的起步应用', description: - '从可审阅的 ObjectStack 模板起步——Helpdesk、合同、采购、CRM、制造与员工服务——而不是让 AI 生成一整个应用代码库。', + '从八个可 fork 的 ObjectStack 模板起步——todo、contracts、procurement、compliance、helpdesk、content、hr、project——而不是让 AI 生成一整个应用代码库。', eyebrow: '模板', heroTitle: '从一个运营模型开始,而不是一个空白应用。', lead: @@ -13,7 +13,7 @@ const page = { primary: { label: '打开模板源码', href: 'https://github.com/objectstack-ai/templates' }, secondary: { label: '查看产品导览', href: '/en/product-tour/' }, metrics: [ - { value: '6 个起步', label: '客服、合同、采购、CRM、制造、员工服务' }, + { value: '8 个模板', label: 'todo、contracts、procurement、compliance、helpdesk、content、hr、project' }, { value: '一层定义', label: '对象、UI、流程、API 与 AI 工具出自同一份定义' }, { value: '可审阅', label: '每个模板的变更都是元数据 diff' }, ], @@ -33,39 +33,49 @@ const page = { { id: 'catalog', eyebrow: '目录', - title: '常见企业工作流的起步应用', + title: '默认目录里有什么', copy: - '这些是参考模板,不是锁死的产品。你的 Agent 可以 fork 一份、修改元数据,然后开一个 diff 供人审阅。', + '八个可 fork 的起步应用,每个都是真实成形的应用:在 Marketplace 一键安装,或用 CLI 克隆得到可扩展的 TypeScript 代码库。Apache-2.0,fork 无需请示。你的 Agent 可以 fork 一份、修改元数据,然后开一个 diff 供人审阅。', items: [ { - title: 'Helpdesk 客服', - body: '工单、客户、SLA、队列、摘要、建议回复、知识检索、升级审批。', - meta: '客服运营', + title: 'todo', + body: '可 fork 的起步应用:任何“待办事项,按某种方式分组”的工具。', + meta: '任务与项目', }, { - title: '合同管理', - body: '相对方、义务、续约、条款提取、审批阶段、红线批注、审计轨迹。', - meta: '法务运营', + title: 'contracts', + body: '可 fork 的起步应用:审批 + 文档 AI。', + meta: '合同全生命周期管理,含 AI 条款提取', }, { - title: '采购', - body: '供应商、采购申请、PO、收货、三单匹配、预算检查、例外审批。', - meta: '支出管控', + title: 'procurement', + body: '可 fork 的起步应用:审批链 + 收货。', + meta: '供应商、PO、三单匹配', }, { - title: 'CRM 案件管理', - body: '客户、商机、案件、活动、归属、风险信号、用户级 AI 摘要。', - meta: '营收与服务', + title: 'compliance', + body: '可 fork 的起步应用:证据收集工作流。', + meta: 'SOC 2 / ISO 27001 控制项', }, { - title: '制造服务', - body: '资产、工单、备件、维护历史、停机报告、现场服务建议。', - meta: '运营', + title: 'helpdesk', + body: '可 fork 的起步应用:工单 + SLA + AI 副驾。', + meta: 'AI 优先的客户支持', }, { - title: '员工服务', - body: '申请、制度、审批、路由、员工数据边界、内部知识检索。', - meta: '内部应用', + title: 'content', + body: '可 fork 的起步应用:生命周期 + 指标。', + meta: '编辑日历 + 渠道 ROI', + }, + { + title: 'hr', + body: '可 fork 的起步应用:以人为中心的应用。', + meta: '通讯录、组织架构、休假', + }, + { + title: 'project', + body: '可 fork 的起步应用:项目管理工具。', + meta: '项目 / 任务 / 里程碑跟踪', }, ], }, @@ -92,12 +102,11 @@ const page = { }, ], table: { - columns: ['模板', '包含的对象', 'Agent 安全动作'], + columns: ['路径', '你做什么', '你得到什么'], rows: [ - ['Helpdesk', '工单、客户、SLA、知识文章', '摘要、分类、建议回复、升级送审'], - ['合同', '合同、相对方、条款、义务', '提取元数据、标记风险、起草续约任务'], - ['采购', '供应商、采购申请、PO、收货单', '校验政策、准备审批、匹配例外'], - ['CRM', '客户、联系人、商机、案件', '客户摘要、识别风险、建议下一步'], + ['从 Marketplace 安装', '打开 Marketplace,搜索模板,点击 Install,刷新', '应用出现在导航里——记录、视图、流程、权限全部就绪'], + ['fork 为代码库', 'pnpm dlx @objectstack/cli create my-app --template todo', '一个自包含的 pnpm 包:*.object.ts、*.view.ts、*.flow.ts 文件,可编辑、lint、测试、提交'], + ['发布你的 fork', 'pnpm build,然后 os package publish', '你的 marketplace——安装你的 fork 的客户拿到的是你的品牌与你的修改'], ], }, checklistTitle: '模板评审清单', diff --git a/content/pages/zh-Hans/trust-center.ts b/content/pages/zh-Hans/trust-center.ts index 187b6a8..9527912 100644 --- a/content/pages/zh-Hans/trust-center.ts +++ b/content/pages/zh-Hans/trust-center.ts @@ -1,4 +1,7 @@ import type { MarketingPage } from '../../../src/content-pages/types'; +import { positioning } from '../../../src/lib/positioning'; + +const p = positioning['zh-Hans']; const page = { slug: 'trust-center', @@ -9,11 +12,11 @@ const page = { eyebrow: '信任中心', heroTitle: 'AI 能写软件。更难的问题是:你敢不敢批准它。', lead: - 'ObjectOS 为演示成功之后的采购、安全与 IT 评审而设计。它让业务数据留在客户控制之下,让 Agent 的权限在元数据与运行时执行中保持显式。', + `ObjectOS 为演示成功之后的采购、安全与 IT 评审而设计。${p.e1}无论哪个版本,Agent 的权限都在元数据与运行时执行中保持显式。`, primary: { label: '阅读安全模型', href: '/en/security/' }, secondary: { label: '查看部署路径', href: '/en/pricing/' }, metrics: [ - { value: '自托管', label: '运行在你的 VPC、服务器或隔离网络中' }, + { value: '两种版本', label: '托管在 ObjectOS Cloud,或以 ObjectOS Enterprise 自管部署在你自己的基础设施上' }, { value: '用户级', label: 'Agent 继承登录用户的权限' }, { value: '可审计', label: '读、写、工具调用、审批与 schema 变更' }, ], @@ -35,15 +38,15 @@ const page = { eyebrow: '边界', title: '敏感业务数据留在受控基础设施内', copy: - 'ObjectOS 可以在客户控制的基础设施中部署并运营 ObjectStack 运行时。除非你显式配置外部服务,业务记录、提示词、文件、审计日志与凭据都留在这个边界内。', + '在 ObjectOS 企业版中,运行时、业务数据、用户身份、审计日志、文件与密钥都在你自己的基础设施内;除非你显式配置外部服务,它们都留在这个边界内。商业版自管同样把单节点许可运行在你的基础设施上。ObjectOS Cloud 由我们运营。', items: [ { title: '数据驻留', - body: '连接客户控制的数据库与存储。ObjectOS 不要求把应用记录复制进供应商的工作区。', + body: '在 ObjectOS 企业版中,连接你自己的数据库与存储:业务记录、身份、审计日志与文件都存放在那里,而不是供应商的工作区。', }, { - title: '无强制遥测', - body: '开源 ObjectStack 运行时不需要 license 回调或产品遥测通道即可运行。', + title: '许可证校验', + body: '自管部署的 ObjectOS 在线校验许可证(企业版离线许可证可离线校验)。开源 ObjectStack 运行时没有遥测、不查许可证、也不检查更新。', }, { title: '模型可选', @@ -95,7 +98,7 @@ const page = { { question: 'ObjectOS 会用客户数据训练模型吗?', answer: - '自托管部署由客户完全控制。云端与企业版的数据使用以相应商业协议为准;产品方向是:除非客户明确同意,客户数据不进入通用模型训练。', + '如果你启用 AI 功能或连接模型提供商,为完成请求,提示词、上下文、客户数据和输出可能会发送给所选提供商。除非客户明确同意或单独协议明确允许,我们不会使用付费云服务中的客户数据训练通用模型。以隐私政策为准。', }, { question: 'ObjectOS 能在无互联网环境运行吗?', diff --git a/src/lib/home.ts b/src/lib/home.ts index f9d4980..86c46c9 100644 --- a/src/lib/home.ts +++ b/src/lib/home.ts @@ -139,7 +139,7 @@ const zhHans: HomeCopy = { securityTitle: ['数据留在你的网络里,', 'AI 在权限边界内工作'], securityCopy: 'ObjectOS 企业版可以把 ObjectStack 应用部署并运营在你的基础设施中。业务数据、身份、审计日志和文件仍由你控制;AI Agent 通过受控工具访问对象,并继承登录用户已有的权限。', securityItems: [ - { title: '数据不出网', copy: '连接你的数据库和存储;业务记录、身份、审计日志和文件都留在那里。自管部署的 ObjectOS 在线校验许可证(企业版离线许可证可离线校验);开源 ObjectStack 运行时没有遥测、不查许可证、也不检查更新。' }, + { title: '数据驻留', copy: '连接你的数据库和存储;业务记录、身份、审计日志和文件都留在那里。自管部署的 ObjectOS 在线校验许可证(企业版离线许可证可离线校验);开源 ObjectStack 运行时没有遥测、不查许可证、也不检查更新。' }, { title: '继承用户权限', copy: 'Agent 以登录用户身份执行,遵守对象级、记录级和字段级权限,看不到用户本来无权看到的数据。' }, { title: '审批与审计', copy: '结构性变更进入人机协作审批队列;读取、写入、工具调用和权限变更都能写入审计日志。' }, { title: '支持离线部署', copy: '可在 VPC、本地服务器或隔离网络中运行,并可接入本地模型、内部身份系统和自有密钥管理。' }, diff --git a/src/lib/pricing.ts b/src/lib/pricing.ts index 01c3708..811b095 100644 --- a/src/lib/pricing.ts +++ b/src/lib/pricing.ts @@ -43,7 +43,7 @@ const pricingEn: PricingCopy = { intro: 'Start on the free plan and upgrade when you need more. Paid plans are billed per AI seat: you pay for the people who use AI, while viewers and non-AI users stay free. Prefer the open-source path? ObjectStack is Apache-2.0: self-host its runtime, bring your own AI through MCP, author metadata as source files, and review changes as diffs. ObjectOS adds the in-app AI Build and Ask experience on Cloud and Enterprise.', billingNote: - 'Prices are in USD per AI seat. The headline price is billed annually (about two months free); monthly billing is also available. Cancel anytime. Exact quotas are shown in the app and at checkout. Private and self-hosted deployment are available on Enterprise, including bring-your-own-model and offline payment options.', + 'Prices are in USD per AI seat. The headline price is billed annually (about two months free); monthly billing is also available. Cancel anytime. Exact quotas are shown in the app and at checkout. Self-managed deployment starts on Business: Business Self-Managed is a single-node license for 10–25 AI seats at $540 per AI seat per year, bring your own model or key. ObjectOS Enterprise is the full private deployment, with offline and invoice payment.', plans: [ { name: 'Free', @@ -87,9 +87,10 @@ const pricingEn: PricingCopy = { features: [ 'Everything in Team, plus:', 'SSO / SAML', - 'Advanced permissions and governance', + 'AI governance: action audit, guardrails, permission scoping', 'More included AI credits', '1-year audit retention', + 'Self-Managed option: single node, 10–25 AI seats, $540 / AI seat / yr, bring your own model', ], cta: 'Get started', ctaHref: GET_STARTED, @@ -100,9 +101,9 @@ const pricingEn: PricingCopy = { cadence: 'contact sales', tagline: 'For larger organizations with custom security, deployment, and contracting needs.', features: [ - 'Private or self-hosted deployment', + 'Full private deployment, including air-gapped', 'Volume AI-seat pricing', - 'SCIM + compliance (SOC 2 / ISO)', + 'SCIM, compliance attestations', 'Clustering / HA / multi-node', 'SLA + dedicated support', ], @@ -121,7 +122,7 @@ const pricingZhHans: PricingCopy = { intro: '从免费版开始,需要更多时再升级。付费套餐按 AI 席位计费——只为真正使用 AI 的人付费;查看者与非 AI 用户免费。偏好开源路径?ObjectStack 采用 Apache-2.0:可自托管运行时、经 MCP 自带 AI,以源码方式编写元数据并审阅 diff。ObjectOS 在云端与企业版中提供产品内 AI Build 与 Ask。', billingNote: - '价格以美元计,按 AI 席位计费。标价为年付价(约省两个月);也可选择月付。可随时取消。具体配额在应用内和结账时显示。私有/自托管部署在企业版提供(自带模型;支持线下付款)。', + '价格以美元计,按 AI 席位计费。标价为年付价(约省两个月);也可选择月付。可随时取消。具体配额在应用内和结账时显示。自管部署从商业版起:商业版自管(Business Self-Managed)为单节点许可,10–25 个 AI 席位,每 AI 席每年 $540,自带模型或密钥;企业版为完整私有部署,支持线下与发票付款。', plans: [ { name: '免费版', @@ -165,9 +166,10 @@ const pricingZhHans: PricingCopy = { features: [ '含团队版全部,另加:', 'SSO / SAML', - '高级权限与治理', + 'AI 治理:动作审计、护栏、权限限定', '更高 AI 额度', '1 年审计留存', + '自管选项:单节点、10–25 个 AI 席位、每 AI 席每年 $540、自带模型', ], cta: '开始使用', ctaHref: GET_STARTED, @@ -178,9 +180,9 @@ const pricingZhHans: PricingCopy = { cadence: '联系销售', tagline: '面向有定制安全与合同需求的大型组织。', features: [ - '私有/自托管部署', + '完整私有部署,含离线隔离(air-gap)', 'AI 席位批量定价', - 'SCIM + 合规(SOC 2 / ISO)', + 'SCIM、合规鉴证', '集群 / HA / 多节点', 'SLA + 专属支持', ], diff --git a/src/lib/security.ts b/src/lib/security.ts index 0aea158..dae6f9f 100644 --- a/src/lib/security.ts +++ b/src/lib/security.ts @@ -163,7 +163,7 @@ const ja: SecurityCopy = { hero: ['データは自社ネットワーク内に。', 'AI は権限の内側で動く。'], lead: `ObjectOS は ${positioning.ja.descriptor}です。ObjectOS Enterprise では ObjectStack ランタイムを自社インフラに展開でき、業務データ、ID、監査ログ、ファイル、シークレットは自社管理のままです。`, metrics: [ - { value: '0', label: '既定の外部テレメトリ' }, + { value: 'Agent', label: 'ユーザーの ID を継承' }, { value: '5 層', label: 'ID からフィールドまでの権限モデル' }, { value: 'HITL', label: 'AI 変更の人による承認' }, ], From 51e704de4fe9e90e547e6098b0311ec7631005a0 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:57:30 +0000 Subject: [PATCH 04/24] fix(layout): phone-width tables, code blocks and body diagrams Article tables on a phone no longer run each row onto one line. Cells wrap, each column keeps a 6.5rem floor (three columns fit the 328px column of a 360px phone), and overflow-wrap: anywhere keeps an overflowing table at exactly its column count x 104px. The widest English table goes from 2,035px to 328px at 360x800; the 12 tables with four to six columns still scroll, to at most 625px, with the always-on scrollbar and trailing-edge fade they already had. Code blocks get the same two scroll affordances at every width, since they overflow at desktop sizes too. The marketing decision table drops its 760px floor on a phone and takes the same wrap, floor and affordances. Every scroll container is keyboard-reachable and named: post tables get tabindex="0" and aria-labelledby their section heading (the article title when no heading precedes them); code blocks keep Shiki's tabindex and gain role="group" with a "Code (lang)" name; the decision-table wrapper becomes a named, focusable group. group, not region, because a region is a landmark and duplicate names fail axe landmark-unique. Body images become links to their own asset: one tap opens the diagram in the browser, where pinch zoom takes its smallest text past 11px. The link's name is the image's alt text. No SVG changes; desktop geometry of tables and images is unchanged. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- src/components/MarketingEvidencePage.astro | 60 +++++++- src/layouts/BlogPost.astro | 151 +++++++++++++++++++-- 2 files changed, 198 insertions(+), 13 deletions(-) diff --git a/src/components/MarketingEvidencePage.astro b/src/components/MarketingEvidencePage.astro index 138ea30..bd5529d 100644 --- a/src/components/MarketingEvidencePage.astro +++ b/src/components/MarketingEvidencePage.astro @@ -124,7 +124,7 @@ const localizeHref = (href: string): string => {

{ui.decisionEyebrow}

{ui.decisionTitle}

-
+
@@ -511,6 +511,64 @@ const localizeHref = (href: string): string => { tr:last-child td { border-bottom: none; } + /* On a phone the 760px floor put the third column wholly off-screen, with + nothing to say it was there. Same treatment as the article tables + (BlogPost.astro): cells wrap, each column keeps a 6.5rem floor — three + columns fit a 360px phone — and if the wrap still overflows (a 320px + phone), it draws an always-on scrollbar and a trailing-edge fade. The + wrap, a scroll container at some widths, is focusable and named by the + heading; `group`, not `region`, because the section is that region. */ + @media (max-width: 640px) { + .table-wrap { + overscroll-behavior-x: contain; + } + .table-wrap::-webkit-scrollbar { + height: 6px; + } + .table-wrap::-webkit-scrollbar-track { + background: color-mix(in srgb, var(--surface-sunk) 70%, transparent); + border-radius: 999px; + } + .table-wrap::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--ink) 32%, transparent); + border-radius: 999px; + } + @supports (animation-timeline: scroll(self inline)) { + .table-wrap { + animation: decision-table-edge-fade linear both; + animation-timeline: scroll(self inline); + } + } + @keyframes decision-table-edge-fade { + from { + mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent); + } + to { + mask-image: linear-gradient(to right, #000 100%, transparent); + } + } + table { + min-width: 0; + } + th, + td { + box-sizing: border-box; + min-width: 6.5rem; + padding-right: 0.6rem; + padding-left: 0.6rem; + overflow-wrap: anywhere; + } + /* At the desktop padding, type size and tracking, words such as + "ObjectStack" and "visualizations" do not fit a 6.5rem column and + break mid-word: 28 splits over the 30 en + zh-Hans pages at 360px. + With the three tightened here, 1. */ + th { + letter-spacing: 0.02em; + } + td { + font-size: 0.82rem; + } + } .checklist { display: grid; grid-template-columns: minmax(260px, 0.78fr) minmax(420px, 1.22fr); diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro index 1c71e6c..573f53a 100644 --- a/src/layouts/BlogPost.astro +++ b/src/layouts/BlogPost.astro @@ -99,6 +99,85 @@ const jsonLd = [ ], }, ]; + +// The post body is MDX this layout does not author, so the attributes the +// article owes its body markup are added here, once, on the rendered HTML — +// not in every post × eight locales. +// - A table is a scroll container on a phone (see the stylesheet), and a +// scroll container a keyboard cannot reach is axe's +// `scrollable-region-focusable`. It gets `tabindex="0"` and is named by +// the heading of the section it sits in — the article title for a table +// that comes before the first heading. +// - A code block is a scroll container at every width. Shiki already gives +// it `tabindex="0"`; it gets a name ("Code (bash)") so the focus stop +// announces what it is. `aria-label` is not allowed on a bare `pre`, whose +// role is generic, hence `role="group"` — not `region`: a region is a +// landmark, and two "Code (bash)" landmarks on one page fail axe's +// `landmark-unique`. +// - A body image becomes a link to its own asset. A 1200-wide diagram fit +// into a 328px column renders its smallest text at 3–4px; one tap opens +// the asset in the browser, whose own pinch zoom takes that text past +// legible. The link's accessible name is the image's alt text, so an +// image without alt text (decorative) is left alone rather than turned +// into a nameless link. +const CODE_LABEL: Record = { + en: 'Code', + 'zh-Hans': '代码', + 'zh-Hant': '程式碼', + ja: 'コード', + ko: '코드', + de: 'Code', + es: 'Código', + fr: 'Code', +}; +const PLAIN_LANGUAGES = new Set(['plaintext', 'plain', 'text', 'txt']); +const attrOf = (tag: string, name: string): string | undefined => + tag.match(new RegExp(`\\s${name}="([^"]*)"`))?.[1]; +const withAttrs = (tag: string, attrs: Record): string => { + const added = Object.entries(attrs) + .filter(([name]) => attrOf(tag, name) === undefined) + .map(([name, value]) => ` ${name}="${value}"`) + .join(''); + return tag.replace(/\s*\/?>$/, (end) => `${added}${end}`); +}; +const annotateBody = (html: string): string => { + let headingId = 'article-title'; + let linkDepth = 0; + return html.replace( + /]*>|]*>|<\/a>|]*>|]*>|]*>/g, + (tag) => { + if (tag === '') { + linkDepth = Math.max(0, linkDepth - 1); + return tag; + } + if (tag.startsWith(' 0 || !src || !attrOf(tag, 'alt')?.trim()) return tag; + return `${tag}`; + } + ); +}; +const bodyHtml = annotateBody(await Astro.slots.render('default')); --- {t.by} {data.author} -

{data.title}

+

{data.title}

{data.description &&

{data.description}

} { @@ -201,7 +280,7 @@ const jsonLd = [
- +
{ @@ -446,6 +525,13 @@ const jsonLd = [ display: block; margin: 1.5rem auto; } + /* A block box with no padding or border: the image's margins collapse + through it, so the link adds a tap target and a focus ring without + moving the image by a pixel at any width. */ + .prose .figure-link { + display: block; + cursor: zoom-in; + } .prose :not(pre) > code { font-family: var(--font-mono); font-size: 0.88em; @@ -466,6 +552,37 @@ const jsonLd = [ border: 1px solid var(--line); background: color-mix(in srgb, var(--surface-sunk) 72%, var(--surface)); box-shadow: 0 18px 42px -36px rgba(var(--shadow), 0.45); + overscroll-behavior-x: contain; + } + /* A code block scrolls sideways at every width, not only on a phone, so + it carries the two scroll affordances the phone table carries (see the + comments there) at every width. Shiki paints the block dark in both + themes, so its scrollbar is drawn light. A block that fits draws no + scrollbar and its scroll timeline is inactive, so it is unchanged. */ + .prose pre::-webkit-scrollbar { + height: 6px; + } + .prose pre::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.08); + border-radius: 999px; + } + .prose pre::-webkit-scrollbar-thumb { + background: rgba(225, 228, 232, 0.42); + border-radius: 999px; + } + @supports (animation-timeline: scroll(self inline)) { + .prose pre { + animation: prose-scroll-edge-fade linear both; + animation-timeline: scroll(self inline); + } + } + @keyframes prose-scroll-edge-fade { + from { + mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent); + } + to { + mask-image: linear-gradient(to right, #000 100%, transparent); + } } .prose table { width: 100%; @@ -548,11 +665,29 @@ const jsonLd = [ max-width: 100%; overflow-x: auto; font-size: 0.82rem; - white-space: nowrap; /* Keep a horizontal swipe that reaches the end of the table from chaining into the page/browser behind it. */ overscroll-behavior-x: contain; } + /* Cells wrap; each column keeps a floor. Unwrapped (`nowrap`), a row of + prose cells ran as long as its longest sentence: 2,035px on a 328px + column, seven screens of swiping per row. 6.5rem (104px, padding + included) is the widest floor at which three columns still fit the + 328px column of a 360px phone (3 × 104 = 312), so every two- and + three-column table, 57 of the 69 in the English corpus, needs no + sideways scroll at 360 or 390; four to six columns still scroll, + to at most 6 × 104 = 624px. `overflow-wrap: anywhere` is what keeps a + column at its floor: it lets a long identifier break instead of + setting the column's minimum width, so an overflowing table is never + wider than its column count × 104px. */ + .prose th, + .prose td { + box-sizing: border-box; + min-width: 6.5rem; + padding-right: 0.6rem; + padding-left: 0.6rem; + overflow-wrap: anywhere; + } /* Affordance 1 — a scrollbar that is always drawn, not just mid-gesture. Styling ::-webkit-scrollbar opts the container out of overlay scrollbars on iOS Safari and Blink, which is the whole point. @@ -584,18 +719,10 @@ const jsonLd = [ table that does not scroll. */ @supports (animation-timeline: scroll(self inline)) { .prose table { - animation: prose-table-edge-fade linear both; + animation: prose-scroll-edge-fade linear both; animation-timeline: scroll(self inline); } } - @keyframes prose-table-edge-fade { - from { - mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent); - } - to { - mask-image: linear-gradient(to right, #000 100%, transparent); - } - } } From 435313ee373d1324c5e4a8a1316b4586256d353e Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 10:16:40 +0000 Subject: [PATCH 05/24] fix(layout): state the overflowing-table bound with its border Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- src/layouts/BlogPost.astro | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/layouts/BlogPost.astro b/src/layouts/BlogPost.astro index 573f53a..eae2dee 100644 --- a/src/layouts/BlogPost.astro +++ b/src/layouts/BlogPost.astro @@ -676,10 +676,11 @@ const bodyHtml = annotateBody(await Astro.slots.render('default')); 328px column of a 360px phone (3 × 104 = 312), so every two- and three-column table, 57 of the 69 in the English corpus, needs no sideways scroll at 360 or 390; four to six columns still scroll, - to at most 6 × 104 = 624px. `overflow-wrap: anywhere` is what keeps a - column at its floor: it lets a long identifier break instead of - setting the column's minimum width, so an overflowing table is never - wider than its column count × 104px. */ + to at most 6 × 104px + 1px of border = 625px. `overflow-wrap: + anywhere` is what keeps a column at its floor: it lets a long + identifier break instead of setting the column's minimum width, so + an overflowing table is never wider than its column count × 104px + plus its border. */ .prose th, .prose td { box-sizing: border-box; From 0e5d9c85530fe55fc4bd9044ace3f5a5c3b70992 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 10:33:44 +0000 Subject: [PATCH 06/24] fix(layout): focusable artifact code block, phone-width security table The marketing artifact code block is a scroll container; it gets tabindex="0" and role="group" named by its section heading, like the other scroll containers, which clears the last axe scrollable-region-focusable finding on the marketing pages. The security page's data-residency table had the same 620px floor as the decision table, which put its third column ("Leaves boundary?") wholly off-screen on a phone. It takes the same phone block: cells wrap, a 6.5rem column floor, tighter padding, type size and tracking, and the scrollbar and trailing-edge fade if it still overflows. Its wrapper is focusable and named by the section heading. Desktop is unchanged. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- src/components/MarketingEvidencePage.astro | 2 +- src/components/SecurityPage.astro | 59 +++++++++++++++++++++- 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/components/MarketingEvidencePage.astro b/src/components/MarketingEvidencePage.astro index bd5529d..d49996d 100644 --- a/src/components/MarketingEvidencePage.astro +++ b/src/components/MarketingEvidencePage.astro @@ -83,7 +83,7 @@ const localizeHref = (href: string): string => { { page.artifact.code ? ( -
{page.artifact.code}
+
{page.artifact.code}
) : (
    {page.artifact.steps?.map((step) =>
  1. {step}
  2. )} diff --git a/src/components/SecurityPage.astro b/src/components/SecurityPage.astro index 790a8bf..78bed25 100644 --- a/src/components/SecurityPage.astro +++ b/src/components/SecurityPage.astro @@ -60,10 +60,10 @@ const blogHref = `${blogPath(locale)}${blogSlug}`;

    {copy.residencyTitle}

    -

    {copy.residencyTitle}

    +

    {copy.residencyTitle}

    {copy.residencyLead}

    -
    +
@@ -294,6 +294,61 @@ const blogHref = `${blogPath(locale)}${blogSlug}`; tr:last-child td { border-bottom: none; } + /* On a phone the 620px floor put the third column off-screen, with nothing + to say it was there. Same treatment as the article tables (BlogPost.astro) + and the marketing decision table: cells wrap, each column keeps a 6.5rem + floor (three columns fit a 360px phone), and if the wrap still overflows + (a 320px phone) it draws an always-on scrollbar and a trailing-edge fade. + The wrap is focusable and named by the section heading. The tighter + padding, type size and tracking match the decision table; here they + shorten the wrapped table from 694px to 569px at 360px. */ + @media (max-width: 640px) { + .table-wrap { + overscroll-behavior-x: contain; + } + .table-wrap::-webkit-scrollbar { + height: 6px; + } + .table-wrap::-webkit-scrollbar-track { + background: color-mix(in srgb, var(--surface-sunk) 70%, transparent); + border-radius: 999px; + } + .table-wrap::-webkit-scrollbar-thumb { + background: color-mix(in srgb, var(--ink) 32%, transparent); + border-radius: 999px; + } + @supports (animation-timeline: scroll(self inline)) { + .table-wrap { + animation: residency-table-edge-fade linear both; + animation-timeline: scroll(self inline); + } + } + @keyframes residency-table-edge-fade { + from { + mask-image: linear-gradient(to right, #000 calc(100% - 34px), transparent); + } + to { + mask-image: linear-gradient(to right, #000 100%, transparent); + } + } + table { + min-width: 0; + } + th, + td { + box-sizing: border-box; + min-width: 6.5rem; + padding-right: 0.6rem; + padding-left: 0.6rem; + overflow-wrap: anywhere; + } + th { + letter-spacing: 0.02em; + } + td { + font-size: 0.82rem; + } + } .section-head { max-width: 760px; margin-bottom: 2.5rem; From 83d1088dae2678e2c5cb8310fd15f5a79d22c747 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 6 Oct 2026 09:33:09 +0000 Subject: [PATCH 07/24] site: fix the layout breaks, add 404 pages, gate internal links on the build Six visible defects from the 2026-10-06 site audit, plus the gate that would have caught the broken links. - Home "Latest insights": the cover's height attribute became CSS height:500px and beat aspect-ratio, cutting each 16:9 cover to a 344x500 sliver. height:auto, a 16:9 box (the covers' own ratio), contain for the odd 16:10 cover. - Blog index: the feature card spanned three rows beside three cards (~1470px) with ~670px of content, leaving an ~800px band. It now takes its own row, cover beside text from 1024px, the other three below. - 404: a top-level 404.html (Cloudflare Pages otherwise runs the site as an SPA and answers every missing URL 200 with the language redirect) and a localized one per locale at /404.html, moved there by a small build hook. noindexed, no canonical, out of the sitemap. - Cluster pages linked topic hubs not built in de/es/fr/ja/ko. They now link the hub really built (this locale's, else English), and content-lint --dist fails on any internal link to a page the build did not produce. - Footer links and the drawer's close label come from the i18n copy. - Alignment: .item > span; align-content:start on capability cards and next-page links; no min-height floor on template/compare cards; pricing cards on a subgrid; footer brand nowrap; the phone rule now clears the odd capability items' border; non-breaking space before card arrows. Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- astro.config.mjs | 42 ++++- scripts/content-lint.mjs | 200 ++++++++++++++++++++- scripts/seo-smoke.mjs | 40 ++++- src/components/ArticleCard.astro | 43 +++++ src/components/ArticleList.astro | 15 +- src/components/ClusterPage.astro | 32 +++- src/components/Header.astro | 2 +- src/components/MarketingEvidencePage.astro | 16 +- src/components/MarketingHome.astro | 33 ++-- src/components/NotFound.astro | 134 ++++++++++++++ src/components/Pricing.astro | 15 +- src/layouts/Base.astro | 35 ++-- src/lib/i18n.ts | 104 ++++++++++- src/lib/posts.ts | 21 +++ src/pages/404.astro | 26 +++ src/pages/[lang]/404.astro | 26 +++ 16 files changed, 730 insertions(+), 54 deletions(-) create mode 100644 src/components/NotFound.astro create mode 100644 src/pages/404.astro create mode 100644 src/pages/[lang]/404.astro diff --git a/astro.config.mjs b/astro.config.mjs index f233d18..5e8c334 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,3 +1,4 @@ +import { readdir, rename, rmdir } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'astro/config'; import mdx from '@astrojs/mdx'; @@ -12,6 +13,39 @@ import { readPostLastmods } from './scripts/lib/post-dates.mjs'; // other page class intentionally gets no `lastmod`. const POST_LASTMOD = readPostLastmods(fileURLToPath(new URL('.', import.meta.url))); +// `//404/`, the per-locale 404 route before `locale404` renames it. +const LOCALE_404 = /^\/[^/]+\/404\/$/; + +/** + * Astro names only the top-level `/404` route `404.html`; `src/pages/[lang]/404.astro` + * is emitted as `/404/index.html`. Cloudflare Pages serves the nearest + * `404.html` to a missing URL, so each locale's page is moved to + * `/404.html`, where Pages finds it for every missing URL under that + * locale. A `/404/` directory left behind would be a real, 200-status + * URL, so `rmdir` (which refuses a non-empty directory) fails the build if + * anything else landed there; `pnpm seo:smoke` asserts every locale has its + * `404.html` and no `404/` directory. + */ +const locale404 = { + name: 'locale-404', + hooks: { + 'astro:build:done': async ({ dir }) => { + const root = fileURLToPath(dir); + for (const entry of await readdir(root, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const from = `${root}${entry.name}/404/index.html`; + try { + await rename(from, `${root}${entry.name}/404.html`); + } catch (error) { + if (error.code === 'ENOENT') continue; + throw error; + } + await rmdir(`${root}${entry.name}/404`); + } + }, + }, +}; + // https://astro.build export default defineConfig({ site: 'https://www.objectos.ai', @@ -21,7 +55,12 @@ export default defineConfig({ integrations: [ mdx(), sitemap({ - filter: (page) => new URL(page).pathname !== '/', + // The root language redirect and the per-locale 404 pages are not + // content (the top-level /404 the integration drops by itself). + filter: (page) => { + const { pathname } = new URL(page); + return pathname !== '/' && !LOCALE_404.test(pathname); + }, // Spread the item through: the integration also fills `links`, // `priority` and `changefreq`, and returning a fresh object would drop // whatever it set. Returning `undefined` would drop the URL entirely, @@ -31,5 +70,6 @@ export default defineConfig({ return lastmod ? { ...item, lastmod: lastmod.toISOString() } : item; }, }), + locale404, ], }); diff --git a/scripts/content-lint.mjs b/scripts/content-lint.mjs index fd39e68..f8137db 100644 --- a/scripts/content-lint.mjs +++ b/scripts/content-lint.mjs @@ -227,8 +227,206 @@ async function checkBuiltProse() { return 1; } +// ─── Built HTML: no internal link to a page the build does not produce ───── +// +// A link is only as good as the page at the other end, and nothing upstream +// can see the other end: `astro build` happily emits `href="/de/blog/topics/x/"` +// whether or not the topics route produced a German `x` hub, and the MDX pass +// below checks only links authors write in post bodies. Components render +// links too — a cluster page's hand-curated `relatedTerms`, a footer, a nav — +// and an audit at 53764e0 found five such links on the de/es/fr/ja/ko cluster +// pages, each to a hub built only in en/zh-Hans/zh-Hant. On Cloudflare Pages a +// missing page is not an error a reader sees in the build; it is the 404 page, +// or (with no top-level 404.html) the language-redirect home answering 200. +// +// So this reads every `` and every `` +// in every built HTML file and resolves it the way Pages does: `/x/` is +// `x/index.html`, `/x` is the file `x`, `x.html` or `x/index.html`, and a +// source pattern in `dist/_redirects` counts as served. Only same-origin +// targets are checked; external sites are not this build's to answer for. +// Relative links on a `404.html` page are errors in themselves: that page +// answers for every missing URL, so a relative link resolves against whatever +// address the reader mistyped. + +const SITE_ORIGIN = 'https://www.objectos.ai'; + +/** Files under `dir`, as `/`-separated paths relative to it. */ +async function distFiles(dir, prefix = '') { + const files = []; + let entries; + try { + entries = await readdir(dir, { withFileTypes: true }); + } catch { + return files; + } + for (const entry of entries) { + const rel = prefix ? `${prefix}/${entry.name}` : entry.name; + if (entry.isDirectory()) files.push(...(await distFiles(path.join(dir, entry.name), rel))); + else if (entry.isFile()) files.push(rel); + } + return files; +} + +/** Start tags of a document with their attributes, outside comments and raw-text bodies. */ +function* startTags(html) { + let i = 0; + while (i < html.length) { + const lt = html.indexOf('<', i); + if (lt === -1) return; + if (html.startsWith('', lt + 4); + i = end === -1 ? html.length : end + 3; + continue; + } + const tag = /^<([a-zA-Z][a-zA-Z0-9-]*)/.exec(html.slice(lt, lt + 64)); + if (!tag) { + i = lt + 1; + continue; + } + const end = tagEnd(html, lt + tag[0].length); + if (end === -1) return; + const name = tag[1].toLowerCase(); + const attrs = new Map(); + const source = html.slice(lt + tag[0].length, end); + for (const m of source.matchAll(/([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g)) { + attrs.set(m[1].toLowerCase(), decodeEntities(m[2] ?? m[3] ?? m[4] ?? '')); + } + yield { name, attrs }; + i = end + 1; + if (RAW_TEXT_ELEMENTS.has(name) && name !== 'title') { + const close = html.toLowerCase().indexOf(` String.fromCodePoint(Number(n))) + .replace(/&#[xX]([0-9a-fA-F]+);/g, (_, n) => String.fromCodePoint(parseInt(n, 16))) + .replace(/"/g, '"') + .replace(/'/g, "'") + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/&/g, '&'); +} + +/** `dist/_redirects` source patterns as matchers (`*` splat, `:name` placeholder). */ +async function redirectSources() { + let text = ''; + try { + text = await readFile(path.join(DIST, '_redirects'), 'utf8'); + } catch { + return []; + } + return text + .split('\n') + .map((line) => line.trim()) + .filter((line) => line && !line.startsWith('#')) + .map((line) => line.split(/\s+/)[0]) + .map((source) => { + const pattern = source + .split('*') + .map((part) => part.replace(/[.+?^${}()|[\]\\]/g, '\\$&').replace(/:[A-Za-z]\w*/g, '[^/]+')) + .join('.*'); + return new RegExp(`^${pattern}$`); + }); +} + +/** The page URL a built HTML file is served at. */ +function servedUrl(file) { + return `${SITE_ORIGIN}/${file.replace(/(^|\/)index\.html$/, '$1')}`; +} + +async function checkBuiltLinks() { + const files = await distFiles(DIST); + const built = new Set(files); + const pages = files.filter((file) => file.endsWith('.html')).sort(); + if (pages.length === 0) { + console.error( + `✗ content lint --dist found no built HTML under ${path.relative(ROOT, DIST)}/ — ` + + `the link check measured nothing, which is not the same as nothing being broken.` + ); + return 1; + } + const redirects = await redirectSources(); + const resolves = (pathname) => { + if (redirects.some((re) => re.test(pathname))) return true; + let rel; + try { + rel = decodeURIComponent(pathname).replace(/^\//, ''); + } catch { + return false; + } + if (rel === '' || rel.endsWith('/')) return built.has(`${rel}index.html`); + return built.has(rel) || built.has(`${rel}.html`) || built.has(`${rel}/index.html`); + }; + + const broken = new Map(); // target -> pages linking it + const relativeOn404 = []; + let checked = 0; + for (const file of pages) { + const html = await readFile(path.join(DIST, file), 'utf8'); + const base = servedUrl(file); + const is404 = /(^|\/)404\.html$/.test(file); + for (const { name, attrs } of startTags(html)) { + const rel = (attrs.get('rel') ?? '').toLowerCase().split(/\s+/); + const isLink = name === 'a' || (name === 'link' && (rel.includes('canonical') || rel.includes('alternate'))); + if (!isLink || !attrs.has('href')) continue; + const href = attrs.get('href').trim(); + if (href.startsWith('#')) continue; + let url; + try { + url = new URL(href, base); + } catch { + broken.set(href, [...(broken.get(href) ?? []), file]); + continue; + } + if (url.origin !== SITE_ORIGIN) continue; + checked++; + if (is404 && !/^(\/|[a-zA-Z][a-zA-Z0-9+.-]*:)/.test(href)) relativeOn404.push({ file, href }); + if (!resolves(url.pathname)) { + const key = url.pathname; + broken.set(key, [...(broken.get(key) ?? []), file]); + } + } + } + if (checked === 0) { + console.error( + `✗ content lint --dist found no internal links in ${pages.length} built pages — ` + + `an empty match is a failure, not a pass.` + ); + return 1; + } + if (broken.size === 0 && relativeOn404.length === 0) { + console.log( + `✓ content lint --dist links passed (${checked} internal links in ${pages.length} built pages resolve to built files)` + ); + return 0; + } + for (const [target, from] of [...broken].sort(([a], [b]) => a.localeCompare(b))) { + const sources = [...new Set(from)]; + console.log(`✗ ${target} is not built — linked from ${sources.length} page${sources.length === 1 ? '' : 's'}:`); + for (const source of sources.slice(0, 8)) console.log(` ${source}`); + if (sources.length > 8) console.log(` … and ${sources.length - 8} more`); + } + for (const { file, href } of relativeOn404) { + console.log(`✗ ${file}: relative link "${href}" — a 404 page is served at every missing URL, so link root-relative`); + } + console.error( + `\n✗ content lint --dist failed (${broken.size} internal link target${broken.size === 1 ? '' : 's'} not built` + + `${relativeOn404.length ? `, ${relativeOn404.length} relative link${relativeOn404.length === 1 ? '' : 's'} on a 404 page` : ''})\n` + + ` Each target above is a URL this build does not produce, so on Cloudflare Pages the reader gets the 404 page.\n` + + ` Fix the producer, not this check: link only pages that are built for that locale (a topic hub exists\n` + + ` only where a published post carries the term — \`builtHubLocale\` in src/lib/posts.ts answers which one).` + ); + return 1; +} + if (args.has('--dist')) { - exit(await checkBuiltProse()); + const prose = await checkBuiltProse(); + const links = await checkBuiltLinks(); + exit(prose || links); } // The frontmatter taxonomy, derived from the module that declares it instead of diff --git a/scripts/seo-smoke.mjs b/scripts/seo-smoke.mjs index 0984330..44a5c94 100644 --- a/scripts/seo-smoke.mjs +++ b/scripts/seo-smoke.mjs @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { readdir, readFile } from 'node:fs/promises'; +import { access, readdir, readFile } from 'node:fs/promises'; import path from 'node:path'; import { cwd, exit } from 'node:process'; @@ -154,6 +154,40 @@ for (const clusterPath of CLUSTER_PATHS) { requireContains('sitemap-0.xml', sitemap, `${SITE}${clusterPath}`, `missing cluster page ${clusterPath}`); } +// The 404 surface. Cloudflare Pages treats a deployment with no top-level +// `404.html` as a single-page app: every missing URL answers 200 with the root +// language-redirect page, which is a soft 404 to crawlers and a silent landing +// on a home page for readers. With one, Pages serves the NEAREST `404.html` to +// the missing URL with status 404, so each locale carries its own at +// `/404.html` (renamed there by the `locale-404` integration in +// astro.config.mjs). A 404 page answers for every missing URL, so it names no +// canonical, is noindexed, stays out of the sitemap, and links the three +// pages a lost reader most likely wants. +const NOT_FOUND_PAGES = [ + { file: '404.html', locale: 'en' }, + ...LOCALES.map((locale) => ({ file: `${locale}/404.html`, locale })), +]; +for (const { file, locale } of NOT_FOUND_PAGES) { + const html = await readDist(file); + if (!html) continue; + requireContains(file, html, ``, 'missing matching html lang'); + requireContains(file, html, '', '404 page must be noindexed'); + requireExcludes(file, html, 'rel="canonical"', '404 page must not name a canonical URL'); + requireExcludes(file, html, 'property="og:url"', '404 page must not name an Open Graph URL'); + // Inside
: the header already links all three on every page, so a + // whole-document match would pass with the 404 body missing. + const main = html.match(/]*>([\s\S]*?)<\/main>/)?.[1] ?? ''; + for (const href of [`/${locale}/`, `/${locale}/blog/`, `/${locale}/pricing/`]) { + requireContains(file, main, `href="${href}"`, `404 page body must link ${href}`); + } +} +for (const locale of LOCALES) { + const leftover = `${locale}/404/index.html`; + const exists = await access(path.join(DIST, leftover)).then(() => true, () => false); + if (exists) issues.push(`${leftover}: the per-locale 404 was not moved to ${locale}/404.html — it is a 200-status page`); +} +requireExcludes('sitemap-0.xml', sitemap, /\/404(\.html|\/)?<\/loc>/, 'lists a 404 page'); + const rootRss = await readDist('rss.xml'); requireContains('rss.xml', rootRss, ' file.endsWith('.html')) .map((file) => path.relative(DIST, file).split(path.sep).join('/')) - .filter((file) => file !== 'index.html'); + .filter((file) => file !== 'index.html') + // Checked above: a 404 page has no canonical URL and no hreflang set. + .filter((file) => !/(^|\/)404\.html$/.test(file)); for (const file of htmlFiles) { const html = await readDist(file); diff --git a/src/components/ArticleCard.astro b/src/components/ArticleCard.astro index 76595d4..f1bd68a 100644 --- a/src/components/ArticleCard.astro +++ b/src/components/ArticleCard.astro @@ -214,6 +214,49 @@ const coverSrc = (cover: unknown): string => .feature .body { padding: 1.35rem 1.45rem 1.55rem; } + /* Desktop: the feature spans a full row (ArticleList) with the cover beside + the text. The cover column is at least the cover's own 16:9 (the padding + box) and stretches with the text when that is taller, cropping a sliver + at the sides instead of leaving a band under the image; text shorter than + the cover is centred, so neither column has an empty band. Below 1024px + the text column gets too narrow and tall for the cover beside it (the + crop passes the ~11% a 16:10 card thumb already takes), so narrower + screens keep the stacked card, alone in its row. */ + @media (min-width: 1024px) { + .feature .card { + display: grid; + grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr); + } + .feature .thumb { + position: relative; + aspect-ratio: auto; + } + .feature .thumb::before { + content: ''; + flex: none; + width: 0; + padding-top: 56.25%; + } + .feature .thumb :global(img) { + position: absolute; + inset: 0; + } + .feature .body { + justify-content: center; + padding: 1.35rem 1.6rem; + } + /* Sized so the text fits beside the cover's 16:9 at the 1072px content + width; longer localized titles fall back to the stretch above. */ + .feature h2 { + font-size: clamp(1.45rem, 2.2vw, 1.85rem); + } + .feature .desc { + -webkit-line-clamp: 3; + } + .feature .facets { + margin-top: 0; + } + } @media (max-width: 640px) { .card { border-radius: 8px; diff --git a/src/components/ArticleList.astro b/src/components/ArticleList.astro index 0547257..ed52e9e 100644 --- a/src/components/ArticleList.astro +++ b/src/components/ArticleList.astro @@ -213,17 +213,26 @@ const c = homeCopy[locale]; margin: 0.45rem 0 0; line-height: 1.65; } + /* The feature gets a row of its own and the other three share the next. + It used to span three rows beside them, but three stacked cards (~1470px) + are more than twice the feature's own content (~670px), so the feature + card stretched with an ~800px band between its description and tags. Its + horizontal layout lives in ArticleCard (`.feature`). */ .featured-grid { list-style: none; display: grid; - grid-template-columns: minmax(0, 1.28fr) minmax(280px, 0.72fr); - grid-auto-flow: dense; + grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1.25rem; margin: 0; padding: 0; } .featured-grid :global(.feature) { - grid-row: span 3; + grid-column: 1 / -1; + } + @media (max-width: 900px) { + .featured-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } } @media (max-width: 640px) { .hero { diff --git a/src/components/ClusterPage.astro b/src/components/ClusterPage.astro index 65ca253..cdb87da 100644 --- a/src/components/ClusterPage.astro +++ b/src/components/ClusterPage.astro @@ -3,6 +3,7 @@ import type { CollectionEntry } from 'astro:content'; import type { Cluster } from '../lib/clusters'; import { clusterCopy, clusterLabels } from '../lib/cluster-i18n'; import { blogPath, formatDate, postPath, type Locale } from '../lib/i18n'; +import { builtHubLocale } from '../lib/posts'; import { termBySlug, termHref, termLabel } from '../lib/terms'; interface Props { @@ -14,6 +15,24 @@ interface Props { const { cluster, locale, posts } = Astro.props; const copy = clusterCopy(cluster, locale); const labels = clusterLabels(locale); +// `relatedTerms` is hand-curated, so a term may have no hub in this locale: +// link the hub that is really built (this locale's, else English's) and drop +// the term when neither exists. `hreflang` marks the English fallback. +const termLinks = ( + await Promise.all( + cluster.relatedTerms.map(async (slug) => { + const term = termBySlug(slug); + const hubLocale = term && (await builtHubLocale(term, locale)); + return term && hubLocale + ? { + href: termHref(hubLocale, term), + label: termLabel(slug, locale), + hreflang: hubLocale === locale ? undefined : hubLocale, + } + : undefined; + }) + ) +).filter((link) => link !== undefined); const coverSrc = (cover: unknown): string => typeof cover === 'string' ? cover @@ -99,14 +118,11 @@ const coverSrc = (cover: unknown): string =>

{labels.explore}

diff --git a/src/components/Header.astro b/src/components/Header.astro index b4cb0de..b90f728 100644 --- a/src/components/Header.astro +++ b/src/components/Header.astro @@ -320,7 +320,7 @@ const menus = ( diff --git a/src/components/MarketingHome.astro b/src/components/MarketingHome.astro index b64a6e6..6f2d70e 100644 --- a/src/components/MarketingHome.astro +++ b/src/components/MarketingHome.astro @@ -1,6 +1,6 @@ --- import { getPostsInLocale } from '../lib/posts'; -import { blogPath, formatDate, pricingPath, postPath, type Locale } from '../lib/i18n'; +import { blogPath, formatDate, pricingPath, postPath, ui, type Locale } from '../lib/i18n'; import { homeCopy } from '../lib/home'; import { heroLineJoin } from '../lib/positioning'; import { termBySlug, termHref, termLabel } from '../lib/terms'; @@ -301,7 +301,7 @@ const coverSrc = (cover: unknown): string => ))} {clusterLinks.length > 0 && ( -
- + {copy.residencyColumns.map((column) => )} - + {copy.residencyRows.map((row) => ( @@ -85,11 +93,11 @@ const blogHref = `${blogPath(locale)}${blogSlug}`;
-

{copy.permissionTitle}

-

{copy.permissionTitle}

-

{copy.permissionLead}

+

{copy.permissionTitle}

+

{copy.permissionTitle}

+

{copy.permissionLead}

-
+
{copy.permissionLayers.map((layer) => (

{layer.title}

@@ -101,11 +109,11 @@ const blogHref = `${blogPath(locale)}${blogSlug}`;
-

{copy.deploymentTitle}

-

{copy.deploymentTitle}

-

{copy.deploymentLead}

+

{copy.deploymentTitle}

+

{copy.deploymentTitle}

+

{copy.deploymentLead}

-
+
{copy.deployments.map((deployment) => (

{deployment.title}

@@ -117,10 +125,10 @@ const blogHref = `${blogPath(locale)}${blogSlug}`;
-

{copy.faqTitle}

-

{copy.faqTitle}

+

{copy.faqTitle}

+

{copy.faqTitle}

-
+
{copy.faqs.map((faq) => (
{faq.question} @@ -131,12 +139,12 @@ const blogHref = `${blogPath(locale)}${blogSlug}`;
-

{copy.ctaKicker}

-

{copy.ctaTitle}

-

{copy.ctaCopy}

+

{copy.ctaKicker}

+

{copy.ctaTitle}

+

{copy.ctaCopy}

diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index ebc1ce7..8549422 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -1,14 +1,25 @@ --- +import FooterClusters from '../components/FooterClusters.astro'; import Header from '../components/Header.astro'; import Logo from '../components/Logo.astro'; import { marketingPagePath } from '../content-pages/registry'; import { glossaryIndexPath } from '../glossary/registry'; -import { htmlLang, pricingPath, privacyPath, rssPath, termsPath, ui, type Locale } from '../lib/i18n'; +import { htmlLang, OG_LOCALE, pricingPath, privacyPath, rssPath, termsPath, ui, type Locale } from '../lib/i18n'; +import { ORGANIZATION_ID, ORGANIZATION_SAME_AS } from '../lib/organization'; interface Props { title: string; description?: string; locale: Locale; + /** + * The language the page body is written in, when it is not `locale`: the + * fallback pages (marketing, glossary, pricing, legal) render the English + * copy at every locale URL. The header and footer stay in `locale`, so + * `` does too; `
` gets this language, which is what a + * screen reader pronounces the body in (WCAG 3.1.1 / 3.1.2) and what + * `og:locale` reports. Omit it when the body is in `locale`. + */ + contentLocale?: Locale; /** * `null` omits the canonical link and `og:url` altogether. Only the 404 * pages use it: they answer for every URL that does not exist, so there is @@ -17,6 +28,15 @@ interface Props { */ canonical?: string | null; image?: string; + /** + * `og:image:alt` and the image's intrinsic size. Only meaningful with + * `image`; the default share image carries its own. Width and height are + * emitted only when both are known — a guessed size is worse than none, + * since a consumer lays out the preview card from it before fetching. + */ + imageAlt?: string; + imageWidth?: number; + imageHeight?: number; type?: 'website' | 'article'; noindex?: boolean; publishedTime?: string; @@ -40,8 +60,12 @@ const { title, description, locale, + contentLocale = locale, canonical, image, + imageAlt, + imageWidth, + imageHeight, type = 'website', noindex = false, publishedTime, @@ -64,9 +88,21 @@ const canonicalUrl = canonical === null : site ? `${site}${Astro.url.pathname}` : Astro.url.href; -const imageUrl = image - ? absoluteUrl(image) - : absoluteUrl('/images/objectos-home-hero.png'); +// The default share image is the home hero; its alt text is the one the home +// and marketing pages give the same picture. +const DEFAULT_IMAGE = { + src: '/images/objectos-home-hero.png', + alt: 'ObjectOS product surface connecting business data, applications, and AI agents', + width: 1586, + height: 992, +}; +const imageUrl = absoluteUrl(image ?? DEFAULT_IMAGE.src); +const ogImageAlt = image ? (imageAlt ?? title) : DEFAULT_IMAGE.alt; +const ogImageSize = image + ? imageWidth && imageHeight + ? { width: imageWidth, height: imageHeight } + : null + : { width: DEFAULT_IMAGE.width, height: DEFAULT_IMAGE.height }; const hreflangs = Object.entries(alternates ?? {}).map(([l, href]) => ({ hreflang: htmlLang(l as Locale), href: absoluteUrl(href), @@ -77,10 +113,12 @@ const jsonLdItems = [ { '@context': 'https://schema.org', '@type': 'Organization', + '@id': ORGANIZATION_ID, name: 'ObjectOS', legalName: 'ObjectStack AI LLC', url: absoluteUrl('/en/'), logo: absoluteUrl('/logo.svg'), + sameAs: ORGANIZATION_SAME_AS, }, { '@context': 'https://schema.org', @@ -90,6 +128,7 @@ const jsonLdItems = [ inLanguage: htmlLang(locale), publisher: { '@type': 'Organization', + '@id': ORGANIZATION_ID, name: 'ObjectStack AI LLC', }, }, @@ -135,7 +174,10 @@ const jsonLdHtml = (value: Record) => {canonicalUrl && } - + + {ogImageSize && } + {ogImageSize && } + {publishedTime && } {modifiedTime && } {author && } @@ -143,16 +185,18 @@ const jsonLdHtml = (value: Record) => {description && } + {jsonLdItems.map((item) =>
{column}
{row.data}