From 7b4fd191a59883bb9fc49486185f5a3686869763 Mon Sep 17 00:00:00 2001 From: "objectstack-fleet[bot]" <332303061+objectstack-fleet[bot]@users.noreply.github.com> Date: Tue, 6 Oct 2026 10:22:27 +0000 Subject: [PATCH 1/5] docs: SEO titles for the 67 English pages whose title tag was two words or fewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a `seoTitle:` frontmatter line to every English page whose built `` read as one or two words plus the ` | ObjectOS` suffix, so the tab/result title carries the terms a reader searches for while the H1, sidebar and breadcrumb keep the short noun (#166's mechanism). - 67 pages: each gains exactly one line; no `title`, `description` or body line changes. - Every title leads with the page's own specific term, lifted from its description and headings, and renders at 52–60 characters including the suffix (measured on the built HTML). - The three duplicated titles (Approvals, Dashboards, Notifications — each used by a build/configure page and a use page) are now distinct. - English only. Locale siblings are translation artifacts that a non-translator commit may not modify (AGENTS.md, Translation workflow; check-translation-ownership.mjs); the translation pass carries the new key over, and the output report lists the 163 siblings now missing it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- content/docs/architecture.mdx | 1 + content/docs/build/agents.mdx | 1 + content/docs/build/ai-builder.mdx | 1 + content/docs/build/automation/approvals.mdx | 1 + content/docs/build/automation/index.mdx | 1 + content/docs/build/automation/workflows.mdx | 1 + content/docs/build/data/formulas.mdx | 1 + content/docs/build/data/index.mdx | 1 + content/docs/build/data/relationships.mdx | 1 + content/docs/build/data/validation-rules.mdx | 1 + content/docs/build/index.mdx | 1 + content/docs/build/interface/actions.mdx | 1 + content/docs/build/interface/apps.mdx | 1 + content/docs/build/interface/dashboards.mdx | 1 + content/docs/build/interface/forms.mdx | 1 + content/docs/build/interface/index.mdx | 1 + content/docs/build/interface/pages.mdx | 1 + content/docs/build/interface/views.mdx | 1 + content/docs/build/marketplace.mdx | 1 + content/docs/build/packages.mdx | 1 + content/docs/build/templates.mdx | 1 + content/docs/configure/ai.mdx | 1 + content/docs/configure/api-access.mdx | 1 + content/docs/configure/authentication.mdx | 1 + content/docs/configure/data-sources.mdx | 1 + content/docs/configure/email.mdx | 1 + content/docs/configure/index.mdx | 1 + content/docs/configure/localization.mdx | 1 + content/docs/configure/notifications.mdx | 1 + content/docs/configure/permissions/field-level-security.mdx | 1 + content/docs/configure/permissions/index.mdx | 1 + content/docs/configure/permissions/managing-access.mdx | 1 + content/docs/configure/permissions/permission-sets.mdx | 1 + content/docs/configure/permissions/positions.mdx | 1 + content/docs/configure/permissions/record-access.mdx | 1 + content/docs/configure/runtime.mdx | 1 + content/docs/configure/storage.mdx | 1 + content/docs/configure/system-settings.mdx | 1 + content/docs/configure/webhooks.mdx | 1 + content/docs/deploy/air-gapped.mdx | 1 + content/docs/deploy/docker.mdx | 1 + content/docs/deploy/index.mdx | 1 + content/docs/deploy/kubernetes.mdx | 1 + content/docs/index.mdx | 1 + content/docs/operate/audit-logs.mdx | 1 + content/docs/operate/index.mdx | 1 + content/docs/operate/observability.mdx | 1 + content/docs/operate/production.mdx | 1 + content/docs/operate/troubleshooting.mdx | 1 + content/docs/quickstart.mdx | 1 + content/docs/reference/cel.mdx | 1 + content/docs/reference/cli.mdx | 1 + content/docs/reference/environment-variables.mdx | 1 + content/docs/reference/field-types.mdx | 1 + content/docs/reference/objectql.mdx | 1 + content/docs/reference/rest-api.mdx | 1 + content/docs/reference/runtime-capabilities.mdx | 1 + content/docs/reference/skills-cli.mdx | 1 + content/docs/resources/faq.mdx | 1 + content/docs/resources/glossary.mdx | 1 + content/docs/resources/support.mdx | 1 + content/docs/use/approvals.mdx | 1 + content/docs/use/dashboards.mdx | 1 + content/docs/use/index.mdx | 1 + content/docs/use/notifications.mdx | 1 + content/docs/use/views.mdx | 1 + content/docs/why.mdx | 1 + 67 files changed, 67 insertions(+) diff --git a/content/docs/architecture.mdx b/content/docs/architecture.mdx index 4eae96f..375f039 100644 --- a/content/docs/architecture.mdx +++ b/content/docs/architecture.mdx @@ -1,5 +1,6 @@ --- title: Architecture +seoTitle: "Architecture: What Runs, Where Data Lives" description: What you're actually running — for the engineer evaluating whether to bring this in. --- diff --git a/content/docs/build/agents.mdx b/content/docs/build/agents.mdx index 767a88e..752157b 100644 --- a/content/docs/build/agents.mdx +++ b/content/docs/build/agents.mdx @@ -1,5 +1,6 @@ --- title: Agents +seoTitle: "AI Agents: Define Agents, Skills and Tools" description: End-user AI assistants — Agent → Skill → Tool — wired from your data and actions. --- diff --git a/content/docs/build/ai-builder.mdx b/content/docs/build/ai-builder.mdx index 64591aa..3320b0b 100644 --- a/content/docs/build/ai-builder.mdx +++ b/content/docs/build/ai-builder.mdx @@ -1,5 +1,6 @@ --- title: AI Builder +seoTitle: "AI Builder: Build Apps from Plain-Language Chat" description: The built-in chat that turns plain-language requirements into running metadata. --- diff --git a/content/docs/build/automation/approvals.mdx b/content/docs/build/automation/approvals.mdx index 964b9ce..7e95d65 100644 --- a/content/docs/build/automation/approvals.mdx +++ b/content/docs/build/automation/approvals.mdx @@ -1,5 +1,6 @@ --- title: Approvals +seoTitle: "Approval Flows: Route Records for Human Sign-off" description: Route records for human sign-off — without letting the automation quietly bypass row-level security. --- diff --git a/content/docs/build/automation/index.mdx b/content/docs/build/automation/index.mdx index 1a40d34..a252ea4 100644 --- a/content/docs/build/automation/index.mdx +++ b/content/docs/build/automation/index.mdx @@ -1,5 +1,6 @@ --- title: Automation +seoTitle: "Automation: Flows, Workflows and Approvals" description: Pick the right tool for the job — flows for steps, workflows for state, approvals for human sign-off. --- diff --git a/content/docs/build/automation/workflows.mdx b/content/docs/build/automation/workflows.mdx index d50cd67..434032d 100644 --- a/content/docs/build/automation/workflows.mdx +++ b/content/docs/build/automation/workflows.mdx @@ -1,5 +1,6 @@ --- title: Workflows +seoTitle: "Workflows: Record Lifecycle as a State Machine" description: Model a record's lifecycle as a state machine — valid states, guarded transitions, and nothing a flow can do better. --- diff --git a/content/docs/build/data/formulas.mdx b/content/docs/build/data/formulas.mdx index 8a0559d..dc32425 100644 --- a/content/docs/build/data/formulas.mdx +++ b/content/docs/build/data/formulas.mdx @@ -1,5 +1,6 @@ --- title: Formulas +seoTitle: "Formula Fields, Dynamic Defaults and CEL Logic" description: Computed fields, dynamic defaults, and conditional logic — one CEL expression language everywhere metadata needs to think. --- diff --git a/content/docs/build/data/index.mdx b/content/docs/build/data/index.mdx index 4d7a8db..d49b519 100644 --- a/content/docs/build/data/index.mdx +++ b/content/docs/build/data/index.mdx @@ -1,5 +1,6 @@ --- title: Data Model +seoTitle: "Data Model: Objects, Fields and Relationships" description: Objects, fields, relationships, validation, indexes — described to AI or written in TypeScript. --- diff --git a/content/docs/build/data/relationships.mdx b/content/docs/build/data/relationships.mdx index cc8dd4e..bd332b0 100644 --- a/content/docs/build/data/relationships.mdx +++ b/content/docs/build/data/relationships.mdx @@ -1,5 +1,6 @@ --- title: Relationships +seoTitle: "Relationships: Lookup, Master-Detail and Roll-ups" description: Connect objects with lookups and master-detail — cascade rules, filtered pickers, hierarchies, junction objects, and roll-ups. --- diff --git a/content/docs/build/data/validation-rules.mdx b/content/docs/build/data/validation-rules.mdx index a1e749f..3714098 100644 --- a/content/docs/build/data/validation-rules.mdx +++ b/content/docs/build/data/validation-rules.mdx @@ -1,5 +1,6 @@ --- title: Validation Rules +seoTitle: "Validation Rules: Required, Unique and CEL Rules" description: Required fields, unique constraints, and CEL-powered rules that stop bad data at the platform level — with error messages you control. --- diff --git a/content/docs/build/index.mdx b/content/docs/build/index.mdx index 3d84c73..94c73f8 100644 --- a/content/docs/build/index.mdx +++ b/content/docs/build/index.mdx @@ -1,5 +1,6 @@ --- title: Build +seoTitle: "Build Apps: AI Chat, Studio Forms, or Templates" description: How apps come to life in ObjectOS — by chatting with AI, by clicking through forms in Studio, or by forking a template. --- diff --git a/content/docs/build/interface/actions.mdx b/content/docs/build/interface/actions.mdx index f5dbf1f..a1dbd61 100644 --- a/content/docs/build/interface/actions.mdx +++ b/content/docs/build/interface/actions.mdx @@ -1,5 +1,6 @@ --- title: Actions +seoTitle: "Actions: REST Endpoints, Buttons and AI Tools" description: Named operations the platform exposes as REST endpoints, buttons, flow steps, and AI tools — from one declaration. --- diff --git a/content/docs/build/interface/apps.mdx b/content/docs/build/interface/apps.mdx index 6fc7a24..a5ee6ef 100644 --- a/content/docs/build/interface/apps.mdx +++ b/content/docs/build/interface/apps.mdx @@ -1,5 +1,6 @@ --- title: Apps +seoTitle: "Apps: Navigation, Branding and Audience Gating" description: Bundle objects, views, pages, and dashboards into a branded, navigable shell — and gate exactly who sees what. --- diff --git a/content/docs/build/interface/dashboards.mdx b/content/docs/build/interface/dashboards.mdx index b4a9a64..7aafc88 100644 --- a/content/docs/build/interface/dashboards.mdx +++ b/content/docs/build/interface/dashboards.mdx @@ -1,5 +1,6 @@ --- title: Dashboards +seoTitle: "Build Dashboards: Datasets, Widgets, Drill-down" description: Analytics pages built from chart widgets bound to named datasets — with global filters, auto-refresh, and drill-through to the underlying records. --- diff --git a/content/docs/build/interface/forms.mdx b/content/docs/build/interface/forms.mdx index bfa06b7..d5ad351 100644 --- a/content/docs/build/interface/forms.mdx +++ b/content/docs/build/interface/forms.mdx @@ -1,5 +1,6 @@ --- title: Forms +seoTitle: "Forms: Create vs Edit, Sections, After Submit" description: Derive create and edit forms from one flat field set, group fields into sections without drift, and control what happens after submit. --- diff --git a/content/docs/build/interface/index.mdx b/content/docs/build/interface/index.mdx index 20bc80f..f8f67ba 100644 --- a/content/docs/build/interface/index.mdx +++ b/content/docs/build/interface/index.mdx @@ -1,5 +1,6 @@ --- title: Interface +seoTitle: "Interface: Apps, Views, Forms, Dashboards, Pages" description: Apps, views, forms, dashboards, pages, and actions — every user-facing surface declared as metadata, rendered by ObjectOS. --- diff --git a/content/docs/build/interface/pages.mdx b/content/docs/build/interface/pages.mdx index d77da55..f4c0ae5 100644 --- a/content/docs/build/interface/pages.mdx +++ b/content/docs/build/interface/pages.mdx @@ -1,5 +1,6 @@ --- title: Pages +seoTitle: "Free-form Pages: Regions, Components, Doc Pages" description: Free-form layouts composed from regions, components, and local state — plus Markdown doc pages that ship inside your package. --- diff --git a/content/docs/build/interface/views.mdx b/content/docs/build/interface/views.mdx index 1ef60f8..617b1fa 100644 --- a/content/docs/build/interface/views.mdx +++ b/content/docs/build/interface/views.mdx @@ -1,5 +1,6 @@ --- title: Views +seoTitle: "Views: List, Kanban, Calendar, Gantt and Form" description: List, Form, Kanban, Calendar, Gantt and more — how every object surface is declared. --- diff --git a/content/docs/build/marketplace.mdx b/content/docs/build/marketplace.mdx index 5305d33..7589fbf 100644 --- a/content/docs/build/marketplace.mdx +++ b/content/docs/build/marketplace.mdx @@ -1,5 +1,6 @@ --- title: Marketplace +seoTitle: "Marketplace: Install and Publish Ready-made Apps" description: Install ready-made apps into a running ObjectOS without writing code. --- diff --git a/content/docs/build/packages.mdx b/content/docs/build/packages.mdx index 72cde73..8854caa 100644 --- a/content/docs/build/packages.mdx +++ b/content/docs/build/packages.mdx @@ -1,5 +1,6 @@ --- title: Packages +seoTitle: "Packages: Create, Version, Publish and Install" description: The unit of organization in ObjectOS — versioned, installable, shareable. --- diff --git a/content/docs/build/templates.mdx b/content/docs/build/templates.mdx index 879f1af..4b3b25d 100644 --- a/content/docs/build/templates.mdx +++ b/content/docs/build/templates.mdx @@ -1,5 +1,6 @@ --- title: Templates +seoTitle: "App Templates: Forkable Helpdesk, Contracts, Todo" description: Forkable starter packages — `todo`, `contracts`, `procurement`, `helpdesk`, and more. --- diff --git a/content/docs/configure/ai.mdx b/content/docs/configure/ai.mdx index 0e357ea..05471a4 100644 --- a/content/docs/configure/ai.mdx +++ b/content/docs/configure/ai.mdx @@ -1,5 +1,6 @@ --- title: AI Service +seoTitle: "AI Service: Model Provider, Embedders, RAG, MCP" description: Where ObjectOS's AI is configured — the in-product AI runtime's provider settings, the open embedder and knowledge packages, and the MCP server. --- diff --git a/content/docs/configure/api-access.mdx b/content/docs/configure/api-access.mdx index 1e2ae17..b39dd10 100644 --- a/content/docs/configure/api-access.mdx +++ b/content/docs/configure/api-access.mdx @@ -1,5 +1,6 @@ --- title: API Access +seoTitle: "API Access: REST APIs, API Keys and OAuth 2.1" description: Generated REST APIs, authentication, and API keys for integrations. --- diff --git a/content/docs/configure/authentication.mdx b/content/docs/configure/authentication.mdx index a49c659..0c65b5b 100644 --- a/content/docs/configure/authentication.mdx +++ b/content/docs/configure/authentication.mdx @@ -1,5 +1,6 @@ --- title: Authentication +seoTitle: "Authentication: OAuth, OIDC/SSO and Device Flow" description: Configure sign-in, sessions, OAuth, OIDC/SSO, and device flow. --- diff --git a/content/docs/configure/data-sources.mdx b/content/docs/configure/data-sources.mdx index 40c6d68..8d13aa6 100644 --- a/content/docs/configure/data-sources.mdx +++ b/content/docs/configure/data-sources.mdx @@ -1,5 +1,6 @@ --- title: Data Sources +seoTitle: "Data Sources: Bind Objects to External Databases" description: Connect ObjectOS to your existing business databases, route objects to them, and let AI query the data — natively. --- diff --git a/content/docs/configure/email.mdx b/content/docs/configure/email.mdx index 864b2f3..6369868 100644 --- a/content/docs/configure/email.mdx +++ b/content/docs/configure/email.mdx @@ -1,5 +1,6 @@ --- title: Email +seoTitle: "Transactional Email: Transports and Templates" description: Configure transactional email delivery providers and templates. --- diff --git a/content/docs/configure/index.mdx b/content/docs/configure/index.mdx index cc87002..fab03e3 100644 --- a/content/docs/configure/index.mdx +++ b/content/docs/configure/index.mdx @@ -1,5 +1,6 @@ --- title: Administration +seoTitle: "Administration: Users, Access and Settings" description: Where system administrators manage users, access, settings, and integrations — and which page answers which task. --- diff --git a/content/docs/configure/localization.mdx b/content/docs/configure/localization.mdx index 73941c3..b03877e 100644 --- a/content/docs/configure/localization.mdx +++ b/content/docs/configure/localization.mdx @@ -1,5 +1,6 @@ --- title: Localization +seoTitle: "Localization: Timezone, Language and Formats" description: Set the tenant-wide timezone, language, country, and date, time, and number formats that every rendered value inherits. --- diff --git a/content/docs/configure/notifications.mdx b/content/docs/configure/notifications.mdx index 98ac3f0..2200e01 100644 --- a/content/docs/configure/notifications.mdx +++ b/content/docs/configure/notifications.mdx @@ -1,5 +1,6 @@ --- title: Notifications +seoTitle: "Notification Routing, Preferences and Templates" description: Configure how ObjectOS routes events into user inboxes — the audience a producer emits to, per-user preferences, and render templates. --- diff --git a/content/docs/configure/permissions/field-level-security.mdx b/content/docs/configure/permissions/field-level-security.mdx index 75f8c13..921cb6f 100644 --- a/content/docs/configure/permissions/field-level-security.mdx +++ b/content/docs/configure/permissions/field-level-security.mdx @@ -1,5 +1,6 @@ --- title: Field-Level Security +seoTitle: "Field-Level Security: Grants and API Enforcement" description: Hide or lock down individual fields — grant semantics, server-side enforcement, and how FLS behaves in forms, views, and the API. --- diff --git a/content/docs/configure/permissions/index.mdx b/content/docs/configure/permissions/index.mdx index 5d80a88..583fb80 100644 --- a/content/docs/configure/permissions/index.mdx +++ b/content/docs/configure/permissions/index.mdx @@ -1,5 +1,6 @@ --- title: Permissions +seoTitle: "Permission Model: Positions, Sets, Record Access" description: Identity, positions, permission sets, record access, and field security — the whole access model on one page. --- diff --git a/content/docs/configure/permissions/managing-access.mdx b/content/docs/configure/permissions/managing-access.mdx index cacbcc0..edd7775 100644 --- a/content/docs/configure/permissions/managing-access.mdx +++ b/content/docs/configure/permissions/managing-access.mdx @@ -1,5 +1,6 @@ --- title: Managing Access +seoTitle: "Managing Access: Onboard, Change Roles, Offboard" description: The daily admin playbook — onboard a new hire, change a role, verify what someone can see, and offboard cleanly. --- diff --git a/content/docs/configure/permissions/permission-sets.mdx b/content/docs/configure/permissions/permission-sets.mdx index a047a5d..efc5c7c 100644 --- a/content/docs/configure/permissions/permission-sets.mdx +++ b/content/docs/configure/permissions/permission-sets.mdx @@ -1,5 +1,6 @@ --- title: Permission Sets +seoTitle: "Permission Sets: Object, Field and System Grants" description: Grant application, object, field, and system permissions. --- diff --git a/content/docs/configure/permissions/positions.mdx b/content/docs/configure/permissions/positions.mdx index 3d921fa..b3202f6 100644 --- a/content/docs/configure/permissions/positions.mdx +++ b/content/docs/configure/permissions/positions.mdx @@ -1,5 +1,6 @@ --- title: Positions +seoTitle: "Positions: Job Functions and Audience Anchors" description: Model job functions and audience anchors with positions. --- diff --git a/content/docs/configure/permissions/record-access.mdx b/content/docs/configure/permissions/record-access.mdx index 54e0e57..6465f2d 100644 --- a/content/docs/configure/permissions/record-access.mdx +++ b/content/docs/configure/permissions/record-access.mdx @@ -1,5 +1,6 @@ --- title: Record Access +seoTitle: "Record Access: Sharing Model, Rules and Shares" description: Control which records a user can see or modify once object permissions allow it — sharing-model defaults, tenant isolation, sharing rules, record shares. --- diff --git a/content/docs/configure/runtime.mdx b/content/docs/configure/runtime.mdx index bcc7079..f3b32cf 100644 --- a/content/docs/configure/runtime.mdx +++ b/content/docs/configure/runtime.mdx @@ -1,5 +1,6 @@ --- title: Runtime Configuration +seoTitle: "Runtime Config: App Artifact, Database, Startup" description: Where the running app comes from, which database it uses, and the startup decisions a deployment has to declare. --- diff --git a/content/docs/configure/storage.mdx b/content/docs/configure/storage.mdx index a0b24d2..57b2ada 100644 --- a/content/docs/configure/storage.mdx +++ b/content/docs/configure/storage.mdx @@ -1,5 +1,6 @@ --- title: Storage +seoTitle: "File Storage: Local Disk, S3, R2, MinIO, Spaces" description: Where ObjectOS puts files — local disk, S3, R2, MinIO, Spaces. --- diff --git a/content/docs/configure/system-settings.mdx b/content/docs/configure/system-settings.mdx index e8e9f21..bdec98c 100644 --- a/content/docs/configure/system-settings.mdx +++ b/content/docs/configure/system-settings.mdx @@ -1,5 +1,6 @@ --- title: System Settings +seoTitle: "System Settings: Branding, Feature Flags, Secrets" description: Configure tenant and user settings through manifests and a shared K/V store. --- diff --git a/content/docs/configure/webhooks.mdx b/content/docs/configure/webhooks.mdx index 88a261f..f4c109a 100644 --- a/content/docs/configure/webhooks.mdx +++ b/content/docs/configure/webhooks.mdx @@ -1,5 +1,6 @@ --- title: Webhooks +seoTitle: "Webhooks: At-least-once Delivery, HMAC, Retries" description: Outbound webhooks from ObjectOS via a persistent outbox — at-least-once delivery, HMAC signing, bounded retries, and what a receiver must handle. --- diff --git a/content/docs/deploy/air-gapped.mdx b/content/docs/deploy/air-gapped.mdx index 9741f01..7cae3e7 100644 --- a/content/docs/deploy/air-gapped.mdx +++ b/content/docs/deploy/air-gapped.mdx @@ -1,5 +1,6 @@ --- title: Air-gapped Deployment +seoTitle: "Air-gapped Deployment: Licence Mode and Settings" description: Air-gap is a licence mode, not a firewall setting — the two settings a disconnected deployment must declare, and the combinations refused at startup. --- diff --git a/content/docs/deploy/docker.mdx b/content/docs/deploy/docker.mdx index 8e24454..dc34b67 100644 --- a/content/docs/deploy/docker.mdx +++ b/content/docs/deploy/docker.mdx @@ -1,5 +1,6 @@ --- title: Docker +seoTitle: "Docker: Run the Licensed Runtime Image by Digest" description: Run the licensed ObjectOS runtime image with Docker — digest-pinned, signature-verified, licensed, and scaled to multiple replicas. --- diff --git a/content/docs/deploy/index.mdx b/content/docs/deploy/index.mdx index 136e63e..db7fb84 100644 --- a/content/docs/deploy/index.mdx +++ b/content/docs/deploy/index.mdx @@ -1,5 +1,6 @@ --- title: Deployment +seoTitle: "Deployment: Self-Managed from the Licensed Image" description: Run ObjectOS Self-Managed from the licensed runtime image — digest-pinned, licensed on the happy path, with the supported combinations enforced at startup. --- diff --git a/content/docs/deploy/kubernetes.mdx b/content/docs/deploy/kubernetes.mdx index 4a3baab..793159c 100644 --- a/content/docs/deploy/kubernetes.mdx +++ b/content/docs/deploy/kubernetes.mdx @@ -1,5 +1,6 @@ --- title: Kubernetes +seoTitle: "Kubernetes: Digest Pinning, Migrations, Probes" description: What any orchestrator must preserve to run the licensed ObjectOS image — digest pinning, migration ordering, probes, and what multi-replica makes mandatory. --- diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 655d4d2..0a333f8 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -1,5 +1,6 @@ --- title: Introduction +seoTitle: "Documentation: Quickstart, Build, Deploy, Operate" description: "ObjectOS is the commercial runtime environment built on ObjectStack, hosted (ObjectOS Cloud) or self-managed (ObjectOS Enterprise). You own the ontology." --- diff --git a/content/docs/operate/audit-logs.mdx b/content/docs/operate/audit-logs.mdx index 59597a3..18f8fff 100644 --- a/content/docs/operate/audit-logs.mdx +++ b/content/docs/operate/audit-logs.mdx @@ -1,5 +1,6 @@ --- title: Audit Logs +seoTitle: "Audit Logs: Every Change, Sign-in and Event" description: Read the immutable audit trail and diagnostics logs that record every change, sign-in, and emitted event on your deployment. --- diff --git a/content/docs/operate/index.mdx b/content/docs/operate/index.mdx index 09145e2..27ef2f3 100644 --- a/content/docs/operate/index.mdx +++ b/content/docs/operate/index.mdx @@ -1,5 +1,6 @@ --- title: Operate +seoTitle: "Operate: Production Readiness, Backups, Upgrades" description: Keep a running ObjectOS deployment healthy — production readiness, observability, audit trails, backups, upgrades, and recovery. --- diff --git a/content/docs/operate/observability.mdx b/content/docs/operate/observability.mdx index 8aa9100..cd77596 100644 --- a/content/docs/operate/observability.mdx +++ b/content/docs/operate/observability.mdx @@ -1,5 +1,6 @@ --- title: Observability +seoTitle: "Observability: Logs, Request IDs, Metrics, Errors" description: Logs, request ids, metrics, errors, sessions, and audit logs. --- diff --git a/content/docs/operate/production.mdx b/content/docs/operate/production.mdx index e5d0066..afba0b0 100644 --- a/content/docs/operate/production.mdx +++ b/content/docs/operate/production.mdx @@ -1,5 +1,6 @@ --- title: Production Readiness +seoTitle: "Production Readiness: Hardening, Secrets, CORS" description: Checklist for running ObjectOS safely in production. --- diff --git a/content/docs/operate/troubleshooting.mdx b/content/docs/operate/troubleshooting.mdx index d890be1..6ad006b 100644 --- a/content/docs/operate/troubleshooting.mdx +++ b/content/docs/operate/troubleshooting.mdx @@ -1,5 +1,6 @@ --- title: Troubleshooting +seoTitle: "Troubleshooting: Startup, Login, Permissions" description: Diagnose startup, artifact, authentication, permission, and deployment issues. --- diff --git a/content/docs/quickstart.mdx b/content/docs/quickstart.mdx index a13ab55..b56b608 100644 --- a/content/docs/quickstart.mdx +++ b/content/docs/quickstart.mdx @@ -1,5 +1,6 @@ --- title: Quickstart +seoTitle: "Quickstart: Run ObjectStack Locally with os start" description: "From zero to a running app on the open-source ObjectStack runtime: one CLI, one command. ObjectOS Cloud needs none of it — sign in and build in the browser." --- diff --git a/content/docs/reference/cel.mdx b/content/docs/reference/cel.mdx index c0262c2..6915c77 100644 --- a/content/docs/reference/cel.mdx +++ b/content/docs/reference/cel.mdx @@ -1,5 +1,6 @@ --- title: CEL Expressions +seoTitle: "CEL Expressions: Formulas, Predicates, Templates" description: The expression language used for formulas, predicates, schedules, and templated strings — surfaced via five tagged templates. --- diff --git a/content/docs/reference/cli.mdx b/content/docs/reference/cli.mdx index 062fb1e..47ce21e 100644 --- a/content/docs/reference/cli.mdx +++ b/content/docs/reference/cli.mdx @@ -1,5 +1,6 @@ --- title: CLI Reference +seoTitle: "CLI Reference: Every os Command and Its Flags" description: Every `os` command, what it does, and the most useful flags. --- diff --git a/content/docs/reference/environment-variables.mdx b/content/docs/reference/environment-variables.mdx index 70f439a..7c6e50e 100644 --- a/content/docs/reference/environment-variables.mdx +++ b/content/docs/reference/environment-variables.mdx @@ -1,5 +1,6 @@ --- title: Environment Variables +seoTitle: "Environment Variables for Self-Hosted Deployments" description: The environment contract of a self-hosted ObjectOS deployment — what each variable decides, which combinations fail at startup, and which names are retired. --- diff --git a/content/docs/reference/field-types.mdx b/content/docs/reference/field-types.mdx index d6cb6cf..a013dfb 100644 --- a/content/docs/reference/field-types.mdx +++ b/content/docs/reference/field-types.mdx @@ -1,5 +1,6 @@ --- title: Field Types +seoTitle: "Field Types: All 48 Built-in Types and Options" description: Every field type you can declare on an object — what it stores, what options it accepts, how it surfaces in REST, the UI, and the AI Builder. --- diff --git a/content/docs/reference/objectql.mdx b/content/docs/reference/objectql.mdx index efc3d33..c4f6f90 100644 --- a/content/docs/reference/objectql.mdx +++ b/content/docs/reference/objectql.mdx @@ -1,5 +1,6 @@ --- title: ObjectQL +seoTitle: "ObjectQL: JSON Query Format, Filters and Joins" description: The structured query format used by /api/v1/data/*, views, reports, and AI tools. --- diff --git a/content/docs/reference/rest-api.mdx b/content/docs/reference/rest-api.mdx index 204346a..4897115 100644 --- a/content/docs/reference/rest-api.mdx +++ b/content/docs/reference/rest-api.mdx @@ -1,5 +1,6 @@ --- title: REST API +seoTitle: "REST API: Generated Endpoints, Auth, OpenAPI" description: The HTTP surface ObjectOS exposes — generated from your metadata, scoped by permissions, OpenAPI-described. --- diff --git a/content/docs/reference/runtime-capabilities.mdx b/content/docs/reference/runtime-capabilities.mdx index e6d4e8f..6e9ff1d 100644 --- a/content/docs/reference/runtime-capabilities.mdx +++ b/content/docs/reference/runtime-capabilities.mdx @@ -1,5 +1,6 @@ --- title: Runtime Capabilities +seoTitle: "Runtime Capabilities: Base and Optional Packages" description: Capabilities ObjectOS can load from ObjectStack framework packages. --- diff --git a/content/docs/reference/skills-cli.mdx b/content/docs/reference/skills-cli.mdx index 855b17f..426cf74 100644 --- a/content/docs/reference/skills-cli.mdx +++ b/content/docs/reference/skills-cli.mdx @@ -1,5 +1,6 @@ --- title: skills CLI +seoTitle: "skills CLI: Install Skills into Your Coding Agent" description: The npx command that installs ObjectOS skill bundles into your coding agent. --- diff --git a/content/docs/resources/faq.mdx b/content/docs/resources/faq.mdx index 9555af2..7869d56 100644 --- a/content/docs/resources/faq.mdx +++ b/content/docs/resources/faq.mdx @@ -1,5 +1,6 @@ --- title: FAQ +seoTitle: "FAQ: Setup, Multi-tenancy, Pricing and Licensing" description: Common questions about ObjectOS — getting started, databases and multi-tenancy, migrations, permissions, integrations, operations, pricing and licensing. --- diff --git a/content/docs/resources/glossary.mdx b/content/docs/resources/glossary.mdx index b4f612e..1e3ccee 100644 --- a/content/docs/resources/glossary.mdx +++ b/content/docs/resources/glossary.mdx @@ -1,5 +1,6 @@ --- title: Glossary +seoTitle: "Glossary: Platform Terms, One Definition Each" description: The vocabulary used across ObjectOS and ObjectStack — one definition each. --- diff --git a/content/docs/resources/support.mdx b/content/docs/resources/support.mdx index 97f0248..6838aaa 100644 --- a/content/docs/resources/support.mdx +++ b/content/docs/resources/support.mdx @@ -1,5 +1,6 @@ --- title: Support +seoTitle: "Support: Get Help, Report Bugs, Response Times" description: Where to get help, how to report bugs, response expectations. --- diff --git a/content/docs/use/approvals.mdx b/content/docs/use/approvals.mdx index f054e07..b56f554 100644 --- a/content/docs/use/approvals.mdx +++ b/content/docs/use/approvals.mdx @@ -1,5 +1,6 @@ --- title: Approvals +seoTitle: "Approval Inbox: Submit, Review and Track Requests" description: Submit records for sign-off, act on requests assigned to you, and always know where every approval stands. --- diff --git a/content/docs/use/dashboards.mdx b/content/docs/use/dashboards.mdx index 53e8b16..4b9e41c 100644 --- a/content/docs/use/dashboards.mdx +++ b/content/docs/use/dashboards.mdx @@ -1,5 +1,6 @@ --- title: Dashboards +seoTitle: "Reading Dashboards: KPIs, Charts, Date Filters" description: Read your team's KPIs, charts, and tables at a glance — and narrow them by date or filter in two clicks. --- diff --git a/content/docs/use/index.mdx b/content/docs/use/index.mdx index 4d41aef..4241adf 100644 --- a/content/docs/use/index.mdx +++ b/content/docs/use/index.mdx @@ -1,5 +1,6 @@ --- title: Using ObjectOS +seoTitle: "User Guide: Sign In, Apps, Search and Navigation" description: Sign in once and find every app, record, and notification your team shares — a five-minute tour of ObjectOS. --- diff --git a/content/docs/use/notifications.mdx b/content/docs/use/notifications.mdx index c64c146..d5a964e 100644 --- a/content/docs/use/notifications.mdx +++ b/content/docs/use/notifications.mdx @@ -1,5 +1,6 @@ --- title: Notifications +seoTitle: "Notification Inbox: Approvals, Digests, Muting" description: Catch everything routed to you — approvals, digests, and updates — without living in your inbox. --- diff --git a/content/docs/use/views.mdx b/content/docs/use/views.mdx index 19e8e7c..7628097 100644 --- a/content/docs/use/views.mdx +++ b/content/docs/use/views.mdx @@ -1,5 +1,6 @@ --- title: Using views +seoTitle: "Using Views: Grid, Board, Calendar and Timeline" description: See the same records as a grid, board, calendar, or timeline — and filter, group, and sort them your way. --- diff --git a/content/docs/why.mdx b/content/docs/why.mdx index 1f87e85..e7b213a 100644 --- a/content/docs/why.mdx +++ b/content/docs/why.mdx @@ -1,5 +1,6 @@ --- title: Why ObjectOS +seoTitle: "Why ObjectOS: When to Use It and When Not To" description: The honest pitch — when you should use it, when you shouldn't, and what makes it different. --- From b421fbd954d36ab12710233e8425b1fd2dc4159b Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 6 Oct 2026 11:09:08 +0000 Subject: [PATCH 2/5] docs: muted-text and code-comment contrast, phone table affordance, FAQ headings, title-weighted per-locale search, consistency pass - Light-mode muted foreground to hsl(0 0% 40%); code comments recoloured in both shiki themes. - Tables get an always-drawn scrollbar and a scroll-driven trailing fade. - FAQ and License FAQ questions become headings. - /api/search builds one locale's index on that locale's first search and weights title > heading > text; check-search-locales gains own-title-buried; smoke-docs asks /api/search in every locale with a nonce control. - Consistency: one data-residency table, one license-validation sentence, ObjectSchema.create, "license" spelling, Configure title, glossary order plus AI seat and Position, logo to the docs home, a translated llms.txt example, three unsourced configure/ai claims removed (with their locale siblings), release-following lines pointed at a populated feed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- .github/scripts/check-search-locales.mjs | 32 ++- .github/scripts/smoke-docs.mjs | 225 ++++++++++++++- apps/docs/app/api/search/route.ts | 139 ++++++++- apps/docs/app/global.css | 89 ++++++ apps/docs/app/llms.txt/route.ts | 30 +- apps/docs/lib/layout.shared.tsx | 26 +- apps/docs/mdx-components.tsx | 17 ++ apps/docs/source.config.ts | 25 +- content/docs/architecture.mdx | 22 +- content/docs/build/ai-builder.de.mdx | 187 ------------ content/docs/build/ai-builder.es.mdx | 187 ------------ content/docs/build/ai-builder.fr.mdx | 187 ------------ content/docs/build/ai-builder.ja.mdx | 190 ------------ content/docs/build/ai-builder.ko.mdx | 185 ------------ content/docs/build/ai-builder.mdx | 2 +- content/docs/build/ai-builder.zh-Hans.mdx | 187 ------------ content/docs/build/ai-builder.zh-Hant.mdx | 188 ------------ content/docs/build/automation/flows.de.mdx | 239 --------------- content/docs/build/automation/flows.es.mdx | 239 --------------- content/docs/build/automation/flows.fr.mdx | 239 --------------- content/docs/build/automation/flows.ja.mdx | 215 -------------- content/docs/build/automation/flows.ko.mdx | 235 --------------- content/docs/build/automation/flows.mdx | 1 - .../docs/build/automation/flows.zh-Hans.mdx | 271 ----------------- .../docs/build/automation/flows.zh-Hant.mdx | 272 ------------------ content/docs/configure/ai.mdx | 4 +- content/docs/configure/index.mdx | 4 +- .../configure/permissions/record-access.mdx | 2 +- content/docs/configure/runtime.mdx | 10 +- content/docs/deploy/air-gapped.mdx | 46 +-- content/docs/deploy/docker.mdx | 24 +- content/docs/deploy/index.mdx | 12 +- content/docs/deploy/kubernetes.mdx | 6 +- content/docs/index.mdx | 17 +- content/docs/operate/troubleshooting.mdx | 4 +- .../docs/reference/environment-variables.mdx | 14 +- content/docs/reference/rest-api.mdx | 15 +- content/docs/reference/security.mdx | 21 +- content/docs/resources/changelog.mdx | 6 +- content/docs/resources/faq.mdx | 181 +++++++----- content/docs/resources/glossary.mdx | 36 ++- content/docs/resources/license.mdx | 39 +-- content/docs/resources/support.mdx | 2 +- content/docs/why.mdx | 2 +- 44 files changed, 811 insertions(+), 3263 deletions(-) delete mode 100644 content/docs/build/ai-builder.de.mdx delete mode 100644 content/docs/build/ai-builder.es.mdx delete mode 100644 content/docs/build/ai-builder.fr.mdx delete mode 100644 content/docs/build/ai-builder.ja.mdx delete mode 100644 content/docs/build/ai-builder.ko.mdx delete mode 100644 content/docs/build/ai-builder.zh-Hans.mdx delete mode 100644 content/docs/build/ai-builder.zh-Hant.mdx delete mode 100644 content/docs/build/automation/flows.de.mdx delete mode 100644 content/docs/build/automation/flows.es.mdx delete mode 100644 content/docs/build/automation/flows.fr.mdx delete mode 100644 content/docs/build/automation/flows.ja.mdx delete mode 100644 content/docs/build/automation/flows.ko.mdx delete mode 100644 content/docs/build/automation/flows.zh-Hans.mdx delete mode 100644 content/docs/build/automation/flows.zh-Hant.mdx diff --git a/.github/scripts/check-search-locales.mjs b/.github/scripts/check-search-locales.mjs index 9f93f86..7441fa3 100644 --- a/.github/scripts/check-search-locales.mjs +++ b/.github/scripts/check-search-locales.mjs @@ -23,6 +23,13 @@ * fallback pages make `permissions` match in every locale, and Orama's * English tokenizer drops every CJK character, so a Chinese title would * find nothing. + * - `own-title-buried`: and found among the first three pages, not just + * somewhere in the list (#301). Before the route weighted titles, a page's + * title and another page's heading reading the same word scored the same, + * and 49 of the 79 English pages did not come first for their own title. + * Three and not one, because a few pages share a title (Approvals, + * Dashboards and Notifications each appear twice) and only one of two + * can be first. * - `negative-control-passed`: a query no page contains must find nothing. * It runs against the same handler in the same run. If it finds * something, `no-results` cannot fire, so the run fails. @@ -43,7 +50,9 @@ const ROUTE = 'apps/docs/.next/server/app/api/search/route.js'; const QUERY = 'permissions'; const NONCE = 'qzxvjwkq'; // in no page; prefix matching cannot reach a real word from it -const RULES = ['threw', 'status', 'not-array', 'no-results', 'own-title-missed', 'negative-control-passed']; +const TOP = 3; // `own-title-buried`: how far down its own title may rank a page + +const RULES = ['threw', 'status', 'not-array', 'no-results', 'own-title-missed', 'own-title-buried', 'negative-control-passed']; /** `languages` and `defaultLanguage` out of `i18n.ts`. Throws rather than guessing. */ function readI18n(text) { @@ -109,18 +118,25 @@ async function evaluate(GET, languages, pages) { if (probe.body.length === 0) add('no-results', locale, `?query=${QUERY} found nothing`); const own = pages.get(locale) ?? []; let found = 0; + let first = 0; for (const page of own) { const r = await ask(GET, locale, page.title); const broke = shape(r); - if (broke) add(broke[0], locale, `?query=${JSON.stringify(page.title)}: ${broke[1]}`); - else if (r.body.some((hit) => hit.type === 'page' && hit.url === page.url)) found += 1; - else add('own-title-missed', locale, `${page.file}: its title ${JSON.stringify(page.title)} does not find ${page.url} (${r.body.length} hits)`); + if (broke) { + add(broke[0], locale, `?query=${JSON.stringify(page.title)}: ${broke[1]}`); + continue; + } + const rank = r.body.filter((hit) => hit.type === 'page').findIndex((hit) => hit.url === page.url) + 1; + if (rank === 0) add('own-title-missed', locale, `${page.file}: its title ${JSON.stringify(page.title)} does not find ${page.url} (${r.body.length} hits)`); + else if (rank > TOP) add('own-title-buried', locale, `${page.file}: its title ${JSON.stringify(page.title)} ranks ${page.url} page ${rank}, below the first ${TOP}`); + if (rank > 0) found += 1; + if (rank === 1) first += 1; } const control = await ask(GET, locale, NONCE); if (!shape(control) && control.body.length > 0) { add('negative-control-passed', locale, `?query=${NONCE} matches no page yet found ${control.body.length} hits, so no-results cannot fire`); } - lines.push(` ${locale}: 200, ${probe.body.length} hits for "${QUERY}", ${found}/${own.length} own pages found by title`); + lines.push(` ${locale}: 200, ${probe.body.length} hits for "${QUERY}", ${found}/${own.length} own pages found by title, ${first} of them first`); } return { findings, lines }; } @@ -145,7 +161,7 @@ async function gate() { return 1; } console.log(lines.join('\n')); - console.log(`✓ search locales: all ${languages.length} locales answer 200, find "${QUERY}", find every own page by its title, and find nothing for a nonce`); + console.log(`✓ search locales: all ${languages.length} locales answer 200, find "${QUERY}", find every own page by its title within the first ${TOP} pages, and find nothing for a nonce`); return 0; } @@ -155,6 +171,7 @@ const PAGES = new Map([ ['en', [{ file: 'a.mdx', title: 'Alpha', url: '/docs/a' }]], ['ja', [{ file: 'a.ja.mdx', title: '権限', url: '/ja/docs/a' }]], ]); +const others = (n) => Array.from({ length: n }, (_, i) => ({ type: 'page', url: `/docs/other-${i}` })); const json = (body, status = 200) => Response.json(body, { status }); /** A handler that finds a page by its exact title, and finds `permissions` everywhere. */ const good = (override = () => undefined) => async (req) => { @@ -174,6 +191,9 @@ const CASES = [ ['nothing finds permissions', good((l, q) => (q === QUERY ? json([]) : undefined)), ['no-results', 'no-results']], ['ja titles find nothing, as an English tokenizer would', good((l, q) => (l === 'ja' && q !== QUERY ? json([]) : undefined)), ['own-title-missed']], ['a hit under a heading is not the page', good((l, q) => (l === 'en' && q === 'Alpha' ? json([{ type: 'heading', url: '/docs/a' }]) : undefined)), ['own-title-missed']], + ['a page third for its own title is found', good((l, q) => (l === 'en' && q === 'Alpha' ? json([...others(TOP - 1), { type: 'page', url: '/docs/a' }]) : undefined)), []], + ['a page fourth for its own title is buried', good((l, q) => (l === 'en' && q === 'Alpha' ? json([...others(TOP), { type: 'page', url: '/docs/a' }]) : undefined)), ['own-title-buried']], + ['headings above it do not bury a page', good((l, q) => (l === 'en' && q === 'Alpha' ? json([{ type: 'heading', url: '/docs/x#a' }, { type: 'heading', url: '/docs/x#b' }, { type: 'text', url: '/docs/x' }, { type: 'page', url: '/docs/a' }]) : undefined)), []], ['every query matches', good(() => json([{ type: 'page', url: '/docs/a' }, { type: 'page', url: '/ja/docs/a' }])), ['negative-control-passed', 'negative-control-passed']], ]; diff --git a/.github/scripts/smoke-docs.mjs b/.github/scripts/smoke-docs.mjs index dd9e57d..413e812 100644 --- a/.github/scripts/smoke-docs.mjs +++ b/.github/scripts/smoke-docs.mjs @@ -50,6 +50,22 @@ * it, and the runner asserts that the set of rules with a red fixture is the * whole set. Weaken a rule and the self-test exits 1. * + * ## Search is checked too, in every locale + * + * `/api/search` is dynamic, so no build output records whether it works, and + * on 2026-10-06 it answered 500 in four locales while every page rendered + * (#296). `check-search-locales.mjs` now asks the BUILT route under Node; this + * asks the deployed Worker (#301). One request per locale declared in + * `apps/docs/lib/i18n.ts` — read as text from this checkout, because the + * script is zero-dependency — for `permissions`, which must answer 200 with a + * non-empty JSON array. A query no page contains is the search's own negative + * control: it must come back empty, or a cache that ignores the query string + * (every query answered with the same list) would read as green. + * + * The locale list is the tree's, not the serving version's. A locale added on + * a commit whose deploy was rejected is not on the live site yet, and its + * search request reads `search-no-results` — which is what that site does. + * * ## Usage * * node .github/scripts/smoke-docs.mjs # default targets @@ -71,6 +87,9 @@ * gate on content drift wearing a smoke check's name. */ +import { readFileSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + /** The origin the checks run against unless `--base` says otherwise. */ const DEFAULT_BASE = 'https://docs.objectos.ai'; @@ -92,6 +111,10 @@ const RULES = [ 'lang-mismatch', 'final-path', 'negative-control-passed', + 'search-status', + 'search-not-json', + 'search-no-results', + 'search-negative-control-passed', ]; /** @@ -175,6 +198,25 @@ const TARGETS = [ */ const NEGATIVE_CONTROL_PATH = '/docs/objectos-smoke-negative-control-269'; +/** + * What the search requests ask. `permissions` finds pages in every locale, + * because an English fallback page carries it wherever a translation does not; + * the nonce is in no page, and prefix matching cannot reach a real word from + * it. `check-search-locales.mjs` asks the built route the same two. + */ +const SEARCH = { query: 'permissions', nonce: 'qzxvjwkq' }; + +/** `languages` out of `apps/docs/lib/i18n.ts`. Throws rather than guessing, so a smoke run never silently skips search. */ +function searchLocales(text) { + const list = /languages:\s*\[([^\]]+)\]/.exec(text)?.[1]; + const languages = list?.split(',').map((x) => x.trim().replace(/['"]/g, '')).filter(Boolean) ?? []; + if (!languages.length) throw new Error('could not read languages[] out of apps/docs/lib/i18n.ts'); + return languages; +} + +const searchPath = (locale, query) => + `/api/search?locale=${encodeURIComponent(locale)}&query=${encodeURIComponent(query)}`; + /* ------------------------------------------------------------- extraction -- */ const stripTags = (html) => html.replace(/<[^>]*>/g, ''); @@ -305,6 +347,43 @@ function evaluate(target, res) { return { findings, measured }; } +/** + * Judge one search response. `expectHits` is true for the real query and + * false for the nonce. Same response shape as `evaluate`. + */ +function evaluateSearch({ locale, query, expectHits }, res) { + const findings = []; + const label = `search ${locale} ${JSON.stringify(query)}`; + const add = (rule, detail) => findings.push({ rule, detail: `${label}: ${detail}` }); + if (res.error) { + add('fetch-failed', res.error); + return { findings, measured: { error: res.error } }; + } + let hits; + try { + hits = JSON.parse(res.body ?? ''); + } catch { + hits = undefined; + } + const measured = { + status: res.status, + contentType: res.contentType ?? null, + hits: Array.isArray(hits) ? hits.length : null, + }; + if (res.status !== 200) add('search-status', `HTTP ${res.status}, expected 200`); + if (!/^application\/json\b/i.test(measured.contentType ?? '') || !Array.isArray(hits)) { + add('search-not-json', `content-type ${measured.contentType ?? '(none)'}, body ${Array.isArray(hits) ? 'an array' : 'not a JSON array'}`); + } else if (expectHits && hits.length === 0) { + add('search-no-results', 'found nothing'); + } else if (!expectHits && hits.length > 0) { + add( + 'search-negative-control-passed', + `matches no page yet found ${hits.length} hits — this check cannot tell a working search from one that ignores the query`, + ); + } + return { findings, measured }; +} + /* --------------------------------------------------------------- fetching -- */ /** @@ -373,6 +452,7 @@ async function run(options) { base, targets, negativeControlPath, + search = null, attempts = 3, timeoutMs = 20000, fetchImpl = fetch, @@ -415,13 +495,44 @@ async function run(options) { console.log(''); } + // One request per locale, then the nonce once. The nonce is a control, so + // it gets one attempt, like the page control above. + const asks = search + ? [ + ...search.locales.map((locale) => ({ locale, query: search.query, expectHits: true })), + { locale: search.locales[0], query: search.nonce, expectHits: false }, + ] + : []; + for (const ask of asks) { + const started = Date.now(); + const res = await fetchTarget(base, searchPath(ask.locale, ask.query), { + attempts: ask.expectHits ? attempts : 1, + timeoutMs, + fetchImpl, + }); + const result = evaluateSearch(ask, res); + const m = result.measured; + const ok = result.findings.length === 0; + const mark = ask.expectHits ? (ok ? '✓' : '✗') : ok ? '✓ (control, expected empty)' : '✗ (control)'; + console.log( + `${mark} search ${ask.locale} ${JSON.stringify(ask.query)} ` + + (m.error ? `transport: ${m.error}` : `http ${m.status} ${m.contentType ?? '(no content-type)'} ${m.hits ?? '-'} hits ${Date.now() - started} ms`), + ); + for (const f of result.findings) { + console.error(` [${f.rule}] ${f.detail}`); + failures += 1; + } + } + if (asks.length) console.log(''); + if (failures) { console.error(`✗ smoke: ${failures} finding(s) against ${base}`); return 1; } console.log( `✓ smoke: ${targets.length} page(s) rendered against ${base}` + - (negativeControlPath ? ', negative control demonstrated red' : ''), + (negativeControlPath ? ', negative control demonstrated red' : '') + + (search ? `, search answered in ${search.locales.length} locale(s) and found nothing for a nonce` : ''), ); return 0; } @@ -523,6 +634,28 @@ const CASES = [ }, ]; +const JSON_RES = (body, over = {}) => ({ + status: 200, + url: 'https://docs.objectos.ai/api/search', + contentType: 'application/json', + body: JSON.stringify(body), + ...over, +}); +const HIT = { id: '/docs/a', type: 'page', url: '/docs/a', content: 'Permissions' }; +const ASK = { locale: 'ja', query: 'permissions', expectHits: true }; +const NONCE_ASK = { locale: 'en', query: 'qzxvjwkq', expectHits: false }; + +const SEARCH_CASES = [ + { name: 'a search with hits trips nothing', ask: ASK, res: JSON_RES([HIT]), expect: [] }, + { name: 'an empty nonce search trips nothing', ask: NONCE_ASK, res: JSON_RES([]), expect: [] }, + { name: 'search transport error', ask: ASK, res: { error: 'TypeError: fetch failed' }, expect: ['fetch-failed'] }, + { name: 'search answers 500 (#296)', ask: ASK, res: JSON_RES({}, { status: 500, contentType: 'text/plain', body: 'Internal Server Error' }), expect: ['search-status', 'search-not-json'] }, + { name: 'search answers an HTML page', ask: ASK, res: JSON_RES([], { contentType: 'text/html', body: '<html></html>' }), expect: ['search-not-json'] }, + { name: 'search answers an object', ask: ASK, res: JSON_RES({ hits: [HIT] }), expect: ['search-not-json'] }, + { name: 'search finds nothing', ask: ASK, res: JSON_RES([]), expect: ['search-no-results'] }, + { name: 'the nonce finds something', ask: NONCE_ASK, res: JSON_RES([HIT]), expect: ['search-negative-control-passed'] }, +]; + /** Whole-run cases, driven through `run()` with an injected fetch. */ async function runCases() { const results = []; @@ -559,9 +692,58 @@ async function runCases() { throw new TypeError('fetch failed'); }, }); + // A search that answers every query with the same list — a cache keyed + // without the query string — passes every per-locale request and is caught + // only by the nonce. + const pagesFine = async (url) => { + if (url.includes('/api/search')) { + return { + status: 200, + url, + headers: new Map([['content-type', 'application/json']]), + text: async () => JSON.stringify([HIT]), + }; + } + return alwaysGood(url); + }; + const codeSearch = await run({ + base: 'https://example.invalid', + targets: [BASE_TARGET], + negativeControlPath: null, + search: { ...SEARCH, locales: ['en', 'ja'] }, + attempts: 1, + fetchImpl: asResponse(pagesFine), + }); + // The same run with a search that does read the query passes, so the red + // above is the nonce's and nothing else's. + const searchWorks = async (url) => { + if (url.includes('/api/search')) { + const nonce = new URL(url).searchParams.get('query') === SEARCH.nonce; + return { + status: 200, + url, + headers: new Map([['content-type', 'application/json']]), + text: async () => JSON.stringify(nonce ? [] : [HIT]), + }; + } + return alwaysGood(url); + }; + const codeSearchWorks = await run({ + base: 'https://example.invalid', + targets: [BASE_TARGET], + negativeControlPath: null, + search: { ...SEARCH, locales: ['en', 'ja'] }, + attempts: 1, + fetchImpl: asResponse(searchWorks), + }); console.log = logs.log; console.error = logs.error; + results.push({ + name: 'a search that ignores the query fails the run; one that reads it passes', + ok: codeSearch === 1 && codeSearchWorks === 0, + rule: 'search-negative-control-passed', + }); results.push({ name: 'a negative control that renders fails the run', ok: code === 1, @@ -591,6 +773,19 @@ async function selfTest() { if (!ok) for (const f of findings) console.error(` [${f.rule}] ${f.detail}`); } + for (const c of SEARCH_CASES) { + const { findings } = evaluateSearch(c.ask, c.res); + const fired = [...new Set(findings.map((f) => f.rule))].sort(); + const want = [...c.expect].sort(); + const ok = fired.join(',') === want.join(','); + if (!ok) failed += 1; + console.log( + `${ok ? '✓' : '✗'} ${c.name.padEnd(38)} fired [${fired.join(' ') || '—'}]` + + (ok ? '' : ` expected [${want.join(' ') || '—'}]`), + ); + if (!ok) for (const f of findings) console.error(` [${f.rule}] ${f.detail}`); + } + console.log(''); const runResults = await runCases(); for (const r of runResults) { @@ -599,7 +794,11 @@ async function selfTest() { } console.log(''); - const covered = new Set([...CASES.flatMap((c) => c.expect), ...runResults.map((r) => r.rule)]); + const covered = new Set([ + ...CASES.flatMap((c) => c.expect), + ...SEARCH_CASES.flatMap((c) => c.expect), + ...runResults.map((r) => r.rule), + ]); for (const rule of RULES) { if (!covered.has(rule)) { console.error(`✗ rule "${rule}" has no fixture that trips it`); @@ -613,6 +812,22 @@ async function selfTest() { console.error('✗ no fixture asserts that a rendered page trips nothing'); failed += 1; } + if (!SEARCH_CASES.some((c) => c.expect.length === 0 && c.ask.expectHits)) { + console.error('✗ no fixture asserts that a working search trips nothing'); + failed += 1; + } + // The locale list is read off the checkout; an unreadable one must throw, not + // shrink the search check to nothing. + const read = searchLocales("defineI18n({ defaultLanguage: 'en', languages: ['en', 'zh-Hans'] })"); + let refused = false; + try { + searchLocales('defineI18n({})'); + } catch { + refused = true; + } + const i18nOk = read.join() === 'en,zh-Hans' && refused; + if (!i18nOk) failed += 1; + console.log(`${i18nOk ? '✓' : '✗'} i18n.ts locales are read, and an unreadable list is refused`); if (failed) { console.error(`\n✗ self-test: ${failed} case(s) did not behave as declared`); @@ -620,8 +835,8 @@ async function selfTest() { return; } console.log( - `✓ self-test: ${CASES.length} response case(s) and ${runResults.length} run case(s) — ` + - `all ${RULES.length} rules demonstrated able to fail`, + `✓ self-test: ${CASES.length} page case(s), ${SEARCH_CASES.length} search case(s) and ` + + `${runResults.length} run case(s) — all ${RULES.length} rules demonstrated able to fail`, ); } @@ -663,10 +878,12 @@ async function main() { return; } + const i18nPath = fileURLToPath(new URL('../../apps/docs/lib/i18n.ts', import.meta.url)); process.exitCode = await run({ base: opts.base.replace(/\/+$/, ''), targets, negativeControlPath: opts.control, + search: { ...SEARCH, locales: searchLocales(readFileSync(i18nPath, 'utf8')) }, }); } diff --git a/apps/docs/app/api/search/route.ts b/apps/docs/app/api/search/route.ts index fc6bc92..59332c5 100644 --- a/apps/docs/app/api/search/route.ts +++ b/apps/docs/app/api/search/route.ts @@ -1,13 +1,22 @@ import { source } from '@/lib/source'; -import type { i18n } from '@/lib/i18n'; -import { createFromSource } from 'fumadocs-core/search/server'; +import { i18n } from '@/lib/i18n'; +import { + createSearchAPI, + type AdvancedIndex, + type AdvancedOptions, + type SearchAPI, +} from 'fumadocs-core/search/server'; +import { findPath } from 'fumadocs-core/page-tree'; type Locale = (typeof i18n.languages)[number]; +/** One hit as Orama hands it to a custom `sortBy`: id, BM25 score, document. */ +type Hit = [id: unknown, score: number, document: { type: string }]; + /** * A word tokenizer for Chinese, Japanese and Korean, built on `Intl.Segmenter`. * - * Orama, the index behind `createFromSource`, ships tokenizers for about thirty + * Orama, the index behind fumadocs search, ships tokenizers for about thirty * languages, and Chinese, Japanese and Korean are not among them. A locale it * does not know fails index creation with `LANGUAGE_NOT_SUPPORTED`, which made * `/api/search` answer 500 for `zh-Hans`, `zh-Hant`, `ja` and `ko` (#296). Its @@ -19,12 +28,11 @@ type Locale = (typeof i18n.languages)[number]; * built into V8, so it costs no dependency, and it is present in both places * this route runs: Node (`next start`) and workerd (the Cloudflare Worker). * It is created on first use, not at module load, so Worker startup does not - * pay for it. fumadocs builds every locale's index on the first search in any - * locale, so that first search does. + * pay for it. * * Tokens are lowercased and de-duplicated, as Orama's own tokenizer does. */ -function segmented(locale: Locale) { +function segmented(locale: Locale): Partial<AdvancedOptions> { let segmenter: Intl.Segmenter | undefined; return { tokenizer: { @@ -48,6 +56,36 @@ function segmented(locale: Locale) { }; } +/** + * How much a hit counts for, by what it matched (#301). + * + * fumadocs indexes every page as several documents in one `content` field: the + * page's title (`type: 'page'`), each heading, and each paragraph. Orama ranks + * them all on that one field by BM25, so a page's title and a heading + * elsewhere that reads the same scored the same, and the tie went to whichever + * page was indexed first — an order fumadocs-mdx does not keep stable between + * builds, so two builds of one tree ranked such ties differently. Measured on + * `main` @ `601bb37`: "permissions" ranked `/docs/configure/permissions` + * outside the top eight pages, "air-gapped" ranked Marketplace and Packages + * above the Air-gapped page, and only 30 of the 79 English pages came first + * for their own title. + * With these weights 76 do; the other three share their title with a second + * page (Approvals, Dashboards, Notifications) and come second to it. + * + * A per-property `boost` cannot tell the three apart, because they are one + * property, so the weight is applied to each hit's score in a custom `sortBy`. + * Results are grouped by page in score order, so a page whose title matches + * comes first, and a heading match outranks the same word in passing. + */ +const WEIGHT: Record<string, number> = { page: 4, heading: 2, text: 1 }; + +function byWeightedScore(a: Hit, b: Hit): number { + const weighted = ([, score, doc]: Hit) => score * (WEIGHT[doc.type] ?? 1); + // Orama's own default order, with the weight applied: score descending, then + // insertion order. + return weighted(b) - weighted(a) || Number(a[0]) - Number(b[0]); +} + /** * One entry per locale in `lib/i18n.ts`. The `satisfies` makes a missing entry * a type error, because a locale without one falls back to a language Orama @@ -55,15 +93,90 @@ function segmented(locale: Locale) { * `.github/scripts/check-search-locales.mjs` asks the built route for every * locale after each build. */ -const localeMap = { - en: 'english', - de: 'german', - es: 'spanish', - fr: 'french', +const localeOptions = { + en: { language: 'english' }, + de: { language: 'german' }, + es: { language: 'spanish' }, + fr: { language: 'french' }, 'zh-Hans': segmented('zh-Hans'), 'zh-Hant': segmented('zh-Hant'), ja: segmented('ja'), ko: segmented('ko'), -} as const satisfies Record<Locale, unknown>; +} as const satisfies Record<Locale, Partial<AdvancedOptions>>; + +/** + * The breadcrumbs fumadocs' `createFromSource` gives a page — the tree's name, + * then each folder above the page — built the same way from the public + * page-tree API, so the search dialog shows what it showed before. + */ +function breadcrumbs(locale: Locale, url: string): string[] | undefined { + const tree = source.getPageTree(locale); + const path = findPath(tree.children, (node) => node.type === 'page' && node.url === url); + if (!path) return undefined; + path.pop(); + const names = [tree.name, ...path.map((node) => node.name)]; + return names.filter((name): name is string => typeof name === 'string' && name.length > 0); +} + +/** `locale`'s pages, indexed as `createFromSource` indexes them. */ +async function indexes(locale: Locale): Promise<AdvancedIndex[]> { + return Promise.all( + source.getPages(locale).map(async (page) => ({ + id: page.url, + url: page.url, + title: page.data.title, + description: page.data.description, + structuredData: (await page.data.load()).structuredData, + breadcrumbs: breadcrumbs(locale, page.url), + })), + ); +} -export const { GET } = createFromSource(source, { localeMap }); +/** + * One index per locale, built on that locale's first search (#301). + * + * `createFromSource` with i18n builds every locale's index on the first search + * in any locale, so the first reader after a cold start paid for all eight: + * loading the compiled body of every page in every locale (about 600) and + * segmenting four CJK corpora. #296 measured that first search at 7.2–7.4 s + * under Node and about 6.2 s under workerd, against 30–60 ms warm. Building + * only the requested locale makes the first search pay for one. + * + * Built in the request, not at build time, on purpose. fumadocs' build-time + * export (`staticGET`) is a client-side search: it ships every locale's index + * to the browser and moves the query out of this route, so `/api/search` + * would stop answering queries — the contract `check-search-locales.mjs` and + * the post-deploy smoke test both call — and the CJK tokenizer above would + * have to be rebuilt in the browser bundle. + */ +const servers = new Map<Locale, SearchAPI>(); + +function server(locale: Locale): SearchAPI { + let found = servers.get(locale); + if (!found) { + const options: Partial<AdvancedOptions> = localeOptions[locale]; + found = createSearchAPI('advanced', { + ...options, + search: { ...options.search, sortBy: byWeightedScore }, + indexes: () => indexes(locale), + }); + servers.set(locale, found); + } + return found; +} + +function isLocale(value: string): value is Locale { + return (i18n.languages as readonly string[]).includes(value); +} + +/** + * The same contract as fumadocs' i18n search endpoint: `?query=` and + * `?locale=`, the default language when no locale is given, and an empty + * list for an empty query or a locale this site does not have. + */ +export async function GET(request: Request): Promise<Response> { + const params = new URL(request.url).searchParams; + const locale = params.get('locale') ?? i18n.defaultLanguage; + if (!params.get('query') || !isLocale(locale)) return Response.json([]); + return server(locale).GET(request); +} diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index 50b3bc2..ee3e9aa 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -1,3 +1,92 @@ @import 'tailwindcss'; @import 'fumadocs-ui/css/neutral.css'; @import 'fumadocs-ui/css/preset.css'; + +/* + * Muted text, light mode (#301). + * + * The neutral theme's light `--color-fd-muted-foreground` is hsl(0 0% 45.1%), + * #737373. It colours the sidebar, the table of contents, the search button + * and the description under each page title, and it measured 4.12–4.35:1 + * against the backgrounds it sits on (#efefef to #f5f5f5), under the 4.5:1 + * WCAG AA floor for body text. 40% is #666666: 4.99:1 on the darkest of them, + * the search button, and 5.25:1 on the page. + * + * Declared in `@theme`, where the neutral theme declares it, so it replaces + * only the light value. The theme's dark value is set by a plain `.dark` rule, + * which wins over `@theme`, and it already measures 6.08:1. + */ +@theme { + --color-fd-muted-foreground: hsl(0, 0%, 40%); +} + +/* + * Wide tables on a phone (#301). + * + * fumadocs puts every table in a scroll container, so a wide table scrolls + * instead of breaking the page. What it does not give is a sign that it + * scrolls: a phone draws an overlay scrollbar only while a swipe is already + * under way, so on the License page at 390 px the Enterprise column is simply + * cut off at the edge, and a reader who never swipes never learns it exists. + * The same treatment as the marketing site's article tables (www #157): + * + * 1. A scrollbar that is always drawn. Styling `::-webkit-scrollbar` opts the + * container out of overlay scrollbars in Blink and WebKit. Do not add + * `scrollbar-width` or `scrollbar-color` for those engines: Chrome 121+ + * ignores `::-webkit-scrollbar` on any element that sets either, and the + * overlay scrollbar comes back. They are set only where + * `::-webkit-scrollbar` does not exist (Firefox). + * 2. A fade on the trailing edge while there is more table to the right, + * driven by the container's own scroll position, so it narrows over the + * last tenth of the scroll and is gone at the end. A table that does not + * overflow has no active scroll timeline, so the fade keeps its initial + * width of 0 and the table is not masked at all. + */ +.docs-table { + overscroll-behavior-x: contain; +} + +.docs-table::-webkit-scrollbar { + height: 6px; +} + +.docs-table::-webkit-scrollbar-track { + background: color-mix(in oklab, var(--color-fd-muted) 70%, transparent); + border-radius: 999px; +} + +.docs-table::-webkit-scrollbar-thumb { + background: color-mix(in oklab, var(--color-fd-foreground) 32%, transparent); + border-radius: 999px; +} + +@supports not selector(::-webkit-scrollbar) { + .docs-table { + scrollbar-width: thin; + scrollbar-color: color-mix(in oklab, var(--color-fd-foreground) 32%, transparent) transparent; + } +} + +@property --docs-table-fade { + syntax: '<length>'; + inherits: false; + initial-value: 0px; +} + +@supports (animation-timeline: scroll(self inline)) { + .docs-table { + mask-image: linear-gradient(to right, #000 calc(100% - var(--docs-table-fade)), transparent); + animation: docs-table-fade linear both; + animation-timeline: scroll(self inline); + } +} + +@keyframes docs-table-fade { + 0%, + 90% { + --docs-table-fade: 40px; + } + 100% { + --docs-table-fade: 0px; + } +} diff --git a/apps/docs/app/llms.txt/route.ts b/apps/docs/app/llms.txt/route.ts index 6286a92..3a30c83 100644 --- a/apps/docs/app/llms.txt/route.ts +++ b/apps/docs/app/llms.txt/route.ts @@ -1,8 +1,8 @@ -import type { Folder, Item, Node } from 'fumadocs-core/page-tree'; +import { flattenTree, type Folder, type Item, type Node, type Root } from 'fumadocs-core/page-tree'; import { llms } from 'fumadocs-core/source/llms'; import { i18n } from '@/lib/i18n'; import { POSITIONING } from '@/lib/positioning'; -import { SITE_URL, localeUrl } from '@/lib/seo'; +import { SITE_URL, localeUrl, translatedLocales } from '@/lib/seo'; import { SITE_NAME, source } from '@/lib/source'; export const revalidate = false; @@ -88,6 +88,26 @@ function absoluteNode(node: Node): Node { return node; } +/** + * The locale-prefixed URL the Other Languages section gives as its example: + * the first page, in navigation order, that has a real `lang` translation. + * + * It used to be a fixed `docs/quickstart`, and Quickstart has no `zh-Hans` + * source file, so the one example of a translated page was an English + * fallback (#301). Deriving it means a page losing its translation cannot + * leave the example pointing at English again. Navigation order rather than + * `source.getPages()` order, because the tree is what `meta.json` fixes. + */ +function translatedExample(tree: Root, lang: string): string | undefined { + for (const node of flattenTree(tree.children)) { + const page = source.getNodePage(node, LANG); + if (page && translatedLocales(page.slugs).includes(lang)) { + return localeUrl(lang, ['docs', ...page.slugs].join('/')); + } + } + return undefined; +} + /** * The header's two rules — append `.mdx`, and the locale prefixes announced * under Other Languages — are read by a machine that will compose them. So the @@ -144,12 +164,14 @@ export async function GET() { } if (OTHER_LOCALES.length > 0) { + const example = translatedExample(tree, OTHER_LOCALES[0]); lines.push( '', '## Other Languages', '', - `Every page above is also published under a locale prefix — for example ` + - `\`${localeUrl(OTHER_LOCALES[0], 'docs/quickstart')}\`. Available locales: ` + + `Every page above is also published under a locale prefix` + + (example ? ` — for example \`${example}\`` : '') + + `. Available locales: ` + `${OTHER_LOCALES.map((lang) => `\`${lang}\``).join(', ')}. English is the ` + `source of truth; a page with no translation yet falls back to English.`, ); diff --git a/apps/docs/lib/layout.shared.tsx b/apps/docs/lib/layout.shared.tsx index 6523170..5e895e7 100644 --- a/apps/docs/lib/layout.shared.tsx +++ b/apps/docs/lib/layout.shared.tsx @@ -1,5 +1,7 @@ import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared'; +import { Globe } from 'lucide-react'; import Image from 'next/image'; +import { i18n } from '@/lib/i18n'; export const gitConfig = { user: 'objectstack-ai', @@ -9,12 +11,20 @@ export const gitConfig = { const WEBSITE_URL = 'https://www.objectos.ai'; +/** + * The header shared by the docs and the legal pages. + * + * The logo goes to the docs home, in the reader's language (#301). It used to + * go to the marketing site, which on a docs page reads as "start over" and + * lands an English page under a reader who chose another language. The + * marketing site keeps a place in the header as its own link, next to GitHub, + * so it is still one click away. Its label is the host name, which needs no + * translation. + */ export function baseOptions(lang: string = 'en'): BaseLayoutProps { - void lang; - return { nav: { - url: WEBSITE_URL, + url: lang === i18n.defaultLanguage ? '/docs' : `/${lang}/docs`, title: ( <div className="flex items-center gap-2 font-bold"> <Image @@ -29,6 +39,16 @@ export function baseOptions(lang: string = 'en'): BaseLayoutProps { ), transparentMode: 'top', }, + links: [ + { + type: 'icon', + url: WEBSITE_URL, + text: 'www.objectos.ai', + label: 'www.objectos.ai', + icon: <Globe />, + external: true, + }, + ], githubUrl: `https://github.com/${gitConfig.user}/${gitConfig.repo}`, }; } diff --git a/apps/docs/mdx-components.tsx b/apps/docs/mdx-components.tsx index 20beb4c..d642d86 100644 --- a/apps/docs/mdx-components.tsx +++ b/apps/docs/mdx-components.tsx @@ -1,9 +1,26 @@ +import type { ComponentProps } from 'react'; import defaultMdxComponents from 'fumadocs-ui/mdx'; import type { MDXComponents } from 'mdx/types'; +/** + * fumadocs' own table wrapper — the same scroll container with the same + * classes — plus `docs-table`, which `app/global.css` hangs the always-drawn + * scrollbar and the trailing-edge fade on (#301). A class of our own, rather + * than a selector for fumadocs' utility classes, so that a fumadocs release + * that changes its markup cannot quietly detach the affordance. + */ +function Table(props: ComponentProps<'table'>) { + return ( + <div className="docs-table relative overflow-auto prose-no-margin my-6"> + <table {...props} /> + </div> + ); +} + export function getMDXComponents(components?: MDXComponents): MDXComponents { return { ...defaultMdxComponents, + table: Table, ...components, }; } diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index b57875b..c004283 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -1,5 +1,6 @@ import { defineConfig, defineDocs } from 'fumadocs-mdx/config'; import { metaSchema, pageSchema } from 'fumadocs-core/source/schema'; +import { rehypeCodeDefaultOptions } from 'fumadocs-core/mdx-plugins'; import path from 'node:path'; export const docs = defineDocs({ @@ -79,6 +80,28 @@ export const docs = defineDocs({ export default defineConfig({ mdxOptions: { - // MDX options + rehypeCodeOptions: { + /** + * Code comments, recoloured to clear 4.5:1 (#301). + * + * fumadocs' default code themes are `github-light` and `github-dark`, + * and both colour comments #6a737d — and nothing else: that is the only + * scope it is assigned to in either theme. On fumadocs' code-block + * background it measured 3.65:1 in dark mode (#191919) and 4.26:1 in + * light mode (#f1f1f1). The replacements are GitHub's own accessible + * muted greys: #8b949e (5.71:1 on #191919) and #57606a (5.65:1 on + * #f1f1f1). + * + * Scoped by theme name, so each theme's comment colour is replaced on + * its own. fumadocs' defaults are spread in first — the two themes, the + * notation transformers, the meta parser — so this adds one option and + * replaces nothing. + */ + ...rehypeCodeDefaultOptions, + colorReplacements: { + 'github-dark': { '#6a737d': '#8b949e' }, + 'github-light': { '#6a737d': '#57606a' }, + }, + }, }, }); diff --git a/content/docs/architecture.mdx b/content/docs/architecture.mdx index 375f039..8837f8c 100644 --- a/content/docs/architecture.mdx +++ b/content/docs/architecture.mdx @@ -66,21 +66,13 @@ On **ObjectOS Enterprise** — the self-managed edition this table describes — nothing below leaves your network. On **ObjectOS Cloud** the same process runs in our infrastructure instead, and on either edition the ontology itself is yours: export it and run it on the open-source ObjectStack -runtime. +runtime. [Data residency](/docs/reference/security#data-residency) lists +each class of data, where it lives, and whether it leaves your network. -| Data | Lives in | Leaves your network? | -|---|---|---| -| Business records | Your database | **No** | -| User accounts, sessions, OAuth tokens | Your database | **No** | -| Audit log | Your database | **No** | -| Settings, API keys, secrets | Your database / secret manager | **No** | -| Uploaded files | Your disk or your S3/R2 bucket | **No** | -| The compiled app definition (`objectstack.json`) | A file on disk or fetched from your control plane | Optional | - -Self-managed ObjectOS validates its licence online; Enterprise air-gapped -licences validate offline, so a deployment with no internet access at all -keeps running. See [Air-gapped](/docs/deploy/air-gapped) for the supported -licence and cloud-posture pairs. +Self-managed ObjectOS validates its license online; Enterprise air-gapped +licenses validate offline. A deployment with no internet access at all +therefore keeps running — see [Air-gapped](/docs/deploy/air-gapped) for the +supported license and cloud-posture pairs. ## How a request is served @@ -135,7 +127,7 @@ Whether the deployment also talks to a **control plane** is a separate decision, made by the cloud-posture variable — a connected deployment can be any of the modes above. A self-managed runtime authenticates to a control plane with a token minted when the deployment was **bound** to it, not with a key -pasted into a file, and pairing the wrong licence mode with the wrong cloud +pasted into a file, and pairing the wrong license mode with the wrong cloud posture is [refused at startup](/docs/deploy/air-gapped). ## Performance characteristics diff --git a/content/docs/build/ai-builder.de.mdx b/content/docs/build/ai-builder.de.mdx deleted file mode 100644 index c7f1cb8..0000000 --- a/content/docs/build/ai-builder.de.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: AI Builder -description: Der Chat in der Console, der Anforderungen in einfacher Sprache in laufende Metadaten verwandelt. -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -Der AI Builder ist die **primäre Methode, mit der Kunden ObjectOS erweitern**. Öffne -die Console, sprich mit dem Assistenten in einfacher Sprache, und er erstellt die -Metadaten für dich — Pakete, Objekte, Felder, Aktionen, Flows. Jede -Änderung wird in einer Human-in-the-Loop (HITL) Genehmigungsliste eingereiht, bevor sie -live geht. - -## Probiere es in 30 Sekunden - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -Du hast jetzt eine funktionierende Ticketing-App — REST-Endpunkte, Console-Ansichten, -Audit-Log-Einträge, Berechtigungskontrollen, alles inklusive. Es wurde keine Datei -bearbeitet; es fand kein Neustart statt. - -## Was die AI kann - -Der Assistent hat Zugriff auf eine Reihe **integrierter Metadaten-Tools**, die alle -mit Namespace versehen und gegen `@objectstack/spec` validiert sind: - -| Kategorie | Tool | Was es tut | -|---|---|---| -| **Pakete** | `create_package` | Neuer Container mit Manifest + Version | -| | `list_packages` | Vorhandene Pakete durchsuchen | -| | `get_package` / `get_active_package` | Inhalte inspizieren | -| | `set_active_package` | Das Arbeitspaket für die Konversation auswählen | -| **Objekte** | `create_object` | Neues Objekt mit anfänglichen Feldern | -| | `list_objects` | Objekte im aktiven Paket durchsuchen | -| | `describe_object` | Schema, Felder, Beziehungen ausgeben | -| **Felder** | `add_field` | Ein typisiertes Feld hinzufügen (gängige [Feldtypen](/docs/reference/field-types)) | -| | `modify_field` | Label, Picklist-Optionen, Validierung ändern | -| | `delete_field` | Ein Feld entfernen (mit Sicherheitsprüfung) | -| **Daten** | `query_data` | Datensätze über ObjectQL lesen | -| **Aktionen / Wissen** | `action_<name>`, `search_knowledge` | Aktionen aufrufen, RAG über Dokumente | - -Hinzu kommen Action-Tools, die aus jedem deklarierten `*.action.ts` materialisiert werden — -benannt `action_<name>` — sodass die AI eine Aktion, sobald sie im Paket existiert, -wie jedes integrierte Tool aufrufen kann. - -Den vollständigen Tool-Katalog zur Laufzeit ansehen: `GET /api/v1/ai/tools`. - -## Human-in-the-Loop Genehmigung - -Tool-Aufrufe, die **Metadaten verändern**, werden über eine Warteschlange für -ausstehende Aktionen geleitet. Genehmigende Operatoren sehen den vorgeschlagenen Änderungs-Diff, -bevor sie auf *Genehmigen* klicken. - -| Endpunkt | Zweck | -|---|---| -| `GET /api/v1/ai/pending-actions` | Eingereihte Aktionen auflisten (nach Status filtern) | -| `GET /api/v1/ai/pending-actions/:id` | Diff für eine einzelne vorgeschlagene Aktion | -| `POST /api/v1/ai/pending-actions/:id/approve` | Die Änderung anwenden | -| `POST /api/v1/ai/pending-actions/:id/reject` | Mit Begründung verwerfen | - -Erforderliche Berechtigungen: - -| Aktion | Berechtigung | -|---|---| -| Warteschlange lesen | `ai:read` | -| Genehmigen / ablehnen | `ai:approve` | - -Standard: Nur Mitglieder von **Setup Administrator** haben `ai:approve`. -Du kannst feiner unterteilen in [Permission Sets](/docs/configure/permissions/permission-sets) — z. B. -„Product Owner können in `com.acme.crm` genehmigen, sonst niemand.“ - -Die Warteschlange ist auditierbar: Jede Genehmigung/Ablehnung landet in -`sys_audit_log` mit der ID der ursprünglichen Konversation. - -## Konversationen und Kontext - -Jeder Chat ist ein `ai_conversations`-Datensatz. Die Plattform verfolgt: - -- **Aktives Paket** — gesetzt durch `set_active_package`. Neue Objekte / - Felder landen hier. -- **Skills** — der Ausschnitt der Tools, die für den aktuellen Kontext verfügbar sind - (z. B. innerhalb der `support_ticket`-Detailseite kann die AI - `support_ticket`-bezogene Skills aktiviert haben). -- **Wissen** — RAG-Themen + Indizes, die der Konversation angehängt sind. - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -Die Plattform ermittelt den Standard-Agent für die aktive Anwendung, -filtert die Skill-Menge nach Kontext und lässt das LLM auswählen, welches Tool -aufgerufen werden soll. Du verdrahtest Agents nicht vorab fest in bestimmte UIs — der Agent hängt -sich basierend auf den Metadaten selbst an. - -## Was du anfragen kannst - -Verifizierte Muster, mit Einzeiler-Prompts: - -| Ziel | Prompt | -|---|---| -| Neues Objekt | *„Erstelle ein `product`-Objekt mit name, sku, price, category.“* | -| Feld hinzufügen | *„Füge `order` ein `discount` Dezimalfeld hinzu, max 50.“* | -| Picklist ändern | *„Füge bei `task.status` 'blocked' zwischen 'open' und 'done' ein.“* | -| Index | *„Indiziere `order` nach status + created_at für das Dashboard.“* | -| Beziehung | *„Mache `order_line` zu Master-Detail von `order`.“* | -| Validierung | *„Bei `invoice` darf der Rabatt die Zwischensumme nicht überschreiten.“* | -| Flow | *„Wenn ein Ticket mit hoher Priorität 30 Minuten in 'new' verbleibt, benachrichtige den Manager.“* | -| Aktion | *„Erstelle eine Aktion `approve_invoice`, die den Status auf approved setzt.“* | -| Permission Set | *„Erstelle ein `agent` Permission Set mit read/create/edit auf support_ticket und read auf customer.“* | -| Übersetzung | *„Übersetze die support_ticket-Labels und Picklists ins Spanische.“* | - -Wenn die AI etwas nicht kann, sagt sie es — und verweist dich auf den manuellen Weg -(meist eine 30-zeilige `*.ts`-Datei oder ein Console-Formular). - -## Live-Vorschau - -Nach jeder genehmigten Änderung rendert die Console die betroffenen Ansichten an -Ort und Stelle neu. Kein Neuladen erforderlich. Der Kernel-Cache invalidiert das berührte -Paket; nachfolgende Anfragen verwenden die neuen Metadaten. - -## Zurückrollen - -Datensätze ausstehender Aktionen behalten den **Vorher-Zustand**. Wenn eine Änderung -schiefgeht, lehne alle laufenden Elemente ab und frage die AI: - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -Für größere Rollbacks verwende [`os diff`](/docs/reference/cli) gegen -dein letztes als funktionierend bekanntes Artefakt und wende den umgekehrten Diff an. - -## Einschränkungen (heute) - -- **Der Datenzugriff folgt den eigenen Berechtigungen des Operators.** Der - Assistent läuft als der angemeldete Benutzer, sodass er nur Datensätze lesen oder schreiben - kann, die dieser Benutzer bereits bearbeiten darf. Die meisten Teams halten den - Assistenten auf Metadaten fokussiert und lassen ihn Kundendaten abfragen — nicht - verändern. -- **Paketübergreifende Mutationen erfordern eine ausdrückliche Bestätigung.** Die AI zu - bitten, ein Systempaket (`sys_*`) zu modifizieren, gibt eine Ablehnungs- - meldung zurück — die Plattform verweigert dies aus Prinzip. -- **Langlaufende mehrstufige Pläne** (z. B. „eine komplette HR-App von - Grund auf bauen“) funktionieren, werden aber am besten als eine Reihe kleinerer - Konversationen durchgeführt, von denen jede ein oder zwei Objekte umsetzt. - -## Über die Build-Time-AI hinaus - -Dieselbe AI-Infrastruktur betreibt **Runtime-Agents** für deine Endbenutzer — -Support-Assistenten, Sales-Co-Piloten, interne Q&A-Bots — aufgebaut auf -derselben Agent → Skill → Tool Architektur. Siehe [Agents](/docs/build/agents). - -## Wie es weitergeht - -- [Agents](/docs/build/agents) — Endbenutzer-Assistenten auf Basis deiner Daten -- [Data Model](/docs/build/data) — was die AI tatsächlich generiert -- [Actions](/docs/build/interface/actions) — die Einheit, die die AI als Runtime-Tools bereitstellt -- [AI Service](/docs/configure/ai) — Provider-Konfiguration (OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/ai-builder.es.mdx b/content/docs/build/ai-builder.es.mdx deleted file mode 100644 index 3903fe9..0000000 --- a/content/docs/build/ai-builder.es.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: AI Builder -description: El chat dentro de Console que convierte requisitos en lenguaje natural en metadatos en ejecución. -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -El AI Builder es la **forma principal en que los clientes extienden ObjectOS**. Abre -Console, habla con el asistente en lenguaje natural y él construye los -metadatos por ti: paquetes, objetos, campos, acciones, flujos. Cada -cambio se encola en una lista de aprobación Human-in-the-Loop (HITL) antes de -ponerse en marcha. - -## Pruébalo en 30 segundos - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -Ahora tienes una aplicación de tickets en funcionamiento: endpoints REST, vistas en Console, -entradas de registro de auditoría, puntos de control de permisos, todo. No se editó ningún -archivo; no hubo ningún reinicio. - -## Qué puede hacer la IA - -El asistente tiene acceso a un conjunto de **herramientas de metadatos integradas**, todas -con espacio de nombres y validadas contra `@objectstack/spec`: - -| Categoría | Herramienta | Qué hace | -|---|---|---| -| **Paquetes** | `create_package` | Nuevo contenedor con manifiesto + versión | -| | `list_packages` | Explora los paquetes existentes | -| | `get_package` / `get_active_package` | Inspecciona el contenido | -| | `set_active_package` | Elige el paquete de trabajo para la conversación | -| **Objetos** | `create_object` | Nuevo objeto con campos iniciales | -| | `list_objects` | Explora los objetos del paquete activo | -| | `describe_object` | Imprime esquema, campos, relaciones | -| **Campos** | `add_field` | Añade un campo tipado (tipos de campo comunes [field types](/docs/reference/field-types)) | -| | `modify_field` | Cambia etiqueta, opciones de lista de selección, validación | -| | `delete_field` | Elimina un campo (con comprobación de seguridad) | -| **Datos** | `query_data` | Lee registros mediante ObjectQL | -| **Acciones / Conocimiento** | `action_<name>`, `search_knowledge` | Invoca acciones, RAG sobre la documentación | - -Además de herramientas de acción materializadas a partir de cada `*.action.ts` declarado, -nombradas `action_<name>`, de modo que una vez que una acción existe en el paquete, -la IA puede llamarla como cualquier herramienta integrada. - -Consulta el catálogo completo de herramientas en tiempo de ejecución: `GET /api/v1/ai/tools`. - -## Aprobación Human-in-the-Loop - -Las llamadas a herramientas que **modifican metadatos** se enrutan a través de una cola de -acciones pendientes. Los operadores que aprueban ven el diff del cambio propuesto antes de -hacer clic en *Approve*. - -| Endpoint | Propósito | -|---|---| -| `GET /api/v1/ai/pending-actions` | Lista las acciones en cola (filtra por estado) | -| `GET /api/v1/ai/pending-actions/:id` | Diff de una única acción propuesta | -| `POST /api/v1/ai/pending-actions/:id/approve` | Aplica el cambio | -| `POST /api/v1/ai/pending-actions/:id/reject` | Descarta con un motivo | - -Permisos necesarios: - -| Acción | Permiso | -|---|---| -| Leer la cola | `ai:read` | -| Aprobar / rechazar | `ai:approve` | - -Por defecto: solo los miembros de **Setup Administrator** tienen `ai:approve`. -Puedes descomponerlo con mayor granularidad en [Permission Sets](/docs/configure/permissions/permission-sets) — p. ej. -"los product owners pueden aprobar en `com.acme.crm`, nadie más". - -La cola es auditable: cada aprobación/rechazo queda registrado en -`sys_audit_log` con el id de la conversación de origen. - -## Conversaciones y contexto - -Cada chat es un registro `ai_conversations`. La plataforma rastrea: - -- **Paquete activo** — establecido por `set_active_package`. Los nuevos objetos / - campos van aquí. -- **Skills** — el subconjunto de herramientas disponibles para el contexto actual - (p. ej. dentro de la página de detalle de `support_ticket`, la IA puede tener - habilitadas skills con alcance de `support_ticket`). -- **Conocimiento** — temas e índices de RAG asociados a la conversación. - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -La plataforma resuelve el Agent por defecto para la aplicación activa, -filtra el conjunto de skills según el contexto y deja que el LLM elija qué herramienta -llamar. No conectas previamente los agentes a interfaces específicas: el agente se asocia -a sí mismo en función de los metadatos. - -## Qué puedes pedir - -Patrones verificados, con prompts de una sola línea: - -| Objetivo | Prompt | -|---|---| -| Nuevo objeto | *"Create a `product` object with name, sku, price, category."* | -| Añadir campo | *"Add a `discount` decimal field to `order`, max 50."* | -| Cambio de lista de selección | *"On `task.status`, add 'blocked' between 'open' and 'done'."* | -| Índice | *"Index `order` by status + created_at for the dashboard."* | -| Relación | *"Make `order_line` master-detail to `order`."* | -| Validación | *"On `invoice`, the discount can't exceed the subtotal."* | -| Flujo | *"When a high-priority ticket sits in 'new' for 30 minutes, notify the manager."* | -| Acción | *"Create an action `approve_invoice` that sets status to approved."* | -| Permission set | *"Create `agent` permission set with read/create/edit on support_ticket and read on customer."* | -| Traducción | *"Translate the support_ticket labels and picklists to Spanish."* | - -Si la IA no puede hacerlo, lo dice, y te indica el camino manual -(normalmente un archivo `*.ts` de 30 líneas o un formulario de Console). - -## Vista previa en vivo - -Tras cada cambio aprobado, Console vuelve a renderizar las vistas afectadas en -el lugar. No es necesario recargar. La caché del kernel se invalida para el paquete -modificado; las solicitudes posteriores usan los nuevos metadatos. - -## Revertir - -Los registros de acciones pendientes conservan el **estado anterior**. Si un cambio sale -mal, rechaza cualquier elemento en curso y pídeselo a la IA: - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -Para reversiones más grandes, usa [`os diff`](/docs/reference/cli) contra -tu último artefacto correcto conocido y aplica el diff inverso. - -## Limitaciones (por ahora) - -- **El acceso a los datos sigue los propios permisos del operador.** El - asistente se ejecuta como el usuario que ha iniciado sesión, por lo que solo puede leer o escribir - registros que ese usuario ya tiene permitido tocar. La mayoría de los equipos mantienen al - asistente centrado en los metadatos y le dejan consultar —no modificar— - los datos de los clientes. -- **Las mutaciones entre paquetes requieren confirmación explícita.** Pedirle - a la IA que modifique un paquete del sistema (`sys_*`) devuelve un mensaje de - rechazo: la plataforma se niega por principio. -- **Los planes largos de varios pasos** (p. ej. "construye una aplicación de RR. HH. completa desde - cero") funcionan, pero es mejor hacerlos como una serie de conversaciones más pequeñas, - cada una creando uno o dos objetos. - -## Más allá de la IA en tiempo de construcción - -La misma infraestructura de IA impulsa **agentes en tiempo de ejecución** para tus usuarios finales: -asistentes de soporte, copilotos de ventas, bots internos de preguntas y respuestas, construidos sobre -la misma arquitectura Agent → Skill → Tool. Consulta [Agents](/docs/build/agents). - -## Hacia dónde ir después - -- [Agents](/docs/build/agents) — asistentes para usuarios finales sobre tus datos -- [Data Model](/docs/build/data) — lo que la IA está generando realmente -- [Actions](/docs/build/interface/actions) — la unidad que la IA expone como herramientas en tiempo de ejecución -- [AI Service](/docs/configure/ai) — configuración del proveedor (OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/ai-builder.fr.mdx b/content/docs/build/ai-builder.fr.mdx deleted file mode 100644 index e60781b..0000000 --- a/content/docs/build/ai-builder.fr.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: AI Builder -description: Le chat intégré à la Console qui transforme des exigences exprimées en langage courant en métadonnées opérationnelles. -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -L'AI Builder est le **principal moyen pour les clients d'étendre ObjectOS**. Ouvrez -la Console, parlez à l'assistant en langage courant, et il construit les -métadonnées pour vous — packages, objets, champs, actions, flux. Chaque -modification est placée dans une file d'approbation Human-in-the-Loop (HITL) avant -sa mise en production. - -## Essayez-le en 30 secondes - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -Vous disposez désormais d'une application de gestion de tickets fonctionnelle — points de terminaison REST, vues Console, -entrées de journal d'audit, points de contrôle de permissions, tout y est. Aucun fichier n'a été -modifié ; aucun redémarrage n'a eu lieu. - -## Ce que l'IA peut faire - -L'assistant a accès à un ensemble d'**outils de métadonnées intégrés**, tous -regroupés dans un espace de noms et validés par rapport à `@objectstack/spec` : - -| Catégorie | Outil | Ce qu'il fait | -|---|---|---| -| **Packages** | `create_package` | Nouveau conteneur avec manifeste + version | -| | `list_packages` | Parcourir les packages existants | -| | `get_package` / `get_active_package` | Inspecter le contenu | -| | `set_active_package` | Choisir le package de travail pour la conversation | -| **Objects** | `create_object` | Nouvel objet avec champs initiaux | -| | `list_objects` | Parcourir les objets du package actif | -| | `describe_object` | Afficher le schéma, les champs, les relations | -| **Fields** | `add_field` | Ajouter un champ typé (les [types de champs](/docs/reference/field-types) courants) | -| | `modify_field` | Modifier le libellé, les options de liste de sélection, la validation | -| | `delete_field` | Supprimer un champ (avec contrôle de sécurité) | -| **Data** | `query_data` | Lire des enregistrements via ObjectQL | -| **Actions / Knowledge** | `action_<name>`, `search_knowledge` | Invoquer des actions, RAG sur la documentation | - -Auxquels s'ajoutent des outils d'action matérialisés à partir de chaque `*.action.ts` déclaré — -nommés `action_<name>` — de sorte qu'une fois qu'une action existe dans le package, -l'IA peut l'appeler comme n'importe quel outil intégré. - -Consultez le catalogue complet des outils à l'exécution : `GET /api/v1/ai/tools`. - -## Approbation Human-in-the-Loop - -Les appels d'outils qui **modifient les métadonnées** transitent par une file -d'attente d'actions en attente. Les opérateurs chargés de l'approbation voient le diff de la -modification proposée avant de cliquer sur *Approve*. - -| Point de terminaison | Objet | -|---|---| -| `GET /api/v1/ai/pending-actions` | Lister les actions en file d'attente (filtrer par statut) | -| `GET /api/v1/ai/pending-actions/:id` | Diff d'une seule action proposée | -| `POST /api/v1/ai/pending-actions/:id/approve` | Appliquer la modification | -| `POST /api/v1/ai/pending-actions/:id/reject` | Rejeter avec un motif | - -Permissions requises : - -| Action | Permission | -|---|---| -| Lire la file d'attente | `ai:read` | -| Approuver / rejeter | `ai:approve` | - -Par défaut : seuls les membres de **Setup Administrator** disposent de `ai:approve`. -Vous pouvez affiner ce découpage dans les [Permission Sets](/docs/configure/permissions/permission-sets) — par exemple -« les product owners peuvent approuver dans `com.acme.crm`, personne d'autre ». - -La file d'attente est auditable : chaque approbation/rejet est enregistré dans -`sys_audit_log` avec l'identifiant de la conversation d'origine. - -## Conversations et contexte - -Chaque chat correspond à un enregistrement `ai_conversations`. La plateforme suit : - -- **Package actif** — défini par `set_active_package`. Les nouveaux objets / - champs y sont créés. -- **Skills** — l'ensemble des outils disponibles pour le contexte courant - (par exemple, dans la page de détail de `support_ticket`, l'IA peut disposer de - skills limités à `support_ticket`). -- **Knowledge** — les sujets RAG + index rattachés à la conversation. - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -La plateforme résout l'Agent par défaut de l'application active, -filtre l'ensemble des skills selon le contexte, et laisse le LLM choisir quel outil -appeler. Vous ne câblez pas à l'avance les agents dans des interfaces spécifiques — l'agent se rattache -de lui-même en fonction des métadonnées. - -## Ce que vous pouvez demander - -Modèles vérifiés, avec des prompts en une ligne : - -| Objectif | Prompt | -|---|---| -| Nouvel objet | *"Create a `product` object with name, sku, price, category."* | -| Ajouter un champ | *"Add a `discount` decimal field to `order`, max 50."* | -| Modification de liste de sélection | *"On `task.status`, add 'blocked' between 'open' and 'done'."* | -| Index | *"Index `order` by status + created_at for the dashboard."* | -| Relation | *"Make `order_line` master-detail to `order`."* | -| Validation | *"On `invoice`, the discount can't exceed the subtotal."* | -| Flux | *"When a high-priority ticket sits in 'new' for 30 minutes, notify the manager."* | -| Action | *"Create an action `approve_invoice` that sets status to approved."* | -| Permission set | *"Create `agent` permission set with read/create/edit on support_ticket and read on customer."* | -| Traduction | *"Translate the support_ticket labels and picklists to Spanish."* | - -Si l'IA ne peut pas le faire, elle vous le dit — et vous oriente vers la voie manuelle -(généralement un fichier `*.ts` de 30 lignes ou un formulaire Console). - -## Aperçu en direct - -Après chaque modification approuvée, la Console restitue à nouveau les vues affectées sur -place. Aucun rechargement nécessaire. Le cache du noyau invalide le package touché ; -les requêtes suivantes utilisent les nouvelles métadonnées. - -## Annuler les modifications - -Les enregistrements d'actions en attente conservent l'**état antérieur**. Si une modification tourne -mal, rejetez les éléments en cours et demandez à l'IA : - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -Pour des annulations plus importantes, utilisez [`os diff`](/docs/reference/cli) par rapport à -votre dernier artefact connu comme fiable et appliquez le diff inverse. - -## Limitations (aujourd'hui) - -- **L'accès aux données suit les permissions propres de l'opérateur.** L'assistant - s'exécute en tant qu'utilisateur connecté, il ne peut donc lire ou écrire que les - enregistrements que cet utilisateur est déjà autorisé à manipuler. La plupart des équipes - maintiennent l'assistant centré sur les métadonnées et le laissent interroger — et non modifier — - les données client. -- **Les modifications inter-packages nécessitent une confirmation explicite.** Demander - à l'IA de modifier un package système (`sys_*`) renvoie un message de rejet — - la plateforme refuse par principe. -- **Les plans multi-étapes de longue durée** (par exemple « construire une application RH complète à partir - de zéro ») fonctionnent, mais il est préférable de les réaliser sous forme d'une série de - conversations plus courtes, chacune aboutissant à un ou deux objets. - -## Aller au-delà de l'IA au moment de la construction - -La même tuyauterie d'IA alimente les **agents d'exécution** destinés à vos utilisateurs finaux — -assistants de support, copilotes commerciaux, bots de questions-réponses internes — construits sur -la même architecture Agent → Skill → Tool. Voir [Agents](/docs/build/agents). - -## Pour aller plus loin - -- [Agents](/docs/build/agents) — des assistants pour utilisateurs finaux au-dessus de vos données -- [Data Model](/docs/build/data) — ce que l'IA génère réellement -- [Actions](/docs/build/interface/actions) — l'unité que l'IA expose en tant qu'outils d'exécution -- [AI Service](/docs/configure/ai) — configuration des fournisseurs (OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/ai-builder.ja.mdx b/content/docs/build/ai-builder.ja.mdx deleted file mode 100644 index 2ed4525..0000000 --- a/content/docs/build/ai-builder.ja.mdx +++ /dev/null @@ -1,190 +0,0 @@ ---- -title: AI Builder -description: 自然言語の要件を実行可能なメタデータに変換する Console 内チャット。 -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -AI Builder は、**顧客が ObjectOS を拡張するための主要な手段**です。Console -を開き、アシスタントに自然言語で話しかければ、アシスタントがメタデータ -(パッケージ、オブジェクト、フィールド、アクション、フロー)を構築して -くれます。すべての変更は、本番反映前に Human-in-the-Loop (HITL) の承認 -リストにキューイングされます。 - -## 30 秒で試す - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -これで動作するチケット管理アプリが完成します。REST エンドポイント、 -Console ビュー、監査ログのエントリ、権限チェックポイントまで一式そろって -います。ファイルは一切編集されず、再起動も発生していません。 - -## AI ができること - -アシスタントは、すべて名前空間化され `@objectstack/spec` に対して検証された -**組み込みメタデータツール**のセットにアクセスできます。 - -| カテゴリ | ツール | 機能 | -|---|---|---| -| **Packages** | `create_package` | マニフェスト + バージョン付きの新しいコンテナ | -| | `list_packages` | 既存のパッケージを閲覧 | -| | `get_package` / `get_active_package` | 内容を確認 | -| | `set_active_package` | 会話で作業対象とするパッケージを選択 | -| **Objects** | `create_object` | 初期フィールド付きの新しいオブジェクト | -| | `list_objects` | アクティブなパッケージ内のオブジェクトを閲覧 | -| | `describe_object` | スキーマ、フィールド、リレーションを出力 | -| **Fields** | `add_field` | 型付きフィールドを追加(一般的な[フィールドタイプ](/docs/reference/field-types)) | -| | `modify_field` | ラベル、選択リストのオプション、検証を変更 | -| | `delete_field` | フィールドを削除(安全チェック付き) | -| **Data** | `query_data` | ObjectQL でレコードを読み取り | -| **Actions / Knowledge** | `action_<name>`, `search_knowledge` | アクションの呼び出し、ドキュメントに対する RAG | - -さらに、宣言されたすべての `*.action.ts` から具体化されたアクション -ツール(`action_<name>` という名前)が利用できます。そのため、パッケージ -にアクションが存在すれば、AI は組み込みツールと同じように呼び出せます。 - -実行時にツールの完全なカタログを確認するには: `GET /api/v1/ai/tools`。 - -## Human-in-the-Loop の承認 - -**メタデータを変更する**ツール呼び出しは、保留中アクションのキューを経由 -します。承認担当のオペレーターは、*Approve* をクリックする前に、提案され -た変更の差分を確認できます。 - -| エンドポイント | 目的 | -|---|---| -| `GET /api/v1/ai/pending-actions` | キューイングされたアクションの一覧(ステータスでフィルタ) | -| `GET /api/v1/ai/pending-actions/:id` | 単一の提案アクションの差分 | -| `POST /api/v1/ai/pending-actions/:id/approve` | 変更を適用 | -| `POST /api/v1/ai/pending-actions/:id/reject` | 理由を付けて破棄 | - -必要な権限: - -| アクション | 権限 | -|---|---| -| キューの読み取り | `ai:read` | -| 承認 / 却下 | `ai:approve` | - -デフォルトでは、**Setup Administrator** のメンバーのみが `ai:approve` を -持ちます。[Permission Sets](/docs/configure/permissions/permission-sets) で -さらに細かく分割できます。たとえば「プロダクトオーナーは `com.acme.crm` -で承認できるが、それ以外は誰も承認できない」といった設定が可能です。 - -このキューは監査可能で、すべての承認 / 却下は、発生元の会話 ID とともに -`sys_audit_log` に記録されます。 - -## 会話とコンテキスト - -各チャットは `ai_conversations` レコードです。プラットフォームは次を追跡 -します。 - -- **アクティブなパッケージ** — `set_active_package` で設定します。新しい - オブジェクト / フィールドはここに配置されます。 -- **Skills** — 現在のコンテキストで利用可能なツールのスライス(たとえば - `support_ticket` の詳細ページ内では、AI が `support_ticket` スコープの - スキルを有効にできる場合があります)。 -- **Knowledge** — 会話にアタッチされた RAG トピック + インデックス。 - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -プラットフォームはアクティブなアプリケーションのデフォルト Agent を解決し、 -コンテキストでスキルセットをフィルタし、どのツールを呼び出すかを LLM に -選ばせます。エージェントを特定の UI に事前配線する必要はありません。 -エージェントはメタデータに基づいて自身をアタッチします。 - -## 依頼できること - -検証済みのパターンと、ワンライナーのプロンプト: - -| 目的 | プロンプト | -|---|---| -| 新しいオブジェクト | *"Create a `product` object with name, sku, price, category."* | -| フィールドの追加 | *"Add a `discount` decimal field to `order`, max 50."* | -| 選択リストの変更 | *"On `task.status`, add 'blocked' between 'open' and 'done'."* | -| インデックス | *"Index `order` by status + created_at for the dashboard."* | -| リレーション | *"Make `order_line` master-detail to `order`."* | -| 検証 | *"On `invoice`, the discount can't exceed the subtotal."* | -| フロー | *"When a high-priority ticket sits in 'new' for 30 minutes, notify the manager."* | -| アクション | *"Create an action `approve_invoice` that sets status to approved."* | -| 権限セット | *"Create `agent` permission set with read/create/edit on support_ticket and read on customer."* | -| 翻訳 | *"Translate the support_ticket labels and picklists to Spanish."* | - -AI が対応できない場合はそう伝え、手動の方法(通常は 30 行程度の `*.ts` -ファイルか Console フォーム)を案内します。 - -## ライブプレビュー - -承認された変更ごとに、Console は影響を受けるビューをその場で再レンダリング -します。リロードは不要です。カーネルキャッシュは変更されたパッケージを -無効化し、以降のリクエストは新しいメタデータを使用します。 - -## ロールバック - -保留中アクションのレコードは**変更前の状態**を保持します。変更がうまく -いかなかった場合は、処理中のアイテムを却下し、AI に依頼します。 - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -より大規模なロールバックには、最後に正常だったアーティファクトに対して -[`os diff`](/docs/reference/cli) を使用し、逆方向の差分を適用してください。 - -## 制限事項(現時点) - -- **データアクセスはオペレーター自身の権限に従います。** アシスタントは - サインインしたユーザーとして動作するため、そのユーザーがすでに操作を - 許可されているレコードのみを読み取り / 書き込みできます。多くのチームは、 - アシスタントをメタデータに集中させ、顧客データについては変更ではなく - クエリのみを行わせています。 -- **パッケージをまたぐ変更には明示的な確認が必要です。** システム - パッケージ(`sys_*`)の変更を AI に依頼すると、却下メッセージが返され - ます。プラットフォームは原則としてこれを拒否します。 -- **長時間にわたる複数ステップの計画**(たとえば「ゼロから完全な HR アプリ - を構築する」)も動作しますが、それぞれが 1〜2 個のオブジェクトを配置する - 一連の小さな会話に分けて行うのが最適です。 - -## ビルド時 AI のその先へ - -同じ AI の仕組みは、エンドユーザー向けの**ランタイムエージェント** -(サポートアシスタント、セールスコパイロット、社内 Q&A ボットなど)も -動かします。これらは同じ Agent → Skill → Tool アーキテクチャの上に構築 -されています。[Agents](/docs/build/agents) を参照してください。 - -## 次に読むべきページ - -- [Agents](/docs/build/agents) — あなたのデータの上に構築するエンドユーザー向けアシスタント -- [Data Model](/docs/build/data) — AI が実際に生成しているもの -- [Actions](/docs/build/interface/actions) — AI がランタイムツールとして公開する単位 -- [AI Service](/docs/configure/ai) — プロバイダー設定(OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/ai-builder.ko.mdx b/content/docs/build/ai-builder.ko.mdx deleted file mode 100644 index 2ceadff..0000000 --- a/content/docs/build/ai-builder.ko.mdx +++ /dev/null @@ -1,185 +0,0 @@ ---- -title: AI Builder -description: 일상 언어로 작성한 요구사항을 실행 가능한 메타데이터로 바꿔주는 Console 내장 채팅. -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -AI Builder는 **고객이 ObjectOS를 확장하는 기본 방법**입니다. Console을 -열고 어시스턴트에게 일상 언어로 말을 걸면, 패키지, 객체, 필드, 액션, -플로우 등 메타데이터를 대신 만들어 줍니다. 모든 변경은 실제로 적용되기 -전에 Human-in-the-Loop(HITL) 승인 목록에 대기열로 들어갑니다. - -## 30초 만에 사용해 보기 - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -이제 동작하는 티켓 관리 앱이 생겼습니다. REST 엔드포인트, Console 뷰, -감사 로그 항목, 권한 체크포인트까지 전부 갖춰져 있습니다. 파일을 편집하지도, -재시작하지도 않았습니다. - -## AI가 할 수 있는 일 - -어시스턴트는 모두 네임스페이스로 구분되고 `@objectstack/spec`에 대해 -검증되는 **내장 메타데이터 도구** 모음에 접근할 수 있습니다. - -| 분류 | 도구 | 하는 일 | -|---|---|---| -| **Packages** | `create_package` | 매니페스트 + 버전을 포함한 새 컨테이너 | -| | `list_packages` | 기존 패키지 둘러보기 | -| | `get_package` / `get_active_package` | 내용 확인 | -| | `set_active_package` | 대화에서 작업할 패키지 선택 | -| **Objects** | `create_object` | 초기 필드를 포함한 새 객체 | -| | `list_objects` | 활성 패키지의 객체 둘러보기 | -| | `describe_object` | 스키마, 필드, 관계 출력 | -| **Fields** | `add_field` | 타입이 지정된 필드 추가 (일반 [필드 타입](/docs/reference/field-types)) | -| | `modify_field` | 레이블, 선택 목록 옵션, 검증 변경 | -| | `delete_field` | 필드 제거 (안전 검사 포함) | -| **Data** | `query_data` | ObjectQL로 레코드 읽기 | -| **Actions / Knowledge** | `action_<name>`, `search_knowledge` | 액션 호출, 문서에 대한 RAG | - -여기에 더해, 선언된 모든 `*.action.ts`로부터 만들어진 액션 도구가 -`action_<name>` 형태로 제공됩니다. 따라서 패키지에 액션이 존재하기만 하면 -AI는 그것을 다른 내장 도구처럼 호출할 수 있습니다. - -런타임에서 전체 도구 카탈로그 확인: `GET /api/v1/ai/tools`. - -## Human-in-the-Loop 승인 - -**메타데이터를 변경하는** 도구 호출은 대기 중인 액션 대기열을 거칩니다. -승인 담당자는 *Approve*를 클릭하기 전에 제안된 변경 사항의 diff를 확인합니다. - -| 엔드포인트 | 목적 | -|---|---| -| `GET /api/v1/ai/pending-actions` | 대기 중인 액션 목록 (상태로 필터링) | -| `GET /api/v1/ai/pending-actions/:id` | 제안된 단일 액션의 diff | -| `POST /api/v1/ai/pending-actions/:id/approve` | 변경 적용 | -| `POST /api/v1/ai/pending-actions/:id/reject` | 사유와 함께 폐기 | - -필요한 권한: - -| 작업 | 권한 | -|---|---| -| 대기열 읽기 | `ai:read` | -| 승인 / 거부 | `ai:approve` | - -기본값으로는 **Setup Administrator** 멤버만 `ai:approve` 권한을 가집니다. -[Permission Sets](/docs/configure/permissions/permission-sets)에서 더 세분화할 수 -있습니다. 예를 들어 "프로덕트 오너는 `com.acme.crm`에서 승인할 수 있고, 그 외에는 -아무도 할 수 없다"와 같이 지정할 수 있습니다. - -이 대기열은 감사가 가능합니다. 모든 승인/거부는 발신 대화 id와 함께 -`sys_audit_log`에 기록됩니다. - -## 대화와 컨텍스트 - -각 채팅은 하나의 `ai_conversations` 레코드입니다. 플랫폼은 다음을 추적합니다. - -- **활성 패키지** — `set_active_package`로 설정됩니다. 새 객체 / 필드가 - 여기에 생성됩니다. -- **Skills** — 현재 컨텍스트에서 사용할 수 있는 도구의 부분 집합입니다 - (예: `support_ticket` 상세 페이지 내부에서는 AI가 - `support_ticket` 범위의 스킬을 사용할 수 있습니다). -- **Knowledge** — 대화에 연결된 RAG 주제 + 인덱스입니다. - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -플랫폼은 활성 애플리케이션의 기본 Agent를 확인하고, 컨텍스트에 따라 -스킬 집합을 필터링한 다음, LLM이 어떤 도구를 호출할지 선택하도록 합니다. -에이전트를 특정 UI에 미리 연결할 필요가 없습니다. 에이전트는 메타데이터를 -기반으로 스스로 연결됩니다. - -## 요청할 수 있는 것들 - -검증된 패턴과 한 줄 프롬프트입니다. - -| 목표 | 프롬프트 | -|---|---| -| 새 객체 | *"Create a `product` object with name, sku, price, category."* | -| 필드 추가 | *"Add a `discount` decimal field to `order`, max 50."* | -| 선택 목록 변경 | *"On `task.status`, add 'blocked' between 'open' and 'done'."* | -| 인덱스 | *"Index `order` by status + created_at for the dashboard."* | -| 관계 | *"Make `order_line` master-detail to `order`."* | -| 검증 | *"On `invoice`, the discount can't exceed the subtotal."* | -| 플로우 | *"When a high-priority ticket sits in 'new' for 30 minutes, notify the manager."* | -| 액션 | *"Create an action `approve_invoice` that sets status to approved."* | -| 권한 세트 | *"Create `agent` permission set with read/create/edit on support_ticket and read on customer."* | -| 번역 | *"Translate the support_ticket labels and picklists to Spanish."* | - -AI가 처리할 수 없는 경우에는 그렇다고 알려주고, 수동 경로(보통 30줄짜리 -`*.ts` 파일이나 Console 폼)를 안내해 줍니다. - -## 실시간 미리보기 - -승인된 변경이 적용될 때마다 Console은 영향을 받은 뷰를 그 자리에서 다시 -렌더링합니다. 새로고침이 필요 없습니다. 커널 캐시는 변경된 패키지를 -무효화하므로, 이후 요청은 새 메타데이터를 사용합니다. - -## 롤백 - -대기 중인 액션 레코드는 **변경 이전 상태**를 보관합니다. 변경이 잘못된 -경우, 진행 중인 항목을 거부하고 AI에게 요청하세요. - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -더 큰 규모의 롤백에는 마지막으로 정상 동작하던 아티팩트에 대해 -[`os diff`](/docs/reference/cli)를 사용하고 역방향 diff를 적용하세요. - -## 한계 (현재 기준) - -- **데이터 접근은 운영자 본인의 권한을 따릅니다.** 어시스턴트는 로그인한 - 사용자로 실행되므로, 해당 사용자가 이미 접근할 수 있는 레코드만 읽거나 - 쓸 수 있습니다. 대부분의 팀은 어시스턴트를 메타데이터에 집중시키고, - 고객 데이터에 대해서는 변경이 아니라 조회만 하도록 둡니다. -- **패키지 간 변경에는 명시적인 확인이 필요합니다.** AI에게 시스템 패키지 - (`sys_*`)를 수정하도록 요청하면 거부 메시지가 반환됩니다. 플랫폼은 - 원칙적으로 이를 거부합니다. -- **장시간 실행되는 다단계 계획**(예: "HR 앱을 처음부터 전부 구축")도 - 동작하지만, 한두 개의 객체씩 다루는 여러 개의 작은 대화로 나누어 - 진행하는 것이 가장 좋습니다. - -## 빌드 타임 AI를 넘어서 - -동일한 AI 인프라가 최종 사용자를 위한 **런타임 에이전트**(지원 -어시스턴트, 영업 코파일럿, 사내 Q&A 봇 등)를 구동하며, 이들은 모두 동일한 -Agent → Skill → Tool 아키텍처 위에 구축됩니다. [Agents](/docs/build/agents)를 참고하세요. - -## 다음으로 갈 곳 - -- [Agents](/docs/build/agents) — 데이터 위에 구축하는 최종 사용자용 어시스턴트 -- [Data Model](/docs/build/data) — AI가 실제로 생성하는 것 -- [Actions](/docs/build/interface/actions) — AI가 런타임 도구로 노출하는 단위 -- [AI Service](/docs/configure/ai) — 프로바이더 설정 (OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/ai-builder.mdx b/content/docs/build/ai-builder.mdx index 3320b0b..dcf08ac 100644 --- a/content/docs/build/ai-builder.mdx +++ b/content/docs/build/ai-builder.mdx @@ -181,4 +181,4 @@ the same Agent → Skill → Tool architecture. See [Agents](/docs/build/agents) - [Agents](/docs/build/agents) — end-user assistants on top of your data - [Data Model](/docs/build/data) — what the AI is actually generating - [Actions](/docs/build/interface/actions) — the unit the AI exposes as runtime tools -- [AI Service](/docs/configure/ai) — provider config (OpenAI / Anthropic / Doubao / …) +- [AI Service](/docs/configure/ai) — provider config (OpenAI / Anthropic / Google / DeepSeek / …) diff --git a/content/docs/build/ai-builder.zh-Hans.mdx b/content/docs/build/ai-builder.zh-Hans.mdx deleted file mode 100644 index cbadced..0000000 --- a/content/docs/build/ai-builder.zh-Hans.mdx +++ /dev/null @@ -1,187 +0,0 @@ ---- -title: AI Builder -description: Console 内置的对话功能,将自然语言需求转化为可运行的元数据。 -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -AI Builder 是**客户扩展 ObjectOS 的主要方式**。打开 -Console,用自然语言与助手对话,它就会为你构建 -元数据——包、对象、字段、动作、流程。每一次 -变更都会先进入人机协作(HITL)审批列表,然后才会 -正式生效。 - -## 30 秒快速体验 - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -现在你就拥有了一个可用的工单应用——REST 端点、Console 视图、 -审计日志条目、权限检查点,一应俱全。没有编辑任何 -文件,也没有发生重启。 - -## AI 能做什么 - -助手可以访问一组**内置的元数据工具**,它们都 -带有命名空间,并依据 `@objectstack/spec` 进行校验: - -| 类别 | 工具 | 功能 | -|---|---|---| -| **Packages** | `create_package` | 创建带有 manifest + 版本的新容器 | -| | `list_packages` | 浏览已有的包 | -| | `get_package` / `get_active_package` | 查看内容 | -| | `set_active_package` | 为当前对话选定工作包 | -| **Objects** | `create_object` | 创建带有初始字段的新对象 | -| | `list_objects` | 浏览当前活动包中的对象 | -| | `describe_object` | 输出 schema、字段、关系 | -| **Fields** | `add_field` | 添加一个有类型的字段(常见[字段类型](/docs/reference/field-types)) | -| | `modify_field` | 修改标签、选项列表选项、校验规则 | -| | `delete_field` | 删除字段(带安全检查) | -| **Data** | `query_data` | 通过 ObjectQL 读取记录 | -| **Actions / Knowledge** | `action_<name>`、`search_knowledge` | 调用动作,对文档进行 RAG 检索 | - -此外,每一个声明的 `*.action.ts` 都会物化为动作工具—— -命名为 `action_<name>`——因此一旦某个动作存在于包中, -AI 就可以像调用任何内置工具一样调用它。 - -在运行时查看完整的工具目录:`GET /api/v1/ai/tools`。 - -## 人机协作审批 - -**会变更元数据**的工具调用会路由进一个待处理动作 -队列。审批操作员在点击*批准*之前,会看到所提议 -变更的差异(diff)。 - -| 端点 | 用途 | -|---|---| -| `GET /api/v1/ai/pending-actions` | 列出排队中的动作(可按状态过滤) | -| `GET /api/v1/ai/pending-actions/:id` | 查看单个所提议动作的差异 | -| `POST /api/v1/ai/pending-actions/:id/approve` | 应用变更 | -| `POST /api/v1/ai/pending-actions/:id/reject` | 带原因丢弃 | - -所需权限: - -| 操作 | 权限 | -|---|---| -| 读取队列 | `ai:read` | -| 批准 / 拒绝 | `ai:approve` | - -默认情况下,只有 **Setup Administrator** 的成员才拥有 `ai:approve`。 -你可以在[权限集](/docs/configure/permissions/permission-sets)中进行更细粒度的拆分——例如 -“产品负责人可以在 `com.acme.crm` 中批准,其他人都不行。” - -该队列可审计:每一次批准/拒绝都会记录到 -`sys_audit_log`,并附带发起对话的 id。 - -## 对话与上下文 - -每一次聊天都是一条 `ai_conversations` 记录。平台会跟踪: - -- **活动包(Active package)**——由 `set_active_package` 设定。新建的对象/ - 字段会落到这里。 -- **技能(Skills)**——当前上下文中可用的工具子集 - (例如在 `support_ticket` 详情页内,AI 可能启用了 - `support_ticket` 范围的技能)。 -- **知识(Knowledge)**——附加到该对话的 RAG 主题 + 索引。 - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -平台会为当前活动应用解析出默认的 Agent, -根据上下文过滤技能集,然后让 LLM 选择要调用哪个工具。 -你无需把 agent 预先绑定到特定的 UI 上——agent 会 -根据元数据自动挂载。 - -## 你可以请求什么 - -经过验证的模式,附带一行提示词: - -| 目标 | 提示词 | -|---|---| -| 新对象 | *"Create a `product` object with name, sku, price, category."* | -| 添加字段 | *"Add a `discount` decimal field to `order`, max 50."* | -| 选项列表变更 | *"On `task.status`, add 'blocked' between 'open' and 'done'."* | -| 索引 | *"Index `order` by status + created_at for the dashboard."* | -| 关系 | *"Make `order_line` master-detail to `order`."* | -| 校验 | *"On `invoice`, the discount can't exceed the subtotal."* | -| 流程 | *"When a high-priority ticket sits in 'new' for 30 minutes, notify the manager."* | -| 动作 | *"Create an action `approve_invoice` that sets status to approved."* | -| 权限集 | *"Create `agent` permission set with read/create/edit on support_ticket and read on customer."* | -| 翻译 | *"Translate the support_ticket labels and picklists to Spanish."* | - -如果 AI 无法完成,它会明确告知——并为你指出 -手动操作的途径(通常是一个 30 行的 `*.ts` 文件,或一个 Console 表单)。 - -## 实时预览 - -每次变更获批后,Console 会就地重新渲染受影响的视图。 -无需重新加载。内核缓存会使被改动的包失效; -后续请求将使用新的元数据。 - -## 回滚 - -待处理动作记录会保留**变更前的状态**。如果某次变更出错, -拒绝所有在途的条目,然后请 AI: - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -对于规模更大的回滚,请使用 [`os diff`](/docs/reference/cli) -对比你上一个已知良好的产物,并应用反向 diff。 - -## 当前的限制 - -- **数据访问遵循操作员自身的权限。** - 助手以登录用户的身份运行,因此它只能读取或写入 - 该用户本就有权限操作的记录。大多数团队会让 - 助手专注于元数据,并让它查询——而非变更—— - 客户数据。 -- **跨包变更需要显式确认。** 请求 - AI 修改系统包(`sys_*`)会返回拒绝 - 消息——平台在原则上拒绝这类操作。 -- **长时间运行的多步骤计划**(例如“从零构建一个完整的 HR 应用”) - 是可行的,但最好拆分成一系列较小的 - 对话来完成,每次落地一两个对象。 - -## 超越构建期 AI - -同样的 AI 底层能力也为你的终端用户驱动**运行时 agent**—— -支持助手、销售副驾、内部问答机器人——它们构建于 -同一套 Agent → Skill → Tool 架构之上。参见 [Agents](/docs/build/agents)。 - -## 下一步去哪里 - -- [Agents](/docs/build/agents) —— 基于你的数据构建的终端用户助手 -- [Data Model](/docs/build/data) —— AI 实际生成的内容 -- [Actions](/docs/build/interface/actions) —— AI 作为运行时工具暴露的单元 -- [AI Service](/docs/configure/ai) —— 提供商配置(OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/ai-builder.zh-Hant.mdx b/content/docs/build/ai-builder.zh-Hant.mdx deleted file mode 100644 index f842a9d..0000000 --- a/content/docs/build/ai-builder.zh-Hant.mdx +++ /dev/null @@ -1,188 +0,0 @@ ---- -# @generated zh-Hant from zh-Hans -- do not edit. Edit the English source, then regenerate: pnpm --filter @objectos/docs gen:zh-hant -title: AI Builder -description: Console 內建的對話功能,將自然語言需求轉化為可執行的後設資料。 -translation: - source_sha: 8429ad8cc7490b200b2f2faa149acaae74ed9a9f9817ede4fa6a37351e7381f5 - guide_rev: 1 - mode: auto ---- - -AI Builder 是**客戶擴充套件 ObjectOS 的主要方式**。開啟 -Console,用自然語言與助手對話,它就會為你構建 -後設資料——包、物件、欄位、動作、流程。每一次 -變更都會先進入人機協作(HITL)審批列表,然後才會 -正式生效。 - -## 30 秒快速體驗 - -``` -You: I need to track customer support tickets. - Each ticket has a subject, description, priority (low/medium/high/urgent), - status (new/open/pending/resolved/closed), and assignee. - -AI: I'll create that for you. Here's the plan: - • Create package `com.you.support` (v0.1.0) - • Create object `support_ticket` with 5 fields - • Add a kanban view grouped by status - • Add 3 permission sets: agent, manager, viewer - - Approve? [Yes] [Modify] [Cancel] - -You: [Yes] - -AI: ✓ Package created - ✓ Object created with 5 fields and 12 indexes - ✓ Kanban view registered - ✓ Permission sets created - Done. Try it: /support_ticket -``` - -現在你就擁有了一個可用的工單應用——REST 端點、Console 檢視、 -審計日誌條目、許可權檢查點,一應俱全。沒有編輯任何 -檔案,也沒有發生重啟。 - -## AI 能做什麼 - -助手可以訪問一組**內建的後設資料工具**,它們都 -帶有名稱空間,並依據 `@objectstack/spec` 進行校驗: - -| 類別 | 工具 | 功能 | -|---|---|---| -| **Packages** | `create_package` | 建立帶有 manifest + 版本的新容器 | -| | `list_packages` | 瀏覽已有的包 | -| | `get_package` / `get_active_package` | 檢視內容 | -| | `set_active_package` | 為當前對話選定工作包 | -| **Objects** | `create_object` | 建立帶有初始欄位的新物件 | -| | `list_objects` | 瀏覽當前活動包中的物件 | -| | `describe_object` | 輸出 schema、欄位、關係 | -| **Fields** | `add_field` | 新增一個有型別的欄位(常見[欄位型別](/docs/reference/field-types)) | -| | `modify_field` | 修改標籤、選項列表選項、校驗規則 | -| | `delete_field` | 刪除欄位(帶安全檢查) | -| **Data** | `query_data` | 通過 ObjectQL 讀取記錄 | -| **Actions / Knowledge** | `action_<name>`、`search_knowledge` | 呼叫動作,對文件進行 RAG 檢索 | - -此外,每一個宣告的 `*.action.ts` 都會物化為動作工具—— -命名為 `action_<name>`——因此一旦某個動作存在於包中, -AI 就可以像呼叫任何內建工具一樣呼叫它。 - -在執行時檢視完整的工具目錄:`GET /api/v1/ai/tools`。 - -## 人機協作審批 - -**會變更後設資料**的工具呼叫會路由進一個待處理動作 -佇列。審批操作員在點選*批准*之前,會看到所提議 -變更的差異(diff)。 - -| 端點 | 用途 | -|---|---| -| `GET /api/v1/ai/pending-actions` | 列出排隊中的動作(可按狀態過濾) | -| `GET /api/v1/ai/pending-actions/:id` | 檢視單個所提議動作的差異 | -| `POST /api/v1/ai/pending-actions/:id/approve` | 應用變更 | -| `POST /api/v1/ai/pending-actions/:id/reject` | 帶原因丟棄 | - -所需許可權: - -| 操作 | 許可權 | -|---|---| -| 讀取佇列 | `ai:read` | -| 批准 / 拒絕 | `ai:approve` | - -預設情況下,只有 **Setup Administrator** 的成員才擁有 `ai:approve`。 -你可以在[許可權集](/docs/configure/permissions/permission-sets)中進行更細粒度的拆分——例如 -“產品負責人可以在 `com.acme.crm` 中批准,其他人都不行。” - -該佇列可審計:每一次批准/拒絕都會記錄到 -`sys_audit_log`,並附帶發起對話的 id。 - -## 對話與上下文 - -每一次聊天都是一條 `ai_conversations` 記錄。平臺會跟蹤: - -- **活動包(Active package)**——由 `set_active_package` 設定。新建的物件/ - 欄位會落到這裡。 -- **技能(Skills)**——當前上下文中可用的工具子集 - (例如在 `support_ticket` 詳情頁內,AI 可能啟用了 - `support_ticket` 範圍的技能)。 -- **知識(Knowledge)**——附加到該對話的 RAG 主題 + 索引。 - -```text -POST /api/v1/ai/assistant/chat -{ - "messages": [{ "role": "user", "content": "Add a tags field to support_ticket" }], - "context": { "objectName": "support_ticket", "recordId": "tkt_123" } -} -``` - -平臺會為當前活動應用解析出預設的 Agent, -根據上下文過濾技能集,然後讓 LLM 選擇要呼叫哪個工具。 -你無需把 agent 預先繫結到特定的 UI 上——agent 會 -根據後設資料自動掛載。 - -## 你可以請求什麼 - -經過驗證的模式,附帶一行提示詞: - -| 目標 | 提示詞 | -|---|---| -| 新物件 | *"Create a `product` object with name, sku, price, category."* | -| 新增欄位 | *"Add a `discount` decimal field to `order`, max 50."* | -| 選項列表變更 | *"On `task.status`, add 'blocked' between 'open' and 'done'."* | -| 索引 | *"Index `order` by status + created_at for the dashboard."* | -| 關係 | *"Make `order_line` master-detail to `order`."* | -| 校驗 | *"On `invoice`, the discount can't exceed the subtotal."* | -| 流程 | *"When a high-priority ticket sits in 'new' for 30 minutes, notify the manager."* | -| 動作 | *"Create an action `approve_invoice` that sets status to approved."* | -| 許可權集 | *"Create `agent` permission set with read/create/edit on support_ticket and read on customer."* | -| 翻譯 | *"Translate the support_ticket labels and picklists to Spanish."* | - -如果 AI 無法完成,它會明確告知——併為你指出 -手動操作的途徑(通常是一個 30 行的 `*.ts` 檔案,或一個 Console 表單)。 - -## 即時預覽 - -每次變更獲批後,Console 會就地重新渲染受影響的檢視。 -無需重新載入。核心快取會使被改動的包失效; -後續請求將使用新的後設資料。 - -## 回滾 - -待處理動作記錄會保留**變更前的狀態**。如果某次變更出錯, -拒絕所有在途的條目,然後請 AI: - -``` -You: Roll back the last 3 changes to support_ticket. -AI: Found 3 mutations from conversation conv_abc (3 minutes ago). - Restoring 'support_ticket' to its state at 13:42:11. - Approve? [Yes] -``` - -對於規模更大的回滾,請使用 [`os diff`](/docs/reference/cli) -對比你上一個已知良好的產物,並應用反向 diff。 - -## 當前的限制 - -- **資料訪問遵循操作員自身的許可權。** - 助手以登入使用者的身份執行,因此它只能讀取或寫入 - 該使用者本就有許可權操作的記錄。大多數團隊會讓 - 助手專注於後設資料,並讓它查詢——而非變更—— - 客戶資料。 -- **跨包變更需要顯式確認。** 請求 - AI 修改系統包(`sys_*`)會返回拒絕 - 訊息——平臺在原則上拒絕這類操作。 -- **長時間執行的多步驟計劃**(例如“從零構建一個完整的 HR 應用”) - 是可行的,但最好拆分成一系列較小的 - 對話來完成,每次落地一兩個物件。 - -## 超越構建期 AI - -同樣的 AI 底層能力也為你的終端使用者驅動**執行時 agent**—— -支援助手、銷售副駕、內部問答機器人——它們構建於 -同一套 Agent → Skill → Tool 架構之上。參見 [Agents](/docs/build/agents)。 - -## 下一步去哪裡 - -- [Agents](/docs/build/agents) —— 基於你的資料構建的終端使用者助手 -- [Data Model](/docs/build/data) —— AI 實際生成的內容 -- [Actions](/docs/build/interface/actions) —— AI 作為執行時工具暴露的單元 -- [AI Service](/docs/configure/ai) —— 提供商配置(OpenAI / Anthropic / Doubao / …) diff --git a/content/docs/build/automation/flows.de.mdx b/content/docs/build/automation/flows.de.mdx deleted file mode 100644 index ced4fb2..0000000 --- a/content/docs/build/automation/flows.de.mdx +++ /dev/null @@ -1,239 +0,0 @@ ---- -title: Flows & Automatisierung -description: Deklarative Geschäftslogik — der KI beschrieben oder in TypeScript geschrieben, die Runtime führt in beiden Fällen dasselbe Artefakt aus. -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -Flows sind die Art, wie Sie Geschäftslogik ausdrücken, ohne einen Server zu schreiben. -Jeder Flow ist deklarative Metadaten, die die Runtime ausführt — genau so -wie Objekte und Views. Das bedeutet, dass Flows in `os diff`, im -Audit-Log, im Flow-Builder der Console und im [AI Builder](/docs/build/ai-builder) -gleichzeitig erscheinen. - -Die meisten Kunden erstellen Flows, indem sie die KI fragen: - -> *„Wenn ein Ticket mit hoher Priorität 30 Minuten lang im Status ‚new‘ -> bleibt, benachrichtige den Manager auf Slack."* - -Die KI generiert den unten stehenden Flow. Diese Seite beschreibt die Struktur, damit Sie -ihn lesen und bearbeiten können. - -Aktivieren Sie die Funktion in Ihrem Stack: - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## Drei Flow-Typen - -| Typ | Ausgelöst durch | Verwendung für | -|---|---|---| -| **Autolaunched** | Eine Datensatzänderung (insert/update/delete) | „Willkommens-E-Mail senden, wenn sich ein Benutzer registriert" | -| **Scheduled** | Cron-Ausdruck oder Intervall | „Veraltete Aufgaben jede Nacht um 2 Uhr markieren" | -| **Manual** | Benutzer klickt einen Button in der Console oder einen API-Aufruf | „Rechnung genehmigen"-Aktionen | - -## Autolaunched: auf eine Datensatzänderung reagieren - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -Variableninterpolation: `{!trigger.record.<field>}`, `{!org.<field>}`, -`{!user.<field>}`, `{!step.<step-name>.output}`. Verwenden Sie CEL-Ausdrücke in -`condition:`-Blöcken. - -Trigger-Zeitpunkt: - -| `when` | Wird ausgelöst | -|---|---| -| `before_insert` | Innerhalb der Schreibtransaktion, vor INSERT | -| `after_insert` | Nach dem Commit | -| `before_update` | Innerhalb der Schreibtransaktion, vor UPDATE | -| `after_update` | Nach dem Commit | -| `before_delete` | Innerhalb der Schreibtransaktion, vor DELETE | -| `after_delete` | Nach dem Commit | - -`before_*`-Flows können den zu schreibenden Datensatz verändern (Felder berechnen, -Daten normalisieren). `after_*`-Flows laufen asynchron und können langsame externe -Dienste aufrufen. - -## Scheduled: nach der Uhr ausführen - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -Unterstützt durch die `@objectstack/service-job`-Funktion — siehe -[Runtime Capabilities](/docs/reference/runtime-capabilities). - -## Manual: Aktionen und Genehmigungen - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -Stellen Sie ihn in der Console als Button in der Invoice-View bereit oder rufen Sie ihn per -REST auf: - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## Step-Typen - -| Step | Zweck | -|---|---| -| `query` | Datensätze über ObjectQL lesen | -| `create` / `update` / `delete` | In Objekte schreiben | -| `action` | Eine integrierte oder über ein Plugin registrierte Aktion aufrufen (E-Mail, Webhook, KI-Aufruf, …) | -| `condition` | Anhand eines CEL-Ausdrucks verzweigen | -| `foreach` | Über eine Sammlung iterieren | -| `parallel` | Sub-Steps gleichzeitig ausführen | -| `wait` | Pausieren für eine Dauer / bis zu einem Zeitstempel / bis eine Bedingung erfüllt ist | -| `subflow` | Einen anderen Flow aufrufen | -| `approval` | Blockieren, bis ein Benutzer genehmigt (erfordert `@objectstack/plugin-approvals`) | - -## Bedingungen und Verzweigungen - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## Fehlerbehandlung - -Jeder Step akzeptiert: - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -Bei autolaunched `before_*`-Flows bricht `onError: 'fail'` (Standard) die -ursprüngliche Schreibtransaktion ab. Bei `after_*`-Flows ist der ursprüngliche -Schreibvorgang bereits committet; fehlgeschlagene Flow-Läufe landen in der Job-Retry- -Warteschlange. - -## Formeln und Ausdrücke (CEL) - -Bedingungen, dynamische Feldwerte und Filterausdrücke akzeptieren alle -**CEL** (Common Expression Language) — Googles Sprache für die sichere -Auswertung von Ausdrücken: - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -CEL ist sandboxed (keine Seiteneffekte, kein I/O), wird serverseitig ausgewertet und -ist im Flow-Builder auditierbar. - -## Visueller Builder - -Die Console bringt einen visuellen Flow-Builder mit, der sich verlustfrei mit den -deklarativen Metadaten austauscht — Nicht-Ingenieure können einen Flow bearbeiten, und er serialisiert -sich zurück in dieselbe Struktur wie das TypeScript, das Sie von Hand schreiben würden. - -## Flows testen - -```bash -os test --scenario "welcome email fires on signup" -``` - -## Grenzen & Best Practices - -- **Halten Sie Before-Hooks klein.** Sie blockieren die Schreibtransaktion. -- **Verwenden Sie `wait` statt langlaufender Steps.** Ein Flow, der schläft, - blockiert einen Worker; ein `wait until` gibt den Worker an den Pool zurück. -- **Verwenden Sie `parallel` für unabhängige Steps.** Die sequentielle Ausführung ist - der Standard. -- **Idempotenz ist wichtig.** Retries können denselben Step zweimal ausführen; - externe Seiteneffekte sollten dedupliziert werden (verwenden Sie die Flow-Run-ID als Schlüssel). -- **Audit-sensible Aktionen.** Flows, die Berechtigungen ändern oder - Datensätze löschen, sollten selbst in `sys_audit_log` protokollieren. - -## Wie es weitergeht - -- [Webhooks](/docs/configure/webhooks) — ausgehende Benachrichtigungen, oft - von Flows ausgelöst -- [Email](/docs/configure/email) — das Transportmittel der `send_email`-Aktion -- [AI Service](/docs/configure/ai) — `ai_call`-Aktion für LLM-Steps -- [API Access](/docs/configure/api-access) — manuelle Flows von - externen Systemen aufrufen -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) - — Quellcode der Ausführungs-Engine diff --git a/content/docs/build/automation/flows.es.mdx b/content/docs/build/automation/flows.es.mdx deleted file mode 100644 index 02e9fc3..0000000 --- a/content/docs/build/automation/flows.es.mdx +++ /dev/null @@ -1,239 +0,0 @@ ---- -title: Flujos y Automatización -description: Lógica de negocio declarativa — descrita a la IA o escrita en TypeScript, el runtime ejecuta el mismo artefacto en cualquiera de los dos casos. -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -Los flujos son la forma de expresar lógica de negocio sin escribir un servidor. -Cada flujo es metadato declarativo que el runtime ejecuta — igual -que los objetos y las vistas. Eso significa que los flujos aparecen en `os diff`, el -registro de auditoría, el constructor de flujos de Console y el [AI Builder](/docs/build/ai-builder) -todo a la vez. - -La mayoría de los clientes crean flujos pidiéndoselo a la IA: - -> *"Cuando un ticket de alta prioridad permanezca en 'new' durante 30 minutos, notifica -> al responsable por Slack."* - -La IA genera el flujo de abajo. Esta página describe su estructura para que puedas -leerlo y editarlo. - -Habilita la capacidad en tu stack: - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## Tres tipos de flujo - -| Tipo | Activado por | Úsalo para | -|---|---|---| -| **Autolaunched** | Un cambio en un registro (insert/update/delete) | "Enviar correo de bienvenida cuando un usuario se registra" | -| **Scheduled** | Expresión cron o intervalo | "Marcar tareas obsoletas cada noche a las 2am" | -| **Manual** | El usuario hace clic en un botón en Console, o una llamada a la API | Acciones de "Aprobar factura" | - -## Autolaunched: reaccionar a un cambio en un registro - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -Interpolación de variables: `{!trigger.record.<field>}`, `{!org.<field>}`, -`{!user.<field>}`, `{!step.<step-name>.output}`. Usa expresiones CEL en -los bloques `condition:`. - -Momento del trigger: - -| `when` | Se dispara | -|---|---| -| `before_insert` | Dentro de la transacción de escritura, antes del INSERT | -| `after_insert` | Después del commit | -| `before_update` | Dentro de la transacción de escritura, antes del UPDATE | -| `after_update` | Después del commit | -| `before_delete` | Dentro de la transacción de escritura, antes del DELETE | -| `after_delete` | Después del commit | - -Los flujos `before_*` pueden mutar el registro que se está escribiendo (calcular campos, -normalizar datos). Los flujos `after_*` se ejecutan de forma asíncrona y pueden llamar a servicios -externos lentos. - -## Scheduled: ejecutar según un reloj - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -Respaldado por la capacidad `@objectstack/service-job` — consulta -[Runtime Capabilities](/docs/reference/runtime-capabilities). - -## Manual: acciones y aprobaciones - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -Muéstralo en Console como un botón en la vista de Invoice, o invócalo mediante -REST: - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## Tipos de paso - -| Paso | Propósito | -|---|---| -| `query` | Leer registros mediante ObjectQL | -| `create` / `update` / `delete` | Escribir en objetos | -| `action` | Invocar una acción integrada o registrada por un plugin (email, webhook, llamada a IA, …) | -| `condition` | Ramificar según una expresión CEL | -| `foreach` | Iterar sobre una colección | -| `parallel` | Ejecutar sub-pasos de forma concurrente | -| `wait` | Pausar durante una duración / hasta un timestamp / hasta una condición | -| `subflow` | Llamar a otro flujo | -| `approval` | Bloquear hasta que un usuario apruebe (requiere `@objectstack/plugin-approvals`) | - -## Condiciones y ramificaciones - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## Manejo de errores - -Cada paso acepta: - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -Para los flujos autolaunched `before_*`, `onError: 'fail'` (por defecto) aborta -la transacción de escritura de origen. Para los flujos `after_*`, la escritura de origen -ya está confirmada; las ejecuciones de flujo fallidas pasan a la cola de reintentos -de jobs. - -## Fórmulas y expresiones (CEL) - -Las condiciones, los valores de campo dinámicos y las expresiones de filtro aceptan -todas **CEL** (Common Expression Language) — el lenguaje de Google para la evaluación -segura de expresiones: - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -CEL está aislado en un sandbox (sin efectos secundarios, sin E/S), se evalúa en el servidor y -es auditable en el constructor de flujos. - -## Constructor visual - -Console incluye un constructor visual de flujos que va y vuelve con el -metadato declarativo — quienes no son ingenieros pueden editar un flujo, y este se serializa -de vuelta a la misma estructura que el TypeScript que escribirías a mano. - -## Probar flujos - -```bash -os test --scenario "welcome email fires on signup" -``` - -## Límites y buenas prácticas - -- **Mantén pequeños los before-hooks.** Bloquean la transacción de escritura. -- **Usa `wait` en lugar de pasos de larga duración.** Un flujo que duerme - bloquea un worker; un `wait until` devuelve el worker al pool. -- **Usa `parallel` para pasos independientes.** La ejecución secuencial es - la opción por defecto. -- **La idempotencia importa.** Los reintentos pueden ejecutar el mismo paso dos veces; - los efectos secundarios externos deben deduplicarse (usa el id de ejecución del flujo como clave). -- **Acciones sensibles para auditoría.** Los flujos que cambian permisos o - eliminan registros deberían registrar ellos mismos en `sys_audit_log`. - -## A dónde ir después - -- [Webhooks](/docs/configure/webhooks) — notificaciones salientes, a menudo - activadas desde flujos -- [Email](/docs/configure/email) — el transporte de la acción `send_email` -- [AI Service](/docs/configure/ai) — la acción `ai_call` para pasos con LLM -- [API Access](/docs/configure/api-access) — invocar flujos manuales desde - sistemas externos -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) - — código fuente del motor de ejecución diff --git a/content/docs/build/automation/flows.fr.mdx b/content/docs/build/automation/flows.fr.mdx deleted file mode 100644 index 7e7fda3..0000000 --- a/content/docs/build/automation/flows.fr.mdx +++ /dev/null @@ -1,239 +0,0 @@ ---- -title: Flux & Automatisation -description: Logique métier déclarative — décrite à l'IA ou écrite en TypeScript, le runtime exécute le même artefact dans les deux cas. -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -Les flux sont la façon d'exprimer la logique métier sans écrire de serveur. -Chaque flux est une métadonnée déclarative que le runtime exécute — au même -titre que les objets et les vues. Cela signifie que les flux apparaissent dans `os diff`, le -journal d'audit, le générateur de flux de la Console et l'[AI Builder](/docs/build/ai-builder) -tout à la fois. - -La plupart des clients créent des flux en demandant à l'IA : - -> *« Lorsqu'un ticket à haute priorité reste en statut « new » pendant 30 minutes, notifier -> le manager sur Slack. »* - -L'IA génère le flux ci-dessous. Cette page décrit sa structure afin que vous puissiez -le lire et le modifier. - -Activez la capacité dans votre stack : - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## Trois types de flux - -| Type | Déclenché par | À utiliser pour | -|---|---|---| -| **Autolaunched** | Une modification d'enregistrement (insertion/mise à jour/suppression) | « Envoyer un e-mail de bienvenue à l'inscription d'un utilisateur » | -| **Scheduled** | Expression cron ou intervalle | « Marquer les tâches obsolètes chaque nuit à 2h » | -| **Manual** | L'utilisateur clique sur un bouton dans la Console, ou un appel d'API | Actions « Approuver la facture » | - -## Autolaunched : réagir à une modification d'enregistrement - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -Interpolation de variables : `{!trigger.record.<field>}`, `{!org.<field>}`, -`{!user.<field>}`, `{!step.<step-name>.output}`. Utilisez des expressions CEL dans -les blocs `condition:`. - -Moment de déclenchement : - -| `when` | Se déclenche | -|---|---| -| `before_insert` | À l'intérieur de la transaction d'écriture, avant l'INSERT | -| `after_insert` | Après le commit | -| `before_update` | À l'intérieur de la transaction d'écriture, avant l'UPDATE | -| `after_update` | Après le commit | -| `before_delete` | À l'intérieur de la transaction d'écriture, avant le DELETE | -| `after_delete` | Après le commit | - -Les flux `before_*` peuvent modifier l'enregistrement en cours d'écriture (calculer des champs, -normaliser des données). Les flux `after_*` s'exécutent de manière asynchrone et peuvent appeler des services externes -lents. - -## Scheduled : s'exécuter selon une horloge - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -Reposant sur la capacité `@objectstack/service-job` — voir -[Runtime Capabilities](/docs/reference/runtime-capabilities). - -## Manual : actions et approbations - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -Affichez-le dans la Console sous forme de bouton sur la vue Invoice, ou appelez-le via -REST : - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## Types d'étapes - -| Étape | Objectif | -|---|---| -| `query` | Lire des enregistrements via ObjectQL | -| `create` / `update` / `delete` | Écrire dans les objets | -| `action` | Invoquer une action intégrée ou enregistrée par un plugin (e-mail, webhook, appel IA, …) | -| `condition` | Brancher selon une expression CEL | -| `foreach` | Itérer sur une collection | -| `parallel` | Exécuter des sous-étapes en parallèle | -| `wait` | Mettre en pause pendant une durée / jusqu'à un horodatage / jusqu'à une condition | -| `subflow` | Appeler un autre flux | -| `approval` | Bloquer jusqu'à ce qu'un utilisateur approuve (nécessite `@objectstack/plugin-approvals`) | - -## Conditions et branchements - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## Gestion des erreurs - -Chaque étape accepte : - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -Pour les flux `before_*` de type autolaunched, `onError: 'fail'` (par défaut) interrompt -la transaction d'écriture d'origine. Pour les flux `after_*`, l'écriture d'origine -est déjà validée ; les exécutions de flux échouées sont placées dans la file d'attente de -nouvelle tentative des jobs. - -## Formules et expressions (CEL) - -Les conditions, valeurs de champ dynamiques et expressions de filtre acceptent toutes le -**CEL** (Common Expression Language) — le langage de Google pour l'évaluation sécurisée -d'expressions : - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -Le CEL est isolé (sans effets de bord, sans I/O), évalué côté serveur, et -auditable dans le générateur de flux. - -## Générateur visuel - -La Console est livrée avec un générateur de flux visuel qui fait l'aller-retour avec la -métadonnée déclarative — les non-ingénieurs peuvent modifier un flux, et celui-ci se sérialise -de nouveau dans la même structure que le TypeScript que vous écririez à la main. - -## Tester les flux - -```bash -os test --scenario "welcome email fires on signup" -``` - -## Limites & bonnes pratiques - -- **Gardez les before-hooks petits.** Ils bloquent la transaction d'écriture. -- **Utilisez `wait` plutôt que des étapes de longue durée.** Un flux qui se met en veille - bloque un worker ; un `wait until` rend le worker au pool. -- **Utilisez `parallel` pour les étapes indépendantes.** L'exécution séquentielle est - le comportement par défaut. -- **L'idempotence est importante.** Les nouvelles tentatives peuvent exécuter la même étape deux fois ; - les effets de bord externes doivent dédupliquer (utilisez l'identifiant d'exécution du flux comme clé). -- **Actions sensibles à l'audit.** Les flux qui modifient des permissions ou - suppriment des enregistrements doivent eux-mêmes consigner dans `sys_audit_log`. - -## Où aller ensuite - -- [Webhooks](/docs/configure/webhooks) — notifications sortantes, souvent - déclenchées depuis les flux -- [Email](/docs/configure/email) — le transport de l'action `send_email` -- [AI Service](/docs/configure/ai) — l'action `ai_call` pour les étapes LLM -- [API Access](/docs/configure/api-access) — invoquer des flux manuels depuis des - systèmes externes -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) - — code source du moteur d'exécution diff --git a/content/docs/build/automation/flows.ja.mdx b/content/docs/build/automation/flows.ja.mdx deleted file mode 100644 index 7acd1b2..0000000 --- a/content/docs/build/automation/flows.ja.mdx +++ /dev/null @@ -1,215 +0,0 @@ ---- -title: フローと自動化 -description: 宣言的なビジネスロジック — AI に記述してもらうか TypeScript で記述するかにかかわらず、ランタイムはどちらの場合も同じアーティファクトを実行します。 -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -フローは、サーバーを書くことなくビジネスロジックを表現する方法です。 -すべてのフローは、ランタイムが実行する宣言的なメタデータであり、これはオブジェクトやビューと同じです。つまり、フローは `os diff`、監査ログ、Console のフロービルダー、そして [AI Builder](/docs/build/ai-builder) のすべてに一度に表示されます。 - -ほとんどのお客様は、AI に依頼してフローを作成します。 - -> *「優先度の高いチケットが 30 分間「new」のままになっていたら、Slack でマネージャーに通知して。」* - -AI は以下のフローを生成します。このページでは、その構造を読んで編集できるように説明します。 - -スタックでこの機能を有効にします。 - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## 3 つのフロータイプ - -| タイプ | トリガー元 | 用途 | -|---|---|---| -| **Autolaunched** | レコードの変更(挿入/更新/削除) | 「ユーザー登録時にウェルカムメールを送信する」 | -| **Scheduled** | Cron 式または間隔 | 「毎晩 2 時に古いタスクをマークする」 | -| **Manual** | ユーザーが Console でボタンをクリック、または API 呼び出し | 「請求書を承認する」アクション | - -## Autolaunched: レコードの変更に反応する - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -変数の補間: `{!trigger.record.<field>}`、`{!org.<field>}`、 -`{!user.<field>}`、`{!step.<step-name>.output}`。`condition:` ブロックでは CEL 式を使用します。 - -トリガーのタイミング: - -| `when` | 発火 | -|---|---| -| `before_insert` | 書き込みトランザクション内、INSERT の前 | -| `after_insert` | コミット後 | -| `before_update` | 書き込みトランザクション内、UPDATE の前 | -| `after_update` | コミット後 | -| `before_delete` | 書き込みトランザクション内、DELETE の前 | -| `after_delete` | コミット後 | - -`before_*` フローは、書き込まれるレコードを変更できます(フィールドの計算、データの正規化)。`after_*` フローは非同期で実行され、低速な外部サービスを呼び出すことができます。 - -## Scheduled: 時刻に従って実行する - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -`@objectstack/service-job` 機能に支えられています。詳しくは [Runtime Capabilities](/docs/reference/runtime-capabilities) を参照してください。 - -## Manual: アクションと承認 - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -Console で Invoice ビューのボタンとして表示するか、REST 経由で呼び出します。 - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## ステップタイプ - -| ステップ | 目的 | -|---|---| -| `query` | ObjectQL でレコードを読み取る | -| `create` / `update` / `delete` | オブジェクトに書き込む | -| `action` | 組み込みアクションまたはプラグイン登録アクションを呼び出す(メール、Webhook、AI 呼び出し、…) | -| `condition` | CEL 式で分岐する | -| `foreach` | コレクションを反復処理する | -| `parallel` | サブステップを並行して実行する | -| `wait` | 一定時間 / タイムスタンプまで / 条件が満たされるまで一時停止する | -| `subflow` | 別のフローを呼び出す | -| `approval` | ユーザーが承認するまでブロックする(`@objectstack/plugin-approvals` が必要) | - -## 条件と分岐 - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## エラー処理 - -各ステップは以下を受け付けます。 - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -autolaunched の `before_*` フローでは、`onError: 'fail'`(デフォルト)は元の書き込みトランザクションを中止します。`after_*` フローでは、元の書き込みはすでにコミットされており、失敗したフロー実行はジョブの再試行キューに入ります。 - -## 数式と式(CEL) - -条件、動的なフィールド値、フィルター式はすべて **CEL**(Common Expression Language)を受け付けます。これは、安全な式評価のための Google の言語です。 - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -CEL はサンドボックス化されており(副作用なし、I/O なし)、サーバー側で評価され、フロービルダーで監査可能です。 - -## ビジュアルビルダー - -Console には、宣言的なメタデータとラウンドトリップするビジュアルフロービルダーが付属しています。エンジニアでない人でもフローを編集でき、手作業で記述する TypeScript と同じ構造にシリアライズされます。 - -## フローのテスト - -```bash -os test --scenario "welcome email fires on signup" -``` - -## 制限とベストプラクティス - -- **before フックは小さく保ちます。** 書き込みトランザクションをブロックします。 -- **長時間実行されるステップの代わりに `wait` を使用します。** スリープするフローはワーカーをブロックしますが、`wait until` はワーカーをプールに返します。 -- **独立したステップには `parallel` を使用します。** デフォルトは順次実行です。 -- **冪等性が重要です。** 再試行では同じステップが 2 回実行されることがあります。外部の副作用は重複排除する必要があります(フロー実行 ID をキーとして使用します)。 -- **監査が必要なアクション。** 権限を変更したりレコードを削除したりするフローは、それ自体を `sys_audit_log` に記録する必要があります。 - -## 次のステップ - -- [Webhooks](/docs/configure/webhooks) — アウトバウンド通知。多くの場合フローからトリガーされます -- [Email](/docs/configure/email) — `send_email` アクションのトランスポート -- [AI Service](/docs/configure/ai) — LLM ステップ用の `ai_call` アクション -- [API Access](/docs/configure/api-access) — 外部システムから手動フローを呼び出す -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) - — 実行エンジンのソース diff --git a/content/docs/build/automation/flows.ko.mdx b/content/docs/build/automation/flows.ko.mdx deleted file mode 100644 index 6db3baa..0000000 --- a/content/docs/build/automation/flows.ko.mdx +++ /dev/null @@ -1,235 +0,0 @@ ---- -title: 플로우 및 자동화 -description: 선언형 비즈니스 로직 — AI에게 설명하거나 TypeScript로 작성하면, 런타임은 어느 쪽이든 동일한 아티팩트를 실행합니다. -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -플로우는 서버를 작성하지 않고도 비즈니스 로직을 표현하는 방법입니다. -모든 플로우는 런타임이 실행하는 선언형 메타데이터로, 객체나 뷰와 -동일합니다. 즉, 플로우는 `os diff`, 감사 로그, Console의 플로우 빌더, -그리고 [AI Builder](/docs/build/ai-builder)에 한꺼번에 나타납니다. - -대부분의 고객은 AI에게 요청하여 플로우를 만듭니다. - -> *"우선순위가 높은 티켓이 'new' 상태로 30분간 머물러 있으면, -> Slack으로 매니저에게 알려줘."* - -AI가 아래 플로우를 생성합니다. 이 페이지에서는 읽고 편집할 수 있도록 -그 구조를 설명합니다. - -스택에서 해당 기능을 활성화하세요. - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## 세 가지 플로우 유형 - -| 유형 | 트리거 | 사용 사례 | -|---|---|---| -| **Autolaunched** | 레코드 변경 (insert/update/delete) | "사용자 등록 시 환영 이메일 보내기" | -| **Scheduled** | Cron 표현식 또는 간격 | "매일 밤 오래된 작업 표시하기" | -| **Manual** | 사용자가 Console에서 버튼을 클릭하거나 API 호출 | "송장 승인" 액션 | - -## Autolaunched: 레코드 변경에 반응하기 - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -변수 보간: `{!trigger.record.<field>}`, `{!org.<field>}`, -`{!user.<field>}`, `{!step.<step-name>.output}`. `condition:` 블록에서는 -CEL 표현식을 사용하세요. - -트리거 타이밍: - -| `when` | 실행 시점 | -|---|---| -| `before_insert` | 쓰기 트랜잭션 내부, INSERT 이전 | -| `after_insert` | 커밋 이후 | -| `before_update` | 쓰기 트랜잭션 내부, UPDATE 이전 | -| `after_update` | 커밋 이후 | -| `before_delete` | 쓰기 트랜잭션 내부, DELETE 이전 | -| `after_delete` | 커밋 이후 | - -`before_*` 플로우는 작성 중인 레코드를 변경할 수 있습니다(필드 계산, -데이터 정규화). `after_*` 플로우는 비동기로 실행되며 느린 외부 서비스를 -호출할 수 있습니다. - -## Scheduled: 정해진 시각에 실행하기 - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -`@objectstack/service-job` 기능이 이를 지원합니다 — -[Runtime Capabilities](/docs/reference/runtime-capabilities)를 참조하세요. - -## Manual: 액션 및 승인 - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -Console에서 Invoice 뷰의 버튼으로 노출하거나, REST를 통해 호출하세요. - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## 스텝 유형 - -| 스텝 | 용도 | -|---|---| -| `query` | ObjectQL을 통해 레코드 읽기 | -| `create` / `update` / `delete` | 객체에 쓰기 | -| `action` | 내장 또는 플러그인 등록 액션 호출 (이메일, 웹훅, AI 호출 등) | -| `condition` | CEL 표현식으로 분기 | -| `foreach` | 컬렉션 순회 | -| `parallel` | 하위 스텝을 동시에 실행 | -| `wait` | 일정 시간 / 특정 시각까지 / 조건 충족까지 일시 정지 | -| `subflow` | 다른 플로우 호출 | -| `approval` | 사용자가 승인할 때까지 차단 (`@objectstack/plugin-approvals` 필요) | - -## 조건 및 분기 - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## 오류 처리 - -각 스텝은 다음을 받습니다: - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -Autolaunched `before_*` 플로우의 경우, `onError: 'fail'`(기본값)은 -원본 쓰기 트랜잭션을 중단시킵니다. `after_*` 플로우의 경우, 원본 쓰기는 -이미 커밋된 상태이며, 실패한 플로우 실행은 작업 재시도 큐로 들어갑니다. - -## 수식 및 표현식 (CEL) - -조건, 동적 필드 값, 필터 표현식은 모두 **CEL**(Common Expression -Language) — 안전한 표현식 평가를 위한 Google의 언어 — 을 받습니다: - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -CEL은 샌드박스로 격리되어 있고(부수 효과 없음, I/O 없음), 서버 측에서 -평가되며, 플로우 빌더에서 감사가 가능합니다. - -## 비주얼 빌더 - -Console에는 선언형 메타데이터와 양방향으로 변환되는 비주얼 플로우 -빌더가 함께 제공됩니다 — 비엔지니어도 플로우를 편집할 수 있으며, 직접 -손으로 작성하는 TypeScript와 동일한 구조로 다시 직렬화됩니다. - -## 플로우 테스트 - -```bash -os test --scenario "welcome email fires on signup" -``` - -## 한계 및 모범 사례 - -- **before 훅은 작게 유지하세요.** 쓰기 트랜잭션을 차단합니다. -- **장시간 실행되는 스텝 대신 `wait`를 사용하세요.** 슬립하는 플로우는 - 워커를 차단하지만, `wait until`은 워커를 풀로 반환합니다. -- **독립적인 스텝에는 `parallel`을 사용하세요.** 기본값은 순차 실행입니다. -- **멱등성이 중요합니다.** 재시도는 동일한 스텝을 두 번 실행할 수 - 있으므로, 외부 부수 효과는 중복을 제거해야 합니다(플로우 실행 id를 - 키로 사용하세요). -- **감사에 민감한 액션.** 권한을 변경하거나 레코드를 삭제하는 플로우는 - 스스로 `sys_audit_log`에 로그를 남겨야 합니다. - -## 다음으로 볼 내용 - -- [Webhooks](/docs/configure/webhooks) — 아웃바운드 알림으로, 흔히 - 플로우에서 트리거됩니다 -- [Email](/docs/configure/email) — `send_email` 액션의 전송 수단 -- [AI Service](/docs/configure/ai) — LLM 스텝을 위한 `ai_call` 액션 -- [API Access](/docs/configure/api-access) — 외부 시스템에서 manual - 플로우 호출하기 -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) - — 실행 엔진의 소스 diff --git a/content/docs/build/automation/flows.mdx b/content/docs/build/automation/flows.mdx index 3e047e5..a38aafe 100644 --- a/content/docs/build/automation/flows.mdx +++ b/content/docs/build/automation/flows.mdx @@ -304,7 +304,6 @@ os test --scenario "welcome email fires on signup" - [Webhooks](/docs/configure/webhooks) — outbound notifications, often triggered from flows - [Email](/docs/configure/email) — the `send_email` action's transport -- [AI Service](/docs/configure/ai) — `ai_call` action for LLM steps - [API Access](/docs/configure/api-access) — invoke manual flows from external systems - [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) diff --git a/content/docs/build/automation/flows.zh-Hans.mdx b/content/docs/build/automation/flows.zh-Hans.mdx deleted file mode 100644 index 95909c0..0000000 --- a/content/docs/build/automation/flows.zh-Hans.mdx +++ /dev/null @@ -1,271 +0,0 @@ ---- -title: 流程与自动化 -description: 声明式业务逻辑 —— 无论是描述给 AI 还是用 TypeScript 编写,运行时执行的都是同一份产物。 -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -Flow 是你不写服务端就能表达业务逻辑的方式。每个 Flow 都是声明式元数据,由运行时执行 —— 与对象、视图一样。这意味着 Flow 会同时出现在 `os diff`、审计日志、Console 的流程构建器以及 [AI Builder](/docs/build/ai-builder) 中。 - -多数客户通过对 AI 提问来创建 Flow: - -> *"当一个高优先级工单在 'new' 状态停留 30 分钟时,在 Slack 上通知经理。"* - -AI 会生成下面的 Flow。本页描述其结构,以便你阅读和编辑。 - -在 stack 中启用该能力: - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## 流程类型 - -| 类型 | 触发方式 | 用于 | -|---|---|---| -| **Autolaunched** | 记录变更(insert/update/delete) | "用户注册时发欢迎邮件" | -| **Scheduled** | Cron 表达式或间隔 | "每晚 2 点标记过期任务" | -| **Time-relative** *(16.0)* | 日期字段与今天的距离,经由每日扫描 | "合同到期前 60 天提醒我" | -| **Manual** | 用户在 Console 点按钮,或 API 调用 | "审批发票"等 Action | - -## Autolaunched:对记录变更做出反应 - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -变量插值:`{!trigger.record.<field>}`、`{!org.<field>}`、`{!user.<field>}`、`{!step.<step-name>.output}`。`condition:` 块中使用 CEL 表达式。 - -触发时机: - -| `when` | 何时触发 | -|---|---| -| `before_insert` | 写事务内、INSERT 之前 | -| `after_insert` | 提交之后 | -| `before_update` | 写事务内、UPDATE 之前 | -| `after_update` | 提交之后 | -| `before_delete` | 写事务内、DELETE 之前 | -| `after_delete` | 提交之后 | - -`before_*` 流程可以修改正在写入的记录(计算字段、归一化数据)。`after_*` 流程异步运行,可以调用慢的外部服务。 - -## Scheduled:按时钟运行 - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -由 `@objectstack/service-job` 能力承载 —— 见 [Runtime Capabilities](/docs/reference/runtime-capabilities)。 - -## Time-relative:相对日期字段触发(16.0) - -"合同到期前 60 天提醒我"以前只能写成一个用 `record.end_date == daysFromNow(60)` 做门控的记录变更流程 —— 这个谓词只有在记录*恰好被修改*时才会求值,无人照看时几乎永远不会触发。别这么写。改为声明**时间相对触发器**:流程的开始节点携带一个 `config.timeRelative` 描述符,运行时按计划扫描该对象,并为**每条匹配记录各启动一次**流程: - -```ts -export const renewalReminder = defineFlow({ - name: 'contract_renewal_reminder', - type: 'schedule', - status: 'active', - nodes: [ - { - id: 'start', - type: 'start', - config: { - timeRelative: { - object: 'contracts', - dateField: 'end_date', - offsetDays: [60, 30, 7], // T-minus thresholds - filter: { status: 'active' }, // AND-ed with the date window - }, - // Optional sweep cadence — defaults to daily at 08:00 UTC. - schedule: { type: 'cron', expression: '0 8 * * *' }, - }, - }, - { id: 'notify_owner', type: 'notify', label: 'Notify Owner' }, - { id: 'end', type: 'end' }, - ], - edges: [ - { id: 'e1', source: 'start', target: 'notify_owner' }, - { id: 'e2', source: 'notify_owner', target: 'end' }, - ], -}); -``` - -| 键 | 声明什么 | -|---|---| -| `object` | 扫描其记录的对象(机器名) | -| `dateField` | 相对今天求值的 `date` / `datetime` 字段(按天粒度) | -| `offsetDays` | **偏移模式** —— 当 `dateField` 恰好等于今天 + 所列各偏移时触发(`[60, 30, 7]`;负数 = 过去,如 `[-1]` 表示次日) | -| `withinDays` | **区间模式** —— `dateField` 落在距今天 N 天内的每一天都触发:正数 = 即将到来("即将过期"),负数 = 有界的逾期回看,`0` = 今天到期 | -| `filter` | 可选的 ObjectQL where 映射,与计算出的日期窗口做 AND(如 `{ status: 'active' }`) | -| `maxRecords` | 每次扫描启动流程的记录数上限(默认 1000;触发收拢时扫描会记日志) | - -`offsetDays` 与 `withinDays` 必须**恰好设置一个**。另外两种常见形态: - -```ts -// "Expiring soon" — fires every day a document is within 30 days of expiry. -timeRelative: { object: 'hr_document', dateField: 'expires_on', withinDays: 30 } - -// Overdue sweep — fires for POs up to 14 days past due (bounded lookback). -timeRelative: { object: 'purchase_order', dateField: 'due_date', - withinDays: -14, filter: { status: 'open' } } -``` - -每次启动都会把匹配记录放到自动化上下文中,因此开始节点的 `condition` 和 `{record.<field>}` 插值与记录变更流程完全一致。发现查询以 system 身份运行,并做按记录的故障隔离。`os validate` 新增就绪性检查 —— 当 `timeRelative.object` 指向 stack 未定义的对象、或自动触发的流程停留在 `draft` 状态时都会警告。 - -> **真正的"当天"检查现在没问题了(#3183)。**16.0 中 `record.due_date == today()` 能匹配了 —— 引擎会对时间类 `==` / `!=` 比较做强制转换,让日期字段与 `today()` 正确比较。真正的"今天到期"条件用它;而任何"某日期前/后 N 天"形态的需求,用 `timeRelative`。 - -## Manual:Action 与审批 - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -在 Console 中作为 Invoice 视图上的按钮暴露,或通过 REST 调用: - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## 步骤类型 - -| 步骤 | 用途 | -|---|---| -| `query` | 通过 ObjectQL 读取记录 | -| `create` / `update` / `delete` | 写对象 | -| `action` | 调用内置或插件注册的 Action(email、webhook、AI 调用……) | -| `condition` | 按 CEL 表达式分支 | -| `foreach` | 遍历集合 | -| `parallel` | 并发执行子步骤 | -| `wait` | 暂停一段时长 / 至时间戳 / 至条件成立 | -| `subflow` | 调用另一个 Flow | -| `approval` | 阻塞直至用户审批(需要 `@objectstack/plugin-approvals`) | - -## 条件与分支 - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## 错误处理 - -每个步骤接受: - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -对于 autolaunched 的 `before_*` 流程,`onError: 'fail'`(默认)会终止原始写事务。对 `after_*` 流程,原始写已经提交,失败的流程运行会落到任务重试队列。 - -## 公式与表达式(CEL) - -条件、动态字段值和过滤表达式都接受 **CEL**(Common Expression Language) —— 谷歌为安全表达式求值设计的语言: - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -CEL 是沙盒化的(无副作用、无 I/O)、服务端求值、并可在流程构建器中审计。 - -## 可视化构建器 - -Console 自带一个可视化流程构建器,它与声明式元数据来回往返 —— 非工程师可以编辑流程,序列化结果与你手写的 TypeScript 同形。 - -## 测试流程 - -```bash -os test --scenario "welcome email fires on signup" -``` - -## 限制与最佳实践 - -- **保持 before-hook 简短。**它们会阻塞写事务。 -- **用 `wait` 代替长时运行的步骤。**休眠的流程会占用 worker;`wait until` 会把 worker 还回池里。 -- **用 `parallel` 跑独立步骤。**默认是顺序执行。 -- **幂等很重要。**重试可能让同一步骤跑两次;外部副作用应去重(用流程运行 id 作为键)。 -- **审计敏感的 Action。**改权限或删记录的流程自身也应记录到 `sys_audit_log`。 - -## 下一步 - -- [Webhooks](/docs/configure/webhooks) —— 出站通知,常由流程触发 -- [Email](/docs/configure/email) —— `send_email` Action 的传输层 -- [AI Service](/docs/configure/ai) —— 用于 LLM 步骤的 `ai_call` Action -- [API Access](/docs/configure/api-access) —— 从外部系统调用手动流程 -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) —— 执行引擎源码 diff --git a/content/docs/build/automation/flows.zh-Hant.mdx b/content/docs/build/automation/flows.zh-Hant.mdx deleted file mode 100644 index 5b28170..0000000 --- a/content/docs/build/automation/flows.zh-Hant.mdx +++ /dev/null @@ -1,272 +0,0 @@ ---- -# @generated zh-Hant from zh-Hans -- do not edit. Edit the English source, then regenerate: pnpm --filter @objectos/docs gen:zh-hant -title: 流程與自動化 -description: 宣告式業務邏輯 —— 無論是描述給 AI 還是用 TypeScript 編寫,執行時執行的都是同一份產物。 -translation: - source_sha: f08495495034c8de3d9e0e09657c815b81252441b23af0c849670f7c8c2e2c1e - guide_rev: 1 - mode: auto ---- - -Flow 是你不寫服務端就能表達業務邏輯的方式。每個 Flow 都是宣告式後設資料,由執行時執行 —— 與物件、檢視一樣。這意味著 Flow 會同時出現在 `os diff`、審計日誌、Console 的流程構建器以及 [AI Builder](/docs/build/ai-builder) 中。 - -多數客戶通過對 AI 提問來建立 Flow: - -> *"當一個高優先順序工單在 'new' 狀態停留 30 分鐘時,在 Slack 上通知經理。"* - -AI 會生成下面的 Flow。本頁描述其結構,以便你閱讀和編輯。 - -在 stack 中啟用該能力: - -```ts -export default defineStack({ - // ... - requires: ['automation'], -}); -``` - -## 流程型別 - -| 型別 | 觸發方式 | 用於 | -|---|---|---| -| **Autolaunched** | 記錄變更(insert/update/delete) | "使用者註冊時發歡迎郵件" | -| **Scheduled** | Cron 表示式或間隔 | "每晚 2 點標記過期任務" | -| **Time-relative** *(16.0)* | 日期欄位與今天的距離,經由每日掃描 | "合同到期前 60 天提醒我" | -| **Manual** | 使用者在 Console 點按鈕,或 API 呼叫 | "審批發票"等 Action | - -## Autolaunched:對記錄變更做出反應 - -```ts -// src/flows/welcome_email.ts -import { defineFlow } from '@objectstack/spec'; - -export const welcomeEmail = defineFlow({ - name: 'welcome_email', - type: 'autolaunched', - trigger: { - object: 'sys_user', - when: 'after_insert', - }, - steps: [ - { - type: 'action', - action: 'send_email', - inputs: { - to: '{!trigger.record.email}', - subject: 'Welcome to {!org.name}', - body: 'Hi {!trigger.record.name}, welcome aboard.', - }, - }, - ], -}); -``` - -變數插值:`{!trigger.record.<field>}`、`{!org.<field>}`、`{!user.<field>}`、`{!step.<step-name>.output}`。`condition:` 塊中使用 CEL 表示式。 - -觸發時機: - -| `when` | 何時觸發 | -|---|---| -| `before_insert` | 寫事務內、INSERT 之前 | -| `after_insert` | 提交之後 | -| `before_update` | 寫事務內、UPDATE 之前 | -| `after_update` | 提交之後 | -| `before_delete` | 寫事務內、DELETE 之前 | -| `after_delete` | 提交之後 | - -`before_*` 流程可以修改正在寫入的記錄(計算欄位、歸一化資料)。`after_*` 流程非同步執行,可以呼叫慢的外部服務。 - -## Scheduled:按時鐘執行 - -```ts -export const nightlyCleanup = defineFlow({ - name: 'nightly_cleanup', - type: 'scheduled', - schedule: { cron: '0 2 * * *', timezone: 'America/New_York' }, - steps: [ - { - type: 'query', - query: { object: 'task', filter: 'status:open AND due_lt:now()' }, - output: 'stale', - }, - { - type: 'foreach', - items: '{!step.stale}', - do: [ - { type: 'update', record: '{!item.id}', fields: { status: 'overdue' } }, - ], - }, - ], -}); -``` - -由 `@objectstack/service-job` 能力承載 —— 見 [Runtime Capabilities](/docs/reference/runtime-capabilities)。 - -## Time-relative:相對日期欄位觸發(16.0) - -"合同到期前 60 天提醒我"以前只能寫成一個用 `record.end_date == daysFromNow(60)` 做門控的記錄變更流程 —— 這個謂詞只有在記錄*恰好被修改*時才會求值,無人照看時幾乎永遠不會觸發。別這麼寫。改為宣告**時間相對觸發器**:流程的開始節點攜帶一個 `config.timeRelative` 描述符,執行時按計劃掃描該物件,併為**每條匹配記錄各啟動一次**流程: - -```ts -export const renewalReminder = defineFlow({ - name: 'contract_renewal_reminder', - type: 'schedule', - status: 'active', - nodes: [ - { - id: 'start', - type: 'start', - config: { - timeRelative: { - object: 'contracts', - dateField: 'end_date', - offsetDays: [60, 30, 7], // T-minus thresholds - filter: { status: 'active' }, // AND-ed with the date window - }, - // Optional sweep cadence — defaults to daily at 08:00 UTC. - schedule: { type: 'cron', expression: '0 8 * * *' }, - }, - }, - { id: 'notify_owner', type: 'notify', label: 'Notify Owner' }, - { id: 'end', type: 'end' }, - ], - edges: [ - { id: 'e1', source: 'start', target: 'notify_owner' }, - { id: 'e2', source: 'notify_owner', target: 'end' }, - ], -}); -``` - -| 鍵 | 宣告什麼 | -|---|---| -| `object` | 掃描其記錄的物件(機器名) | -| `dateField` | 相對今天求值的 `date` / `datetime` 欄位(按天粒度) | -| `offsetDays` | **偏移模式** —— 當 `dateField` 恰好等於今天 + 所列各偏移時觸發(`[60, 30, 7]`;負數 = 過去,如 `[-1]` 表示次日) | -| `withinDays` | **區間模式** —— `dateField` 落在距今天 N 天內的每一天都觸發:正數 = 即將到來("即將過期"),負數 = 有界的逾期回看,`0` = 今天到期 | -| `filter` | 可選的 ObjectQL where 對映,與計算出的日期視窗做 AND(如 `{ status: 'active' }`) | -| `maxRecords` | 每次掃描啟動流程的記錄數上限(預設 1000;觸發收攏時掃描會記日誌) | - -`offsetDays` 與 `withinDays` 必須**恰好設定一個**。另外兩種常見形態: - -```ts -// "Expiring soon" — fires every day a document is within 30 days of expiry. -timeRelative: { object: 'hr_document', dateField: 'expires_on', withinDays: 30 } - -// Overdue sweep — fires for POs up to 14 days past due (bounded lookback). -timeRelative: { object: 'purchase_order', dateField: 'due_date', - withinDays: -14, filter: { status: 'open' } } -``` - -每次啟動都會把匹配記錄放到自動化上下文中,因此開始節點的 `condition` 和 `{record.<field>}` 插值與記錄變更流程完全一致。發現查詢以 system 身份執行,並做按記錄的故障隔離。`os validate` 新增就緒性檢查 —— 當 `timeRelative.object` 指向 stack 未定義的物件、或自動觸發的流程停留在 `draft` 狀態時都會警告。 - -> **真正的"當天"檢查現在沒問題了(#3183)。**16.0 中 `record.due_date == today()` 能匹配了 —— 引擎會對時間類 `==` / `!=` 比較做強制轉換,讓日期欄位與 `today()` 正確比較。真正的"今天到期"條件用它;而任何"某日期前/後 N 天"形態的需求,用 `timeRelative`。 - -## Manual:Action 與審批 - -```ts -export const approveInvoice = defineFlow({ - name: 'approve_invoice', - type: 'manual', - inputs: { - invoice_id: { type: 'lookup', reference: 'invoice', required: true }, - note: { type: 'textarea' }, - }, - steps: [ - { - type: 'update', - record: '{!inputs.invoice_id}', - fields: { status: 'approved', approved_by: '{!user.id}' }, - }, - ], -}); -``` - -在 Console 中作為 Invoice 檢視上的按鈕暴露,或通過 REST 呼叫: - -```bash -curl -X POST https://app.example.com/api/v1/actions/invoice/approve_invoice \ - -H 'Authorization: Bearer <token>' \ - -d '{"inputs": {"invoice_id": "inv_123", "note": "OK"}}' -``` - -## 步驟型別 - -| 步驟 | 用途 | -|---|---| -| `query` | 通過 ObjectQL 讀取記錄 | -| `create` / `update` / `delete` | 寫物件 | -| `action` | 呼叫內建或外掛註冊的 Action(email、webhook、AI 呼叫……) | -| `condition` | 按 CEL 表示式分支 | -| `foreach` | 遍歷集合 | -| `parallel` | 併發執行子步驟 | -| `wait` | 暫停一段時長 / 至時間戳 / 至條件成立 | -| `subflow` | 呼叫另一個 Flow | -| `approval` | 阻塞直至使用者審批(需要 `@objectstack/plugin-approvals`) | - -## 條件與分支 - -```ts -{ - type: 'condition', - when: 'trigger.record.amount > 10000', - then: [ - { type: 'action', action: 'send_slack', inputs: { /* ... */ } }, - ], - else: [ - { type: 'update', record: '{!trigger.record.id}', fields: { status: 'auto_approved' } }, - ], -} -``` - -## 錯誤處理 - -每個步驟接受: - -```ts -{ - type: 'action', - action: 'send_email', - inputs: { /* ... */ }, - retry: { attempts: 3, backoffMs: 1000, multiplier: 2 }, - onError: 'continue' | 'fail' | 'rollback', -} -``` - -對於 autolaunched 的 `before_*` 流程,`onError: 'fail'`(預設)會終止原始寫事務。對 `after_*` 流程,原始寫已經提交,失敗的流程執行會落到任務重試佇列。 - -## 公式與表示式(CEL) - -條件、動態欄位值和過濾表示式都接受 **CEL**(Common Expression Language) —— 谷歌為安全表示式求值設計的語言: - -```ts -'amount > 10000 && account.tier == "enterprise"' -'duration(now() - created_at) > duration("30d")' -'has(record.notes) && record.notes != ""' -``` - -CEL 是沙盒化的(無副作用、無 I/O)、服務端求值、並可在流程構建器中審計。 - -## 視覺化構建器 - -Console 自帶一個視覺化流程構建器,它與宣告式後設資料來回往返 —— 非工程師可以編輯流程,序列化結果與你手寫的 TypeScript 同形。 - -## 測試流程 - -```bash -os test --scenario "welcome email fires on signup" -``` - -## 限制與最佳實踐 - -- **保持 before-hook 簡短。**它們會阻塞寫事務。 -- **用 `wait` 代替長時執行的步驟。**休眠的流程會佔用 worker;`wait until` 會把 worker 還回池裡。 -- **用 `parallel` 跑獨立步驟。**預設是順序執行。 -- **冪等很重要。**重試可能讓同一步驟跑兩次;外部副作用應去重(用流程執行 id 作為鍵)。 -- **審計敏感的 Action。**改許可權或刪記錄的流程自身也應記錄到 `sys_audit_log`。 - -## 下一步 - -- [Webhooks](/docs/configure/webhooks) —— 出站通知,常由流程觸發 -- [Email](/docs/configure/email) —— `send_email` Action 的傳輸層 -- [AI Service](/docs/configure/ai) —— 用於 LLM 步驟的 `ai_call` Action -- [API Access](/docs/configure/api-access) —— 從外部系統呼叫手動流程 -- [@objectstack/service-automation](https://github.com/objectstack-ai/objectstack/tree/main/packages/services/service-automation) —— 執行引擎原始碼 diff --git a/content/docs/configure/ai.mdx b/content/docs/configure/ai.mdx index 05471a4..fe59244 100644 --- a/content/docs/configure/ai.mdx +++ b/content/docs/configure/ai.mdx @@ -9,11 +9,11 @@ the `build` authoring assistant, and the `/api/v1/ai/*` chat endpoints — on top of AI primitives that ship in the open-source ObjectStack framework: the MCP server, the Knowledge Protocol and its adapters, and the embedder. -On a Self-Managed deployment, a licence is what unlocks the in-product AI. A +On a Self-Managed deployment, a license is what unlocks the in-product AI. A single-organization deployment without one runs with Community behaviour — the open MCP server, bring your own AI — and with one, the AI Builder and "ask your data" are unlocked on your own infrastructure. See -[Docker → Licence](/docs/deploy/docker#licence). +[Docker → License](/docs/deploy/docker#license). ## What is configured where diff --git a/content/docs/configure/index.mdx b/content/docs/configure/index.mdx index fab03e3..e64f6ba 100644 --- a/content/docs/configure/index.mdx +++ b/content/docs/configure/index.mdx @@ -1,6 +1,6 @@ --- -title: Administration -seoTitle: "Administration: Users, Access and Settings" +title: Configure +seoTitle: "Configure: Users, Access and Settings" description: Where system administrators manage users, access, settings, and integrations — and which page answers which task. --- diff --git a/content/docs/configure/permissions/record-access.mdx b/content/docs/configure/permissions/record-access.mdx index 6465f2d..161a1a2 100644 --- a/content/docs/configure/permissions/record-access.mdx +++ b/content/docs/configure/permissions/record-access.mdx @@ -29,7 +29,7 @@ or wider default opens them up. Declare the model explicitly to restore open behavior: ```ts -defineObject({ +ObjectSchema.create({ name: 'crm_lead', sharingModel: 'public_read_write', // … diff --git a/content/docs/configure/runtime.mdx b/content/docs/configure/runtime.mdx index f3b32cf..75da7a3 100644 --- a/content/docs/configure/runtime.mdx +++ b/content/docs/configure/runtime.mdx @@ -62,15 +62,15 @@ Some configurations are checked when configuration is loaded and **refused** rather than degraded. A refusal prints a fatal error naming the settings that disagree, and exits. -- **Licence mode and cloud posture are coupled.** An ordinary licence is - validated online against a control plane; an air-gap licence is verified - locally with no network traffic. Pairing an ordinary licence with "no control +- **License mode and cloud posture are coupled.** An ordinary license is + validated online against a control plane; an air-gap license is verified + locally with no network traffic. Pairing an ordinary license with "no control plane" is impossible, not degraded, and is refused before any validation is attempted. [Air-gapped](/docs/deploy/air-gapped) carries the full matrix. - **Leaving the cloud posture unset is not "no cloud".** Unset resolves to the *public* control plane. A deployment meant to talk to nobody has to say so explicitly. -- **A multi-organization posture requires a licence**, and makes two further +- **A multi-organization posture requires a license**, and makes two further decisions mandatory: what a new sign-up joins, and which AI agents are mounted. Declaring either available value is accepted; not deciding is not. A single-organization deployment sees neither check. @@ -81,7 +81,7 @@ disagree, and exits. ## Connecting to a control plane A control plane is what serves the marketplace catalog and validates an -ordinary licence. Connecting is a **binding**, not a pasted credential: the +ordinary license. Connecting is a **binding**, not a pasted credential: the deployment is bound to the control plane once, and the runtime persists the token it was issued and presents that on every later call. There is no deployment API key to distribute or rotate by hand. diff --git a/content/docs/deploy/air-gapped.mdx b/content/docs/deploy/air-gapped.mdx index 7cae3e7..d7a4b65 100644 --- a/content/docs/deploy/air-gapped.mdx +++ b/content/docs/deploy/air-gapped.mdx @@ -1,27 +1,27 @@ --- title: Air-gapped Deployment -seoTitle: "Air-gapped Deployment: Licence Mode and Settings" -description: Air-gap is a licence mode, not a firewall setting — the two settings a disconnected deployment must declare, and the combinations refused at startup. +seoTitle: "Air-gapped Deployment: License Mode and Settings" +description: Air-gap is a license mode, not a firewall setting — the two settings a disconnected deployment must declare, and the combinations refused at startup. --- -## Air-gap is a licence mode +## Air-gap is a license mode You cannot reach an air-gapped deployment by unplugging the network. An -ordinary ObjectOS licence is validated **online**: the runtime exchanges your -licence key for a short-term entitlement at a control plane and re-checks -periodically. Cut that path and the licence does not "fall back to offline" — +ordinary ObjectOS license is validated **online**: the runtime exchanges your +license key for a short-term entitlement at a control plane and re-checks +periodically. Cut that path and the license does not "fall back to offline" — it can never take effect at all. -An **air-gap licence is a different licence**, issued offline by us on +An **air-gap license is a different license**, issued offline by us on purpose. It is verified locally against a public key embedded in the image and mounts your entitlements with **zero network traffic**. Air-gapped operation is an Enterprise capability — see -[License & Pricing](/docs/resources/license) — and the licence you need is +[License & Pricing](/docs/resources/license) — and the license you need is requested, not configured. A disconnected deployment therefore declares **two** things, together: -- an **air-gap licence**, and +- an **air-gap license**, and - **no control plane**: the cloud-posture setting explicitly set to `off` (its aliases `none`, `local` and `disabled` mean the same thing). @@ -41,20 +41,20 @@ to nobody. Declaring a posture is accepted; **not deciding is not.** Write ## The supported combinations These are checked when configuration is loaded, **before** anything -licence-shaped happens. An unsupported combination is not a warning and not a +license-shaped happens. An unsupported combination is not a warning and not a degrade: the runtime prints a fatal error naming *both* settings and exits. -| Licence | Cloud posture | Outcome | +| License | Cloud posture | Outcome | |---|---|---| -| Air-gap licence | `off` | **Runs, fully licensed, with zero network traffic.** The disconnected answer. | -| No licence | `off` | Runs with Community behaviour. Note that a multi-organization posture still requires a licence. | -| Ordinary (online) licence | reachable control plane, with the device binding completed | The normal connected deployment. | -| Ordinary (online) licence | `off` | **Refused at startup.** | -| Ordinary (online) licence | unset | Treated as the public control plane — see above. Whether it runs depends on whether that control plane is actually reachable and bound. | +| Air-gap license | `off` | **Runs, fully licensed, with zero network traffic.** The disconnected answer. | +| No license | `off` | Runs with Community behaviour. Note that a multi-organization posture still requires a license. | +| Ordinary (online) license | reachable control plane, with the device binding completed | The normal connected deployment. | +| Ordinary (online) license | `off` | **Refused at startup.** | +| Ordinary (online) license | unset | Treated as the public control plane — see above. Whether it runs depends on whether that control plane is actually reachable and bound. | ### Why the refusal exists -An online licence is validated by an exchange that must present a runtime +An online license is validated by an exchange that must present a runtime token minted during the deployment's binding to a control plane. With no control plane there is no binding, so the token cannot exist, so the exchange is never made — not on this boot, not on a later one. The grace period does @@ -63,7 +63,7 @@ there would never be a first one. Left unenforced, that configuration fails later and blames the wrong thing: on a multi-organization posture it ends as a complaint that the deployment -"requires a licence" — when the licence was the one thing that was never +"requires a license" — when the license was the one thing that was never wrong. The runtime refuses earlier instead, and says which two settings disagree. Because the refusal is taken before any validation attempt, a refused boot sends **no packets at all**. @@ -74,8 +74,8 @@ If you run the **composed** shape — a published app booted as this deployment's app, with several organizations sharing one database behind the isolation wall — then air-gap licensing is not optional there either. That mode declines the marketplace and cloud-connection surfaces outright, so the -binding that an online licence would need can never happen on it. It requires -an air-gap licence and `off`, both enforced at startup, and it has its own +binding that an online license would need can never happen on it. It requires +an air-gap license and `off`, both enforced at startup, and it has its own environment template in the deploy bundle. Do not adapt the single-environment template to reach it; copy the composed one, so every value it decides comes with it. @@ -101,7 +101,7 @@ air-gapped network it may be the only copy you have to roll back to. ## What still has to be reachable -Air-gapped means no traffic is required *by ObjectOS itself* — no licence +Air-gapped means no traffic is required *by ObjectOS itself* — no license validation, no marketplace, no update ping. It does not mean the deployment has no network. Inside your perimeter it still needs: @@ -112,11 +112,11 @@ has no network. Inside your perimeter it still needs: OIDC, that identity provider has to be reachable from this network, or you use local accounts instead. -The licence itself carries an expiry, checked locally at each boot — there is +The license itself carries an expiry, checked locally at each boot — there is no phone-home, so plan renewal as a delivery rather than as something the runtime will fetch. ## Next -- [Docker](/docs/deploy/docker) — verification, licence, scaling and upgrade in full. +- [Docker](/docs/deploy/docker) — verification, license, scaling and upgrade in full. - [Upgrade](/docs/operate/upgrade) and [Backup](/docs/operate/backup) — the procedures the steps above summarise. diff --git a/content/docs/deploy/docker.mdx b/content/docs/deploy/docker.mdx index dc34b67..3d225ea 100644 --- a/content/docs/deploy/docker.mdx +++ b/content/docs/deploy/docker.mdx @@ -11,7 +11,7 @@ page is the decision map for what that bundle configures. ## What you are given -Your licence comes with **registry credentials** for the private runtime +Your license comes with **registry credentials** for the private runtime image. Signing in requires a token with package **read** access; there is no public image and no source build: the image is produced by our release pipeline from sources that are not distributed, so `docker build` is not a @@ -67,28 +67,28 @@ questions: The bundle README carries the verification commands with the identity and issuer to check against, matched to your release. -## Licence +## License -**A licence is part of the happy path.** How the runtime behaves without one +**A license is part of the happy path.** How the runtime behaves without one depends on what you asked it to be: -- **Single-organization, no licence** — the deployment runs with Community +- **Single-organization, no license** — the deployment runs with Community behaviour: the open MCP server, bring your own AI. Nothing bricks. - **Single-organization, licensed** — the in-product AI is unlocked on your own infrastructure: the AI Builder and "ask your data". -- **Multi-organization (a walled tenancy posture), no licence** — the runtime - **exits at startup**, naming the licence. Isolation between organizations +- **Multi-organization (a walled tenancy posture), no license** — the runtime + **exits at startup**, naming the license. Isolation between organizations is a licensed capability, and a deployment that asked for isolation must never serve traffic pretending to have it. There is a second constraint on the same line, and it is the one that -surprises people: **a licence has a mode, and the mode is coupled to your -cloud posture.** An ordinary licence is validated online against a control -plane; an air-gap licence is verified locally and needs no network at all. -Pairing an ordinary licence with "no control plane" is not a degraded state, +surprises people: **a license has a mode, and the mode is coupled to your +cloud posture.** An ordinary license is validated online against a control +plane; an air-gap license is verified locally and needs no network at all. +Pairing an ordinary license with "no control plane" is not a degraded state, it is an impossible one — and it is **refused at startup**, naming both settings, rather than failing later with a misleading complaint about the -licence. [Air-gapped](/docs/deploy/air-gapped) has the full matrix of +license. [Air-gapped](/docs/deploy/air-gapped) has the full matrix of supported combinations. ### A walled deployment must declare two more decisions @@ -169,6 +169,6 @@ Multi-node and high availability are Enterprise capabilities — see ## Next - [Kubernetes](/docs/deploy/kubernetes) — the same properties, on an orchestrator. -- [Air-gapped](/docs/deploy/air-gapped) — the licence mode for disconnected sites. +- [Air-gapped](/docs/deploy/air-gapped) — the license mode for disconnected sites. - [Production Readiness](/docs/operate/production) — pre-flight checklist. - [Observability](/docs/operate/observability) — logs, metrics, audit. diff --git a/content/docs/deploy/index.mdx b/content/docs/deploy/index.mdx index db7fb84..e58ecd8 100644 --- a/content/docs/deploy/index.mdx +++ b/content/docs/deploy/index.mdx @@ -5,7 +5,7 @@ description: Run ObjectOS Self-Managed from the licensed runtime image — diges --- ObjectOS Self-Managed is delivered as a **licensed, pre-built runtime image**. -You do not build it, and there is no public image to pull: your licence comes +You do not build it, and there is no public image to pull: your license comes with registry credentials, and the release notes name the exact image digest to run. (ObjectOS is a commercial product — see [License & Pricing](/docs/resources/license). If you are looking for a free @@ -17,8 +17,8 @@ different product.) | Decision | What it means | |---|---| | **Which bytes run** | The image is pinned **by digest**, never by a tag. A tag can be re-pointed; a digest is the same bytes forever. | -| **Which licence** | A licence is part of the happy path, not an add-on. Some deployment shapes *refuse to start* without one. | -| **Which cloud posture** | Whether this runtime talks to a control plane. This is **coupled to the licence mode**, and unsupported combinations are refused at startup rather than degrading quietly. | +| **Which license** | A license is part of the happy path, not an add-on. Some deployment shapes *refuse to start* without one. | +| **Which cloud posture** | Whether this runtime talks to a control plane. This is **coupled to the license mode**, and unsupported combinations are refused at startup rather than degrading quietly. | The last two are one decision in practice, and getting them wrong is a boot failure, not a slow leak. [Air-gapped](/docs/deploy/air-gapped) states the @@ -32,7 +32,7 @@ templates for each supported shape, and a README with the exact commands for your version — including the registry reference, the digest, and the supply-chain verification steps. -These pages carry the **decisions** — why a digest, which licence mode for +These pages carry the **decisions** — why a digest, which license mode for which shape, what multi-node makes mandatory, what the runtime refuses. The bundle carries the **values you copy**. When the two ever seem to disagree, the bundle that shipped with your image is right: it is versioned with the @@ -56,6 +56,6 @@ checks before it serves anything. ## Guides -- [Docker](/docs/deploy/docker) — the supported stack: pull, verify, licence, run, scale, upgrade. +- [Docker](/docs/deploy/docker) — the supported stack: pull, verify, license, run, scale, upgrade. - [Kubernetes](/docs/deploy/kubernetes) — the properties any orchestrator must preserve. -- [Air-gapped](/docs/deploy/air-gapped) — air-gap as a **licence mode**, and the combinations that are refused at startup. +- [Air-gapped](/docs/deploy/air-gapped) — air-gap as a **license mode**, and the combinations that are refused at startup. diff --git a/content/docs/deploy/kubernetes.mdx b/content/docs/deploy/kubernetes.mdx index 793159c..7e5e7f0 100644 --- a/content/docs/deploy/kubernetes.mdx +++ b/content/docs/deploy/kubernetes.mdx @@ -12,7 +12,7 @@ either depends on or enforces. Reproduce the properties; the YAML around them is yours. Everything on [Docker](/docs/deploy/docker) applies unchanged: same image, -same licence rules, same cloud posture rules. Nothing about the orchestrator +same license rules, same cloud posture rules. Nothing about the orchestrator changes what the deployment is entitled to. ## Properties to preserve @@ -61,9 +61,9 @@ Multi-replica is not a matter of raising the replica count: So the secret material belongs in a `Secret` mounted identically by every pod — never generated per pod, never templated from a pod-unique value. -### Licence and cloud posture are deployment-wide +### License and cloud posture are deployment-wide -The licence key, the licence mode, and the cloud-posture setting are part of +The license key, the license mode, and the cloud-posture setting are part of the deployment's identity. They must be the same in every replica, and their supported combinations are checked **at startup** — a mismatched pod does not run degraded, it exits. See [Air-gapped](/docs/deploy/air-gapped) for the diff --git a/content/docs/index.mdx b/content/docs/index.mdx index 0a333f8..127a92e 100644 --- a/content/docs/index.mdx +++ b/content/docs/index.mdx @@ -70,18 +70,11 @@ On either edition, the ontology: your objects, fields, relations, actions, permissions, flows and agent definitions are ordinary ObjectStack metadata, and you can export them and run them on the open-source ObjectStack runtime. On **ObjectOS Enterprise**, everything else stays with you too: +[Data residency](/docs/reference/security#data-residency) lists each class +of data, where it lives, and whether it leaves your network. -| Asset | Where it lives | -|---|---| -| Business data | **Your** database (Postgres, SQLite, MySQL, MongoDB) | -| User identities & sessions | **Your** database | -| Audit log | **Your** database | -| Files | **Your** storage (local disk, S3, or S3-compatible like R2/MinIO) | -| Secrets | **Your** secret manager | -| The runtime itself | **Your** servers or containers | - -Self-managed ObjectOS validates its licence online; Enterprise air-gapped -licences validate offline, so a network with no internet access is a +Self-managed ObjectOS validates its license online; Enterprise air-gapped +licenses validate offline. A network with no internet access is therefore a first-class Enterprise deployment target — see [Air-gapped](/docs/deploy/air-gapped). @@ -94,7 +87,7 @@ matches how you use ObjectOS: |---|---|---| | **An everyday user** | Work in the apps: records, views, dashboards, approvals | [Use](/docs/use) | | **A builder** | Create apps: data models, interfaces, automation, AI agents | [Build](/docs/build) | -| **An administrator** | Run the platform: users, permissions, settings, deployment | [Administration](/docs/configure), [Deploy](/docs/deploy), [Operate](/docs/operate/production) | +| **An administrator** | Run the platform: users, permissions, settings, deployment | [Configure](/docs/configure), [Deploy](/docs/deploy), [Operate](/docs/operate/production) | ## Where to go next diff --git a/content/docs/operate/troubleshooting.mdx b/content/docs/operate/troubleshooting.mdx index 6ad006b..65c47b3 100644 --- a/content/docs/operate/troubleshooting.mdx +++ b/content/docs/operate/troubleshooting.mdx @@ -18,8 +18,8 @@ The refusals you are most likely to meet: | The log says | What it means | |---|---| -| Two settings are named together as an unsupported pairing | A licence mode and a cloud posture that cannot work together — most often an ordinary, online-validated licence on a deployment configured with no control plane. [Air-gapped](/docs/deploy/air-gapped) has the supported matrix. | -| A walled deployment requires a licence | A multi-organization tenancy posture was asked for without a licence. Isolation is a licensed capability; the runtime will not serve traffic while pretending to have it. | +| Two settings are named together as an unsupported pairing | A license mode and a cloud posture that cannot work together — most often an ordinary, online-validated license on a deployment configured with no control plane. [Air-gapped](/docs/deploy/air-gapped) has the supported matrix. | +| A walled deployment requires a license | A multi-organization tenancy posture was asked for without a license. Isolation is a licensed capability; the runtime will not serve traffic while pretending to have it. | | A decision was not made | On a multi-organization posture, the new-sign-up membership policy and the set of mounted AI agents have no safe default. Declaring either available value is accepted; leaving it unset is not. | | A retired variable is set | An old artifact-selection variable refuses the boot and names its replacement. See [retired names](/docs/reference/environment-variables#retired-names). | | The cluster secret is missing | Turning on a cluster driver makes one shared secret key mandatory across every replica. | diff --git a/content/docs/reference/environment-variables.mdx b/content/docs/reference/environment-variables.mdx index 7c6e50e..c424731 100644 --- a/content/docs/reference/environment-variables.mdx +++ b/content/docs/reference/environment-variables.mdx @@ -32,7 +32,7 @@ differ, the template shipped with your image wins. | `OS_DB_USER`, `OS_DB_PASSWORD`, `OS_DB_NAME` | Provision the **bundled** database service in the shipped Compose stack. The credentials inside `OS_DATABASE_URL` must match them. Irrelevant once you use a managed database. | | `AI_GATEWAY_API_KEY` | The AI provider credential behind the "ask your data" agent. | -## Licence and cloud posture +## License and cloud posture These two are a pair. Setting one without reading the other is the most expensive mistake on this page, because unsupported pairings are **refused at @@ -40,17 +40,17 @@ startup** rather than degraded. | Variable | Decides | |---|---| -| `OS_LICENSE_KEY` | The deployment's entitlement. A licence also has a **mode** — ordinary licences are validated online against a control plane; air-gap licences are verified locally with no network traffic at all. The mode is a property of the licence you were issued, not something you configure here. | -| `OS_CLOUD_URL` | Whether this deployment has a control plane, and which one. `off` (and its aliases `none`, `local`, `disabled`) means **no control plane**: no marketplace, and no licence validation either. | +| `OS_LICENSE_KEY` | The deployment's entitlement. A license also has a **mode** — ordinary licenses are validated online against a control plane; air-gap licenses are verified locally with no network traffic at all. The mode is a property of the license you were issued, not something you configure here. | +| `OS_CLOUD_URL` | Whether this deployment has a control plane, and which one. `off` (and its aliases `none`, `local`, `disabled`) means **no control plane**: no marketplace, and no license validation either. | Two consequences follow, and both surprise people: - **Unset is not "off".** An unset `OS_CLOUD_URL` resolves to the *public* control plane. It was never a neutral value — it selects the connected behaviour. A deployment meant to talk to nobody must say `off` out loud. -- **An ordinary licence with `off` is refused at startup**, naming both +- **An ordinary license with `off` is refused at startup**, naming both variables. It is not a degraded state but an impossible one: an online - licence is validated by an exchange that needs a control plane, so with none + license is validated by an exchange that needs a control plane, so with none configured the exchange is never made — not on this boot and not on a later one — and the grace window never starts, because grace runs from the last *successful* validation. @@ -67,7 +67,7 @@ value is accepted; not deciding is not. | Variable | Decides | |---|---| -| `OS_TENANCY_POSTURE` | Whether organizations are isolated from one another. The walled values are `isolated` and `group`. A walled posture **requires a licence**: without one the runtime exits at startup rather than serve traffic while pretending to be isolated. | +| `OS_TENANCY_POSTURE` | Whether organizations are isolated from one another. The walled values are `isolated` and `group`. A walled posture **requires a license**: without one the runtime exits at startup rather than serve traffic while pretending to be isolated. | | `OS_AUTH_MEMBERSHIP_POLICY` | What a fresh sign-up joins. `auto` binds every new user to the deployment's default organization — right for a single-organization box, wrong where a membership *is* the tenant boundary. `invite-only` grants membership only by an explicit act: creating a workspace, accepting an invitation, an admin, or SSO provisioning. Also settable in **Setup → Authentication → Membership**; setting it here pins it and makes the Setup field read-only. | | `OS_AI_STUDIO_AGENTS` | Which AI agents are mounted, as a comma-separated subset of `ask` and `build`. Unset mounts **both**. `ask` reads; `build` **authors metadata** — and metadata is scoped to the deployment, not to an organization, so on a shared database one customer's `build` turn rewrites the schema every other customer runs on. The AI seat does not cover this: it is one flag for both agents. A misspelled value fails the boot rather than quietly falling back to mounting both. | @@ -177,5 +177,5 @@ be removed in a future major version. |---|---|---| | `OS_PORT` | `PORT` | Emits a one-shot deprecation warning. | | `OS_AUTH_SECRET` | `AUTH_SECRET` | Emits a one-shot deprecation warning. | -| `OS_TENANCY_POSTURE` | `OS_MULTI_ORG_ENABLED` | The posture derives from the legacy boolean only when `OS_TENANCY_POSTURE` is unset: `true` there means isolated. Declare the posture directly — it is the variable the licence and startup checks are written against. | +| `OS_TENANCY_POSTURE` | `OS_MULTI_ORG_ENABLED` | The posture derives from the legacy boolean only when `OS_TENANCY_POSTURE` is unset: `true` there means isolated. Declare the posture directly — it is the variable the license and startup checks are written against. | | `OS_MULTI_ORG_ENABLED` | `OS_MULTI_TENANT` | | diff --git a/content/docs/reference/rest-api.mdx b/content/docs/reference/rest-api.mdx index 4897115..54ab5de 100644 --- a/content/docs/reference/rest-api.mdx +++ b/content/docs/reference/rest-api.mdx @@ -35,8 +35,9 @@ row-level security + field-level security inside the data engine. ## Restricting the API surface -The auto-generated endpoints are governed per object, so you can take an -object off the API or make it read-only without writing any route code: +The auto-generated endpoints are governed per object, in its `enable` block, +so you can take an object off the API or make it read-only without writing +any route code: - **`apiEnabled`** (boolean, **default `true`**) — set `false` to omit the object from the REST surface entirely. Requests to its routes 404. @@ -46,17 +47,19 @@ object off the API or make it read-only without writing any route code: `aggregate`, `history`, `search`, `restore`, `purge`, `import`, `export`. ```ts +import { ObjectSchema } from '@objectstack/spec/data'; + // A read-only reference object, never writable over the API: -defineObject({ +ObjectSchema.create({ name: 'exchange_rate', - apiMethods: ['get', 'list'], + enable: { apiMethods: ['get', 'list'] }, // ...fields }) // An internal object that the API never exposes: -defineObject({ +ObjectSchema.create({ name: 'sync_cursor', - apiEnabled: false, + enable: { apiEnabled: false }, // ...fields }) ``` diff --git a/content/docs/reference/security.mdx b/content/docs/reference/security.mdx index e5fc364..b6ec843 100644 --- a/content/docs/reference/security.mdx +++ b/content/docs/reference/security.mdx @@ -10,8 +10,8 @@ answer "is it safe to bring this in?" ObjectOS runs as a single Node.js process that talks to one database — inside **your** network on **ObjectOS Enterprise**, in ours on **ObjectOS -Cloud**. Self-managed, its only outbound call of its own is licence -validation, and Enterprise air-gapped licences validate offline. The blast +Cloud**. Self-managed, its only outbound call of its own is license +validation, and Enterprise air-gapped licenses validate offline. The blast radius of a compromise is the data on the database it connects to — nothing more. @@ -27,14 +27,16 @@ open-source ObjectStack runtime. | Business records | Your database | **No** | | User accounts, sessions, OAuth tokens | Your database | **No** | | Audit log | Your database | **No** | -| Settings, API keys | Your database / your secret manager | **No** | -| Uploaded files | Your disk or S3-compatible bucket | **No** | +| Settings, API keys, secrets | Your database / your secret manager | **No** | +| Uploaded files | Your disk or an S3-compatible bucket (S3, R2, MinIO) | **No** | +| The compiled app definition (`objectstack.json`) | A file on disk, or fetched from your control plane | Optional | +| The runtime itself | Your servers or containers | **No** | | Telemetry / usage data | — | **None collected** | Beyond the integrations you explicitly configure (OIDC discovery, email provider, AI provider, webhook targets, external storage), the one outbound -call self-managed ObjectOS makes on its own is **licence validation** — -Enterprise air-gapped licences validate offline, so an air-gapped +call self-managed ObjectOS makes on its own is **license validation** — +Enterprise air-gapped licenses validate offline, so an air-gapped deployment makes none. See [License & Pricing](/docs/resources/license#faq). ## Encryption @@ -165,8 +167,8 @@ Required inbound: - HTTPS from your ingress / load balancer to ObjectOS on `:3000` (default). Required outbound: -- Licence validation, for self-managed ObjectOS — Enterprise air-gapped - licences validate offline and need no egress for it. +- License validation, for self-managed ObjectOS — Enterprise air-gapped + licenses validate offline and need no egress for it. Required outbound only if you configure these features: - Your database (Postgres / Mongo / Turso / …). @@ -274,8 +276,7 @@ construction: When you configure a provider (OpenAI, Anthropic, …), only the following leaves your network: -- The conversation history the model needs (subject to your AI service - `redact` config — see [Configure → AI](/docs/configure/ai)) +- The conversation history the model needs - Tool definitions (names, JSON schemas — no record data) - Tool outputs that the model needs to continue (e.g. a query result the user explicitly asked for) diff --git a/content/docs/resources/changelog.mdx b/content/docs/resources/changelog.mdx index 12cebcc..8f64874 100644 --- a/content/docs/resources/changelog.mdx +++ b/content/docs/resources/changelog.mdx @@ -46,11 +46,13 @@ major are published with that major's Released ObjectOS versions and their CHANGELOG entries are published at: - **npm**: [`@objectstack/runtime`](https://www.npmjs.com/package/@objectstack/runtime) -- **GitHub**: [github.com/objectstack-ai/objectos/releases](https://github.com/objectstack-ai/objectos/releases) +- **GitHub**: [github.com/objectstack-ai/objectstack/releases](https://github.com/objectstack-ai/objectstack/releases) - **Source CHANGELOG**: [`CHANGELOG.md`](https://github.com/objectstack-ai/objectstack/blob/main/CHANGELOG.md) - **Long-form release notes**: [`RELEASE_NOTES.md`](https://github.com/objectstack-ai/objectstack/blob/main/RELEASE_NOTES.md) -Subscribe to releases on GitHub to get notified. +To be notified, watch +[github.com/objectstack-ai/objectstack](https://github.com/objectstack-ai/objectstack) +→ Releases on GitHub. ## Recent highlights diff --git a/content/docs/resources/faq.mdx b/content/docs/resources/faq.mdx index 7869d56..025333f 100644 --- a/content/docs/resources/faq.mdx +++ b/content/docs/resources/faq.mdx @@ -6,44 +6,52 @@ description: Common questions about ObjectOS — getting started, databases and ## Getting started -**Q: What's the absolute fastest way to try ObjectOS?** -A: Sign in to [ObjectOS Cloud](https://www.objectos.ai) — hosted in the +### What's the absolute fastest way to try ObjectOS? + +Sign in to [ObjectOS Cloud](https://www.objectos.ai) — hosted in the browser, nothing to install. To run the open-source ObjectStack runtime on your own machine instead, `npm i -g @objectstack/cli && os start` and open http://localhost:3000. See [Quickstart](/docs/quickstart). -**Q: Do I need Docker?** -A: No. ObjectOS Cloud needs nothing installed. For the open runtime, Node +### Do I need Docker? + +No. ObjectOS Cloud needs nothing installed. For the open runtime, Node 22+ and the CLI are enough; Docker is the recommended shape for a self-managed production deployment. -**Q: Do I need a database?** -A: No, not to start — ObjectOS Cloud is managed for you, and the open +### Do I need a database? + +No, not to start — ObjectOS Cloud is managed for you, and the open runtime uses local SQLite by default. A self-managed deployment swaps in Postgres / MySQL / Turso / Mongo when it goes to production. -**Q: Do I need an account / cloud service?** -A: ObjectOS Cloud *is* the account: sign in and build. ObjectOS Enterprise +### Do I need an account / cloud service? + +ObjectOS Cloud *is* the account: sign in and build. ObjectOS Enterprise is self-managed and needs no cloud service to run — connecting it to a control plane is optional. The open-source ObjectStack runtime needs no account at all. ## Architecture -**Q: Can I use Postgres / MySQL / MongoDB?** -A: Yes — Postgres, MySQL, SQLite, Turso/libSQL, and MongoDB are +### Can I use Postgres / MySQL / MongoDB? + +Yes — Postgres, MySQL, SQLite, Turso/libSQL, and MongoDB are supported drivers. See [Runtime Configuration](/docs/configure/runtime). -**Q: Can I disable the UI / Account portals and use only the REST API?** -A: Yes. Run `os start --no-ui` or set the corresponding flags. The +### Can I disable the UI / Account portals and use only the REST API? + +Yes. Run `os start --no-ui` or set the corresponding flags. The REST API is the same whether the UIs are mounted or not. -**Q: Can I use my own front-end instead of the generated UI?** -A: Yes. The generated UI uses the same `/api/v1/*` endpoints you'd call from +### Can I use my own front-end instead of the generated UI? + +Yes. The generated UI uses the same `/api/v1/*` endpoints you'd call from your own code. Use `@objectstack/client` SDK or any HTTP client. -**Q: Does ObjectOS support GraphQL?** -A: No. REST is the API surface. The GraphQL endpoint was removed in +### Does ObjectOS support GraphQL? + +No. REST is the API surface. The GraphQL endpoint was removed in ObjectStack 17.0 — it had never been implemented behind the route, and `/graphql` now returns 404. Use the generated REST API with the [ObjectQL](/docs/reference/objectql) query language (over `?filter=` / @@ -52,8 +60,9 @@ system that speaks GraphQL is unaffected: `graphql` remains a valid protocol for an *external* datasource, which is a client of someone else's API rather than a surface ObjectOS serves. -**Q: How is multi-tenancy handled?** -A: One deployment serves one app, against one database — see +### How is multi-tenancy handled? + +One deployment serves one app, against one database — see [Architecture](/docs/architecture). Several organizations can share that deployment: a **walled** tenancy posture (`OS_TENANCY_POSTURE`) puts up the per-organization isolation wall, and it is a licensed capability the runtime @@ -65,72 +74,84 @@ separation: a customer that must have its own database gets its own deployment. See [Multi-organization deployments](/docs/reference/environment-variables#multi-organization-deployments). -**Q: Can ObjectOS run in a serverless / Lambda environment?** -A: The runtime is a long-lived Node process — designed for containers +### Can ObjectOS run in a serverless / Lambda environment? + +The runtime is a long-lived Node process — designed for containers or VMs, not stateless functions. The kernel is built at startup and the process reports ready only afterwards; that warm in-process state, and the Better Auth session model, are what a per-invocation function cannot keep. -**Q: Does it scale horizontally?** -A: Yes. Run multiple instances behind a load balancer. Sessions live in +### Does it scale horizontally? + +Yes. Run multiple instances behind a load balancer. Sessions live in the database (not in-memory), so any instance can serve any request. Use Redis for shared rate limiting and queue if you enable those capabilities. ## Data & migrations -**Q: How are schema migrations handled?** -A: The driver syncs the database schema to your declared objects on +### How are schema migrations handled? + +The driver syncs the database schema to your declared objects on boot. For Postgres, that's `CREATE TABLE` / `ALTER TABLE` statements. For controlled migrations in regulated environments, set `OS_SKIP_SCHEMA_SYNC=1` and manage DDL yourself. -**Q: What happens to data when I rename a field?** -A: A rename is a destructive change at the data layer (it looks like +### What happens to data when I rename a field? + +A rename is a destructive change at the data layer (it looks like "drop old column, add new column"). Use `os diff` to detect this and add a migration step (rename column in DB before deploying the new artifact). -**Q: Can I import data from CSV / Excel / Salesforce?** -A: CSV: yes, via `os data create` in a loop or the bulk upload in the UI. +### Can I import data from CSV / Excel / Salesforce? + +CSV: yes, via `os data create` in a loop or the bulk upload in the UI. Salesforce: best path today is to export to CSV and import. Native connectors are on the roadmap. -**Q: Will upgrading ObjectOS lose my data?** -A: No. Patch and minor upgrades are non-destructive. Major upgrades +### Will upgrading ObjectOS lose my data? + +No. Patch and minor upgrades are non-destructive. Major upgrades (e.g. 4 → 5) document required migrations explicitly. Back up first — [Backup & DR](/docs/operate/backup). ## Permissions & multi-tenancy -**Q: How do I do row-level security?** -A: Declare a sharing rule (declarative, like Salesforce) or a CEL +### How do I do row-level security? + +Declare a sharing rule (declarative, like Salesforce) or a CEL predicate on an object's `recordAccess` config. The security plugin injects the corresponding filter on every query. See [Permissions](/docs/configure/permissions). -**Q: Can I make some fields invisible to certain users?** -A: Yes — field-level security in permission sets. Hide or read-only, +### Can I make some fields invisible to certain users? + +Yes — field-level security in permission sets. Hide or read-only, per field per permission set. Enforced uniformly across REST, ObjectQL, and the UI. See [Permission Sets](/docs/configure/permissions/permission-sets). -**Q: How do I integrate Okta / Entra / Keycloak?** -A: OIDC. Configure the discovery URL + client id/secret in **Setup → +### How do I integrate Okta / Entra / Keycloak? + +OIDC. Configure the discovery URL + client id/secret in **Setup → Authentication** (or via env). Provider callback URL is `/api/v1/auth/oauth2/callback/<provider-id>`. See [Authentication](/docs/configure/authentication). ## Integrations -**Q: Can I send webhooks?** -A: Yes — enable `webhooks` in `requires`. ObjectOS uses a persistent +### Can I send webhooks? + +Yes — enable `webhooks` in `requires`. ObjectOS uses a persistent outbox with HMAC-SHA256 signing. See [Webhooks](/docs/configure/webhooks). -**Q: Can I integrate with Zapier / Make / n8n?** -A: Yes — webhooks for outbound and the REST API + API keys for inbound. +### Can I integrate with Zapier / Make / n8n? + +Yes — webhooks for outbound and the REST API + API keys for inbound. Native connectors for popular iPaaS tools are on the roadmap. -**Q: Can AI agents call my ObjectOS?** -A: Yes, via MCP (`@objectstack/mcp`) — exposes objects (and, increasingly, +### Can AI agents call my ObjectOS? + +Yes, via MCP (`@objectstack/mcp`) — exposes objects (and, increasingly, actions) as MCP tools that Claude Desktop, IDEs, Claude Code, or any other MCP client can use. This is the **AI path on the free, open-source ObjectStack framework** (bring your own model); the in-product AI Builder / "ask your data" @@ -138,48 +159,56 @@ assistant is what the paid ObjectOS editions add. See [AI Service](/docs/configu ## Customization -**Q: Can I write custom plugins?** -A: Yes — plugins follow a simple DI + lifecycle pattern +### Can I write custom plugins? + +Yes — plugins follow a simple DI + lifecycle pattern (`init → start → destroy`). See `@objectstack/plugin-*` packages on GitHub for examples. -**Q: Can I customize how ObjectOS looks?** -A: Branding (logo, accent color, default theme) is in **Setup → +### Can I customize how ObjectOS looks? + +Branding (logo, accent color, default theme) is in **Setup → System Settings**. Deep UI customization means forking `@objectstack/client-react` or building your own front-end against the REST API. -**Q: Can I add languages other than English?** -A: Yes — i18n is first-class. Use `os i18n extract` / `os i18n check` +### Can I add languages other than English? + +Yes — i18n is first-class. Use `os i18n extract` / `os i18n check` and ship a translation bundle. ## Operations -**Q: What's the recommended production deployment?** -A: Docker (or Kubernetes for multi-pod) + managed Postgres + S3 or R2 +### What's the recommended production deployment? + +Docker (or Kubernetes for multi-pod) + managed Postgres + S3 or R2 for files + your secret manager for `OS_AUTH_SECRET`. See [Production Readiness](/docs/operate/production). -**Q: Does ObjectOS have a status page?** -A: For a self-managed (Enterprise) deployment, status is your concern — point +### Does ObjectOS have a status page? + +For a self-managed (Enterprise) deployment, status is your concern — point your monitor at `/api/v1/health` for liveness and `/api/v1/ready` for readiness, the pair [Docker](/docs/deploy/docker) and [Kubernetes](/docs/deploy/kubernetes) wire up. For hosted services, see [status.objectstack.ai](https://status.objectstack.ai). -**Q: What metrics should I monitor?** -A: 5xx rate, p95 latency, readiness (`/api/v1/ready`). There is +### What metrics should I monitor? + +5xx rate, p95 latency, readiness (`/api/v1/ready`). There is no auth-failure metric to monitor — review sign-in activity in Setup's `Auth` audit view instead. Minimal Prometheus example in [Observability](/docs/operate/observability). -**Q: How do I take a backup?** -A: Back up the **database** and the **storage bucket** — those hold +### How do I take a backup? + +Back up the **database** and the **storage bucket** — those hold all customer data. ObjectOS itself is stateless. See [Backup](/docs/operate/backup). ## Pricing & legal -**Q: Is ObjectOS free?** -A: ObjectOS is a **commercial product** (there is a free Cloud tier to start +### Is ObjectOS free? + +ObjectOS is a **commercial product** (there is a free Cloud tier to start on). The free, self-hostable platform is the open-source **[ObjectStack framework](https://github.com/objectstack-ai/objectstack)** (Apache-2.0): no seats, no usage tier, no license server — with AI over MCP @@ -187,35 +216,41 @@ on). The free, self-hostable platform is the open-source and official operations, and you pay only for **AI seats** (viewers and non-AI users are free). See [Editions](/docs/resources/license#editions). -**Q: Can I use it in a commercial product I sell?** -A: The **ObjectStack framework** — yes, Apache-2.0 allows commercial use with +### Can I use it in a commercial product I sell? + +The **ObjectStack framework** — yes, Apache-2.0 allows commercial use with no royalty. Redistributing **ObjectOS** itself requires an OEM agreement. See [License & Pricing](/docs/resources/license). -**Q: Do you collect telemetry?** -A: The open-source ObjectStack runtime makes zero outbound calls unless you -configure them (OIDC, email, AI, webhooks). Self-managed ObjectOS -additionally validates its license online (air-gapped Enterprise licenses are -offline-validated). See [Security & Compliance](/docs/reference/security#data-residency). +### Do you collect telemetry? + +The open-source ObjectStack runtime makes zero outbound calls unless you +configure them (OIDC, email, AI, webhooks). Self-managed ObjectOS validates +its license online; Enterprise air-gapped licenses validate offline. See +[Security & Compliance](/docs/reference/security#data-residency). -**Q: Is ObjectOS SOC 2 / ISO 27001 / HIPAA / GDPR compliant?** -A: ObjectOS provides the **primitives** every framework requires (RBAC, +### Is ObjectOS SOC 2 / ISO 27001 / HIPAA / GDPR compliant? + +ObjectOS provides the **primitives** every framework requires (RBAC, audit, encryption-ready, residency). Certification is a property of your **deployment**, not the binary. Many ObjectOS deployments are certified. See [Security & Compliance](/docs/reference/security#compliance-frameworks). ## Getting unstuck -**Q: Something's broken — where do I start?** -A: `os doctor`. It catches 80% of misconfigurations on its own. After +### Something's broken — where do I start? + +`os doctor`. It catches 80% of misconfigurations on its own. After that, [Troubleshooting](/docs/operate/troubleshooting). -**Q: Where do I report a bug?** -A: [GitHub Issues](https://github.com/objectstack-ai/objectos/issues). +### Where do I report a bug? + +[GitHub Issues](https://github.com/objectstack-ai/objectos/issues). Include `os doctor` output. Security issues: **security@objectstack.ai**. -**Q: Where do I get help from humans?** -A: [GitHub Issues](https://github.com/objectstack-ai/objectos/issues), +### Where do I get help from humans? + +[GitHub Issues](https://github.com/objectstack-ai/objectos/issues), the community Discord, or **sales@objectstack.ai** for commercial support. diff --git a/content/docs/resources/glossary.mdx b/content/docs/resources/glossary.mdx index 1e3ccee..e217952 100644 --- a/content/docs/resources/glossary.mdx +++ b/content/docs/resources/glossary.mdx @@ -6,13 +6,6 @@ description: The vocabulary used across ObjectOS and ObjectStack — one definit A single canonical definition for each term used in this documentation. -### Artifact - -A compiled `objectstack.json` file. Self-contained, immutable -description of an app — manifest, objects, views, apps, flows, -permissions, translations. Produced by `os compile`. The thing -ObjectOS actually executes. - ### Action A named operation declared in metadata, invocable via REST @@ -26,12 +19,26 @@ Express, Fastify, Hono, Next.js, Nuxt, SvelteKit, NestJS. Most ObjectOS deployments don't need one; ObjectOS bundles its own HTTP server. +### AI seat + +The unit ObjectOS is priced in, and the only billed seat in every edition. +Viewers, app users who do not use the in-product AI, and developers +building over MCP are free. See +[License & Pricing](/docs/resources/license#editions). + ### App A bundle of objects + views + permissions presented as a single navigable application. Multiple apps can coexist in one runtime (e.g. CRM + Helpdesk + Setup). +### Artifact + +A compiled `objectstack.json` file. Self-contained, immutable +description of an app — manifest, objects, views, apps, flows, +permissions, translations. Produced by `os compile`. The thing +ObjectOS actually executes. + ### Better Auth The auth library powering `@objectstack/plugin-auth`. You don't @@ -168,7 +175,7 @@ The open stack, Apache-2.0: the protocol, microkernel, SDK, CLI and production runtime that execute a business ontology — the `@objectstack/*` npm packages and the runtime image. ObjectOS is the commercial runtime environment built on it; the two are different products under different -licences. +licenses. ### ObjectUI @@ -188,6 +195,15 @@ A framework package that extends the runtime with a capability — `mcp`, etc. Activated via DI + lifecycle hooks (`init → start → destroy`). +### Position + +The job function a user holds — "Sales Manager", "Support Agent", +"Auditor". The single people-grouping concept since ObjectStack 13, and +deliberately flat: reporting structure lives on the business-unit tree +instead. A position targets apps and tabs at an audience, names approvers, +and distributes permission sets to everyone who holds it. See +[Positions](/docs/configure/permissions/positions). + ### Project Old name for **Environment**. It survives in inherited configuration and @@ -209,7 +225,7 @@ The built-in administration app, at `/apps/setup` inside the UI served at `/_console/`, gated by the `setup.access` permission. Manages users, business units, teams, positions, permission sets, sharing rules, record shares and API keys, plus configuration, diagnostics and -integrations. See [Administration](/docs/configure). +integrations. See [Configure](/docs/configure). ### Sharing Rule @@ -230,7 +246,7 @@ it: Setup operates a running deployment (its people, their grants, its configuration), Studio authors what that deployment runs — hence **permissions are designed in Studio, assigned in Setup**. Unlike Setup it is not an app under `/apps/`; the UI serves it as its own -route subtree. See [Administration](/docs/configure#setup-and-studio). +route subtree. See [Configure](/docs/configure#setup-and-studio). ### Surface diff --git a/content/docs/resources/license.mdx b/content/docs/resources/license.mdx index 5d78a1f..1feed41 100644 --- a/content/docs/resources/license.mdx +++ b/content/docs/resources/license.mdx @@ -16,7 +16,7 @@ open-source edition of ObjectOS**. bundle, the `@objectstack/console` package, the same spelling architecture.mdx and configure/system-settings.mdx were deliberately kept at. #79 retired "Console" as the name of the end-user surface (a reader opens ObjectOS, not - "the Console") but kept the names of things the product ships, and a licence + "the Console") but kept the names of things the product ships, and a license enumerates artifacts rather than surfaces. Both occurrences on this page are therefore deliberate — do not rewrite them to "the UI" or to "ObjectOS". */} @@ -126,37 +126,44 @@ inventory. ## FAQ -**Q: Is ObjectOS open source?** -A: No. ObjectOS is a commercial product with no open-source edition. The +### Is ObjectOS open source? + +No. ObjectOS is a commercial product with no open-source edition. The open-source platform it runs on is the **ObjectStack framework** (Apache-2.0) — build and self-host your own apps with it, free. -**Q: Can I use ObjectStack in a closed-source SaaS I sell to my customers?** -A: Yes. Apache-2.0 has no copyleft and no royalty — you don't need to share +### Can I use ObjectStack in a closed-source SaaS I sell to my customers? + +Yes. Apache-2.0 has no copyleft and no royalty — you don't need to share your code, and you owe nothing if your product makes money. -**Q: Can I self-host ObjectOS without paying?** -A: No — self-managed ObjectOS (Business Self-Managed or Enterprise) is +### Can I self-host ObjectOS without paying? + +No — self-managed ObjectOS (Business Self-Managed or Enterprise) is licensed. Free self-hosting is the ObjectStack framework, which is the same open mechanism with MCP-only AI. -**Q: Does the software stop working if I don't renew?** -A: It never bricks. Business Self-Managed degrades to the free-mechanism +### Does the software stop working if I don't renew? + +It never bricks. Business Self-Managed degrades to the free-mechanism behavior (paid features switch off; your data and runtime keep working). Enterprise keeps the **last paid version perpetually** — you lose updates, new AI capabilities, and support, not your system. -**Q: Does ObjectOS phone home?** -A: ObjectOS Self-Managed validates its license online (Enterprise air-gapped -licenses are offline-validated). The open-source ObjectStack runtime has no +### Does ObjectOS phone home? + +Self-managed ObjectOS validates its license online; Enterprise air-gapped +licenses validate offline. The open-source ObjectStack runtime has no telemetry, no license check, and no update ping. -**Q: What if I want a contractual warranty on the free runtime?** -A: That's the **ObjectOS Runtime Subscription** — a support contract on the +### What if I want a contractual warranty on the free runtime? + +That's the **ObjectOS Runtime Subscription** — a support contract on the open-source ObjectStack runtime (official distribution, SLA, security-patch priority) with zero feature difference. Contact [sales@objectstack.ai](mailto:sales@objectstack.ai). -**Q: Will the open-source framework always be free?** -A: Yes. Apache-2.0 grants are irrevocable, and everything already released +### Will the open-source framework always be free? + +Yes. Apache-2.0 grants are irrevocable, and everything already released stays licensed as released. diff --git a/content/docs/resources/support.mdx b/content/docs/resources/support.mdx index 6838aaa..b1b538d 100644 --- a/content/docs/resources/support.mdx +++ b/content/docs/resources/support.mdx @@ -13,7 +13,7 @@ description: Where to get help, how to report bugs, response expectations. | Found a security vulnerability | **security@objectstack.ai** — do **not** open a public issue | | Need commercial support / SLA | **sales@objectstack.ai** | | Want to chat with users + maintainers | [Discord](https://discord.gg/objectstack) (community-run) | -| Want to follow releases | Watch [github.com/objectstack-ai/objectos](https://github.com/objectstack-ai/objectos) → Releases | +| Want to follow releases | [Release notes](/docs/resources/changelog#release-notes) — where each release is published; to be notified, watch [github.com/objectstack-ai/objectstack](https://github.com/objectstack-ai/objectstack) → Releases | | Want a paid feature implemented | **sales@objectstack.ai** — see [Commercial support & services](#commercial-support--services) | ## Filing a good bug report diff --git a/content/docs/why.mdx b/content/docs/why.mdx index e7b213a..10d3602 100644 --- a/content/docs/why.mdx +++ b/content/docs/why.mdx @@ -55,7 +55,7 @@ Common scenarios where it's a fit: | Replacing a Retool / Appsmith app because data sovereignty came up in security review | ObjectOS Enterprise runs in your VPC; data never leaves | | Building a compliance / risk / vendor management tool for a regulated business | Audit log, RBAC, field security, row-level isolation are first-class — and every AI-driven change is itself an audit entry | | Standing up an internal admin for a SaaS product | One Node process, slots in next to your existing services — or a Cloud tenant with nothing to run | -| Air-gapped or on-prem deployment for an enterprise customer | A first-class ObjectOS Enterprise target: air-gapped licences validate offline, and the AI service can point at a local model | +| Air-gapped or on-prem deployment for an enterprise customer | A first-class ObjectOS Enterprise target: air-gapped licenses validate offline, and the AI service can point at a local model | | Multi-tenant internal portal (several organizations, one deployment) | A walled tenancy posture puts up the per-organization isolation wall, enforced inside the runtime — an Enterprise capability, see [License & Pricing](/docs/resources/license). It separates the organizations' data and memberships, not their schema: they share one database and one metadata set | | You want your users to "vibe-code" their own extensions safely | The AI Builder + HITL approval queue + audit log are the whole point | From c5740f9cac0a9cc04298f23c727cacd578d56a72 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 6 Oct 2026 11:22:45 +0000 Subject: [PATCH 3/5] docs: record the measured search cold start and the smoke search requests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- .github/workflows/ci.yml | 13 ++++++++----- apps/docs/app/api/search/route.ts | 13 +++++++++---- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1858d86..c257e7a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -441,11 +441,14 @@ jobs: # package the step above produced with `--skipNextBuild`. Since #262 # removed the `main`-only condition from that packaging step, that # package exists on every pull request, so this step adds a preview boot - # and four fetches and nothing else. Measured in this repo's container, - # against the package already sitting in the tree: `Ready on` at 41 s, - # the smoke run itself 1 s. No `opennextjs-cloudflare build`, no - # `next build`, no Cloudflare credentials — `wrangler dev` serves the - # Worker locally under real workerd. + # and a few fetches and nothing else: four pages and their control, and + # since #301 one `/api/search` per locale and its control. Measured in + # this repo's container, against the package already sitting in the + # tree: `Ready on` at 41 s, the smoke run itself 1 s, plus 0.6–0.9 s for + # each locale's first search, which builds that locale's index. No + # `opennextjs-cloudflare build`, no `next build`, no Cloudflare + # credentials — `wrangler dev` serves the Worker locally under real + # workerd. # # ## Why it runs LAST, after the artifact upload # diff --git a/apps/docs/app/api/search/route.ts b/apps/docs/app/api/search/route.ts index 59332c5..46004af 100644 --- a/apps/docs/app/api/search/route.ts +++ b/apps/docs/app/api/search/route.ts @@ -137,10 +137,15 @@ async function indexes(locale: Locale): Promise<AdvancedIndex[]> { * * `createFromSource` with i18n builds every locale's index on the first search * in any locale, so the first reader after a cold start paid for all eight: - * loading the compiled body of every page in every locale (about 600) and - * segmenting four CJK corpora. #296 measured that first search at 7.2–7.4 s - * under Node and about 6.2 s under workerd, against 30–60 ms warm. Building - * only the requested locale makes the first search pay for one. + * loading the compiled body of all 79 pages eight times over and segmenting + * four CJK corpora. Building only the requested locale makes the first search + * pay for one. Measured on one box, `main` @ `601bb37` against this change, + * first search after boot: 8.7–10.1 s → 0.6–1.4 s under `next start`, and + * 4.7–5.1 s → 0.8–1.0 s under workerd (`opennextjs-cloudflare preview`). The + * first search in each further locale costs that locale's build, 0.5–2.2 s; + * a warm one, 23–222 ms either way. The results are identical: with the + * weights above set to 1, every locale and query compared byte for byte + * against `createFromSource` in one process. * * Built in the request, not at build time, on purpose. fumadocs' build-time * export (`staticGET`) is a client-side search: it ships every locale's index From 21ef209bb313ce9654d4f89e7ea2019b00a03b4d Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 6 Oct 2026 11:28:09 +0000 Subject: [PATCH 4/5] docs: state the measured contrast ratios in the theme and code-theme comments Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- apps/docs/app/global.css | 2 +- apps/docs/source.config.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/docs/app/global.css b/apps/docs/app/global.css index ee3e9aa..5a07753 100644 --- a/apps/docs/app/global.css +++ b/apps/docs/app/global.css @@ -10,7 +10,7 @@ * and the description under each page title, and it measured 4.12–4.35:1 * against the backgrounds it sits on (#efefef to #f5f5f5), under the 4.5:1 * WCAG AA floor for body text. 40% is #666666: 4.99:1 on the darkest of them, - * the search button, and 5.25:1 on the page. + * the search button, and 5.27:1 on the page. * * Declared in `@theme`, where the neutral theme declares it, so it replaces * only the light value. The theme's dark value is set by a plain `.dark` rule, diff --git a/apps/docs/source.config.ts b/apps/docs/source.config.ts index c004283..a036beb 100644 --- a/apps/docs/source.config.ts +++ b/apps/docs/source.config.ts @@ -89,7 +89,7 @@ export default defineConfig({ * scope it is assigned to in either theme. On fumadocs' code-block * background it measured 3.65:1 in dark mode (#191919) and 4.26:1 in * light mode (#f1f1f1). The replacements are GitHub's own accessible - * muted greys: #8b949e (5.71:1 on #191919) and #57606a (5.65:1 on + * muted greys: #8b949e (5.72:1 on #191919) and #57606a (5.66:1 on * #f1f1f1). * * Scoped by theme name, so each theme's comment colour is replaced on From 911a174ccd83d51dd1fa18a7a4f25df510ac1e02 Mon Sep 17 00:00:00 2001 From: Claude <noreply@anthropic.com> Date: Tue, 6 Oct 2026 11:41:54 +0000 Subject: [PATCH 5/5] docs(views): declare views in a defineView container; list only the six apiMethods primitives The Views page declared views inside a defineObject call under an object-level `view` key. ObjectStack provides no defineObject, and ObjectSchema.create rejects `view` as an unknown key. Its views are a defineView container ({ list, listViews, form, formViews }, each view bound through `data`) registered on the stack with `views: [...]`, which both samples now show; both parse with @objectstack/spec 17.6.0. The REST API page's allowed apiMethods values drop the eight retired ones the spec strips at parse. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr --- content/docs/build/interface/views.mdx | 81 ++++++++++++++++++++------ content/docs/reference/rest-api.mdx | 6 +- 2 files changed, 66 insertions(+), 21 deletions(-) diff --git a/content/docs/build/interface/views.mdx b/content/docs/build/interface/views.mdx index 617b1fa..f25edbf 100644 --- a/content/docs/build/interface/views.mdx +++ b/content/docs/build/interface/views.mdx @@ -20,39 +20,81 @@ Schema source: ## The shortest possible declaration Every object automatically gets a default grid list + simple form even -if you declare no views. Add `view` to override or extend: +if you declare no views. To override or extend them, declare the +object's views in a `defineView` container, beside the object rather +than inside it: `list` is the default list, `form` the default form, +and each view names its object through `data`. ```ts -import { defineObject, F, P } from '@objectstack/spec' +// src/ui/views/support_ticket.view.ts +import { defineView } from '@objectstack/spec'; -export default defineObject({ - name: 'support_ticket', - fields: [ /* ... */ ], +const data = { provider: 'object' as const, object: 'support_ticket' }; - view: { - list: { type: 'grid', columns: ['subject', 'status', 'priority', 'assignee'] }, - form: { sections: [ { label: 'Details', fields: ['subject','description','priority','status','assignee'] } ] } - } -}) +export const SupportTicketViews = defineView({ + list: { + type: 'grid', + data, + columns: ['subject', 'status', 'priority', 'assignee'], + }, + form: { + type: 'simple', + data, + sections: [ + { name: 'details', label: 'Details', fields: ['subject', 'description', 'priority', 'status', 'assignee'] }, + ], + }, +}); +``` + +Register the container on the stack, next to the object it describes: + +```ts +// objectstack.config.ts +export default defineStack({ + // ... + views: [SupportTicketViews], +}); ``` For multi-view objects (e.g. a Kanban *and* a calendar over the same -data) use the named variants: +data), add named views to the same container: `listViews` for the view +switcher, `formViews` for forms opened by name. Every list view keeps a +top-level `columns`, whatever its type: ```ts -view: { +export const SupportTicketViews = defineView({ + list: { type: 'grid', data, columns: ['subject', 'status', 'priority', 'assignee'] }, listViews: { - by_status: { type: 'kanban', kanban: { groupByField: 'status' }, columns: ['subject','priority'] }, - schedule: { type: 'calendar', calendar: { startDateField: 'due_at', titleField: 'subject' } }, - by_owner: { type: 'grid', columns: ['subject','status','priority'], filterableFields: ['assignee'] } + by_status: { + label: 'By status', type: 'kanban', data, columns: ['subject', 'priority'], + kanban: { groupByField: 'status', columns: ['subject', 'priority'] }, + }, + schedule: { + label: 'Schedule', type: 'calendar', data, columns: ['subject', 'due_at'], + calendar: { startDateField: 'due_at', titleField: 'subject' }, + }, + by_owner: { + label: 'By owner', type: 'grid', data, columns: ['assignee', 'subject', 'status', 'priority'], + sort: [{ field: 'assignee', order: 'asc' }], + }, }, formViews: { - quick: { type: 'modal', sections: [ /* ... */ ] }, - full: { type: 'tabbed', sections: [ /* ... */ ] } - } -} + quick: { type: 'modal', data, sections: [{ name: 'quick', label: 'Quick edit', fields: ['status', 'priority', 'assignee'] }] }, + full: { + type: 'tabbed', data, + sections: [ + { name: 'details', label: 'Details', fields: ['subject', 'description'] }, + { name: 'triage', label: 'Triage', fields: ['status', 'priority', 'assignee'] }, + ], + }, + }, +}); ``` +The container and its rules are documented in full in ObjectStack's +[View metadata](https://docs.objectstack.ai/docs/ui/views). + ## List view types | `type` | Renders | Required config | @@ -70,6 +112,7 @@ view: { ### Common list options ```ts +// P is the predicate template tag: import { P } from '@objectstack/spec' { type: 'grid', columns: ['subject','status','priority','assignee','created_at'], diff --git a/content/docs/reference/rest-api.mdx b/content/docs/reference/rest-api.mdx index 54ab5de..82d7c69 100644 --- a/content/docs/reference/rest-api.mdx +++ b/content/docs/reference/rest-api.mdx @@ -43,8 +43,10 @@ any route code: object from the REST surface entirely. Requests to its routes 404. - **`apiMethods`** (optional whitelist) — when set, only the listed operations are reachable; any other operation is rejected at the REST layer. Allowed - values: `get`, `list`, `create`, `update`, `delete`, `upsert`, `bulk`, - `aggregate`, `history`, `search`, `restore`, `purge`, `import`, `export`. + values: `get`, `list`, `create`, `update`, `delete`, `bulk`. The eight + older values — `upsert`, `aggregate`, `history`, `search`, `restore`, + `purge`, `import`, `export` — are retired: a declared one is stripped at + parse, with a warning that names its replacement. ```ts import { ObjectSchema } from '@objectstack/spec/data';