Skip to content

docs: make the views and flows code samples parse with @objectstack/spec 17.7.0 - #315

Merged
hotlong merged 12 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1
Oct 6, 2026
Merged

hotlong merged 12 commits into
mainfrom
claude/pm-dispatch-objectos-ju9td1

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #307

Every code sample on build/interface/views.mdx and build/automation/flows.mdx now parses with the latest published @objectstack/spec, 17.7.0 (npm latest, published 2026-10-06T12:22Z; versioned in objectstack 4e4e8814, "chore: version packages (#21352)"). The check follows #301's method: extract the published blocks, wrap them minimally, parse them, run refusal controls. Results, before and after, are below.

Two English pages change. No locale file, app code or script is touched.

What changed

build/interface/views.mdx

  • Kanban, Calendar, Gantt, Tree and Chart each get the top-level columns that every list view requires (ListViewShapeSchema.columns, packages/spec/src/ui/view.zod.ts:2634). For Kanban, the value mirrors the card columns, as ObjectStack's own docs do (content/docs/ui/views.mdx, Kanban: "still required at the top level").

  • Common list options: fontWeight: 600 becomes fontWeight: '600'. A conditionalFormatting style is z.record(z.string(), z.string()) (view.zod.ts:2860).

  • Fragments say what they omit. Each of the 8 single-view fragments starts with a comment: "One list view (or form view); the container and data are omitted (see above)". One sentence under the table says where such a view goes: a defineView container's list or a listViews entry, with data. The omitted parts are not invalid: data is optional on a list view, and every fragment also parses with data supplied.

  • The "List view types" table said only grid needs columns. It also said which block keys are required, and it was wrong in both directions:

    • under-required: kanban.columns (view.zod.ts:1809) and chart.values (:1841) are required, and the table omitted them;
    • over-required: gallery coverField and titleField, calendar titleField, map locationField, and tree parentField and labelField are optional.

    The table now opens with "every list view needs a top-level columns". It then lists each config block with its required keys, and the common optional keys in a column of their own.

build/automation/flows.mdx

The card's finding 2 was read-only and named send_email. Measured on 17.7.0, the premise holds only in part:

  • send_email is not a FlowNodeAction value (packages/spec/src/automation/flow.zod.ts:31-56).
  • But FlowNodeAction no longer gates a node's type at parse (ADR-0018, flow.zod.ts:9-19): a node of type send_email parses (control F3).
  • What defineFlow refused was the whole sample shape: trigger, steps, inputs, type: 'manual' | 'scheduled', steps with no id or label, and per-step action, retry and onError. Of the 9 TS blocks, 6 failed. The defineStack block passed on its own, the two timeRelative fragments parsed, and the CEL block can only be shape-checked.

So every flow sample is rewritten to FlowSchema's graph (flow.zod.ts:977): nodes plus edges, with the trigger bound on the start node's config. Where the prose explained the old shape, it was rewritten too.

  • Email goes through a notify node with channels: ['email'] — the documented email path. NotifyConfigSchema is at packages/spec/src/automation/io-node-config.zod.ts:180, with channels "default: inbox" at :224. ObjectStack's flows docs map the retired email action to a notify node at content/docs/automation/flows.mdx:310. A stored template goes in template, in place of title and message.
    • The sample's object is now support_ticket and its recipient {record.assignee}. The old sys_user welcome email assumed that a record-change flow fires on sys_user, and nothing public says so.
  • requires: ['automation', 'triggers'], up from ['automation']. defineStack refuses a stack that has a record-change or schedule flow and lacks 'triggers' (control F1). Sources: flow-trigger-kind.ts:20, and ObjectStack flows.mdx:1849,1866.
  • Flow types table: the types are now the real FlowSchema.type enum (flow.zod.ts:1060), record_change | schedule | autolaunched | screen | api. The old table used Autolaunched for a record change, plus Scheduled and Manual.
  • Trigger timing: the when: before_insert… table becomes the start node's triggerType tokens (ObjectStack flows.mdx:2476-2484).
    • The sentence "before_* flows can mutate the record being written" is replaced. ObjectStack's guidance is that mutating the pending record before it is saved is a before hook's job, not a flow's (content/docs/automation/hooks.mdx:29).
    • "after_* flows run async" is dropped: no public source states it.
  • Scheduled:
    • runAs: 'system' is added. A scheduled run has no trigger user, so under the default user, its data operations are refused (flow.zod.ts, runAs describe).
    • The update is one update_record with multi: true: without it, a predicate update is refused (builtin-node-config.zod.ts:495).
    • A one-line pointer to ObjectStack's "The acting organization" covers time-triggered flows.
  • Started by hand:
    • approve_invoice is an autolaunched flow with an isInput variable.
    • The curl calls the canonical route, POST /api/v1/automation/:name/trigger (AutomationApiContracts.triggerFlow, packages/spec/src/api/automation-api.zod.ts:674), with params. The old route was the action door, /api/v1/actions/invoice/approve_invoice, with an inputs body that TriggerFlowRequestSchema would drop silently (control F13).
  • Step types → Node types: the names now come from FlowNodeAction, plus the region and plugin types (parallel, try_catch, approval).
  • Conditions and branches: a complete flow, with a decision node, a conditioned edge and an isDefault edge (ObjectStack flows.mdx:1381-1420). The old then / else step had no counterpart.
  • Error handling: fault edges plus flow-level errorHandling: { strategy, maxRetries, backoffMs, backoffMultiplier } (flow.zod.ts:1200; ObjectStack flows.mdx:1500-1575). The old per-step retry and onError: 'continue' | 'fail' | 'rollback' are removed: there is no rollback strategy. "Failed flow runs land in the job retry queue" is removed too, because no public source states it.
  • CEL samples:
    • has(record.notes) && record.notes != "" becomes !isBlank(record.notes), with one sentence of why: record is total over declared fields, so has() is not a value test (ObjectStack flows.mdx, the total-binding callout).
    • duration("30d") is gone: d is not a CEL duration unit.
    • account.tier becomes record.tier.
  • Testing: os test --scenario "…" becomes os test qa/FILE.test.json. --scenario is not an os test flag (ObjectStack content/docs/deployment/cli.mdx:1937-1948).

Every ObjectStack reference above is to the public repository at 4e4e8814, the 17.7.0 version commit.

Parse results — @objectstack/spec 17.7.0 from npm

The checker is in the PM scratchpad: spec-check/scripts/{extract,views-check,flows-check,controls}.mjs.

How blocks are checked:

  • A defineView, defineFlow or defineStack block is evaluated as published, with only TS syntax removed (import, export, as const).
  • A single-view fragment is parsed three ways: ListViewSchema.parse (or FormViewSchema.parse), defineView({ list | form: fragment }), and with the omitted data supplied.
  • Every builtin node of every flow is also parsed against the executor contract its executor parses with (getBuiltinNodeConfigContracts()). This is stricter than defineFlow, which checks only that required keys are present.
  • Fragments of flow keys are wrapped with the nodes their comment names.
  • CEL strings get only the slot-shape check: the spec ships no CEL parser.

views.mdx — before (main 5d2f837): 9 pass, 18 fail · after: 27 pass, 0 fail

Block (line before → after) Before After
1 defineView shortest (28 → 28) PASS PASS
2 defineStack registration (52 → 52) PASS PASS
3 defineView named views (65 → 65) PASS PASS
4 Common list options (114 → 121) FAIL ×3 — conditionalFormatting.0.style.fontWeight: expected string, received number PASS ×3
5 Kanban (146 → 154) FAIL ×3 — columns: invalid_union (required) PASS ×3
6 Calendar (165 → 175) FAIL ×3 — columns PASS ×3
7 Gantt (179 → 191) FAIL ×3 — columns PASS ×3
8 Tree (194 → 208) FAIL ×3 — columns PASS ×3
9 Chart (215 → 231) FAIL ×3 — columns PASS ×3
10 Tabbed form (242 → 260) PASS ×3 PASS ×3
11 Public form (258 → 277) PASS ×3 PASS ×3

flows.mdx — before: 13 pass, 6 fail, 2 n/a · after: 27 pass, 0 fail, 1 n/a

Block (line before → after) Before After
1 defineStack requires (22 → 27) PASS alone. No page flow parsed, so requires was never judged; control F1 shows ['automation'] is refused once a record-change flow is present PASS alone, and PASS with all 5 page flows
2 welcome email → ticket email (40 → 51) FAIL — label, nodes and edges missing; unrecognized trigger and steps PASS (defineFlow) and PASS (executor contracts)
3 nightly cleanup (86 → 123) FAIL — type 'scheduled' invalid; unrecognized schedule and steps PASS · PASS
4 renewal reminder (121 → 176) FAIL — label missing on the flow, nodes.0 and nodes.2 PASS · PASS
5 timeRelative fragments (163 → 226) PASS ×6 PASS ×6
6 approve invoice (187 → 252) FAIL — type 'manual' invalid; unrecognized inputs and steps PASS · PASS
7 curl (208 → 286) N/A — the action door PASS ×4: the route is AutomationApiContracts.triggerFlow, the flow is on the page, the body parses as TriggerFlowRequestSchema, and every params key is a declared isInput
8 conditions (230 → 320) FAIL — no id or label; unrecognized when, then and else PASS · PASS
9 error handling (247 → 365) FAIL — no id or label; unrecognized action, inputs, retry and onError PASS: wrapped with the 3 nodes its comment names, then executor contracts
10 CEL (268 → 395) slot shape only ×3 slot shape only ×3
11 os test (286 → 418) N/A (not a spec sample; --scenario is not a flag) N/A

Refusal controls — 28 of 28 hold (26 refusal or acceptance controls, plus 2 baselines of the fixed shapes)

  • Views:
    • V1: the kanban without top-level columns is FAIL.
    • V2: fontWeight: 600 is FAIL; '600' is PASS.
    • V3: a chart without values is FAIL.
    • V4: kanban without its own columns is FAIL.
    • V5: a calendar without startDateField is FAIL.
    • V6: a calendar with only startDateField is PASS, so titleField is optional.
    • V7: the misspelling colums is FAIL, which shows the parse is strict.
  • Flows:
    • F1: requires: ['automation'] plus a record-change flow is FAIL; with 'triggers' added it is PASS.
    • F2: the old trigger / steps shape is FAIL.
    • F3: a node of type send_email is PASS, but FlowNodeAction.parse('send_email') is FAIL, and so is a node carrying action: 'send_email'.
    • F4: a notify node with neither title nor template is FAIL.
    • F5: notify with the old subject / body spelling is PASS under defineFlow and FAIL under the executor contract, which is what the extra check adds.
    • F6: strategy: 'retry' with no maxRetries is FAIL.
    • F7: onError: 'rollback' is FAIL.
    • F8: per-node retry and onError are FAIL.
    • F9: a fault edge spelled as a label parses, as documented; os validate is what flags it.
    • F10 and F11: type: 'manual' and type: 'scheduled' are FAIL.
    • F12: offsetDays together with withinDays is FAIL.
    • F13: the old {"inputs": …} body parses, and inputs is dropped.
    • F14: filters in an update_record config is FAIL.

Page-text ablation, run on scratch copies; the repository is untouched:

  • Deleting the kanban's top-level columns line (anchor count 1 → 0) turns the views check red: EXIT 1, block 5 FAIL ×3.
  • Changing requires back to ['automation'] (mutant marker 1, anchor 0) turns the flows check red: EXIT 1, refused for the 4 flows with triggers.

Locale siblings — ruling A of #256

Verification (at 4d12d0a)

Gate Verdict
pnpm turbo run type-check --continue --force VERDICT command-exit 0 · ✓ Types generated successfully · 1 successful, 0 cached
NEXT_PRIVATE_STANDALONE=true pnpm turbo run build --force VERDICT command-exit 0 · 1 successful, 0 cached. The log carries "Failed to load dynamic font … self-signed certificate": the OG-image font fetch is blocked by this container's egress, and it is non-fatal
pnpm turbo run test --force VERDICT command-exit 0 · ✓ 10 self-test(s) passed
check-locale-surface.mjs EXIT 0 · ✓ every advertised URL has a source file and every source file is advertised; …
check-positioning.mjs EXIT 0 · ✓ positioning: 4 copies equal their constants; the brand is right in 659 pages and 2 llms bodies; no stale sentence in 79 English sources …
check-search-locales.mjs EXIT 0 · ✓ search locales: all 8 locales answer 200, find "permissions", find every own page by its title within the first 3 pages, and find nothing for a nonce
gen-zh-hant.mjs --check EXIT 0 · ✓ zh-Hant: 60 generated file(s) match the zh-Hans sources byte for byte.
Ownership, with the workflow argv: git diff --name-status --no-renames origin/main...HEAD, --actor hotlong, TRANSLATION_BOT_LOGIN set EXIT 0 · ✓ 2 file(s) changed, no translation artifacts touched. The control, with the bot as actor, is EXIT 1
check-translations.mjs EXIT 0 · ✓ translations gate passed. Stale is reported only: zh-Hans 47, the others 24, the same as base
check-translation-output.mjs --self-test / --files EXIT 0 / EXIT 0 · ✓ translation output gate passed (256 pre-existing finding(s) reported)
check-node-floor.mjs (with --self-test) / check-half-states.mjs --self-test EXIT 0 / EXIT 0 · 1551 cases pass
Control bytes in the 2 changed files 0

Browser. next start served the build on :3307, and Chromium (Playwright) loaded both pages at 1440 and 390: HTTP 200, the correct h1, and 11 code blocks per page.

  • scrollWidth equals the viewport at both widths (1440/1440, 390/390).
  • Every code box sits inside the viewport (17–373 px at 390).
  • At 390, the long lines scroll inside their own box (overflow-x: auto), as everywhere else on the site.
  • The clips were inspected: the views table, the flow types table, and the kanban, common-options, ticket-email, nightly-cleanup and error-handling blocks.

NOT MEASURED:

  • The live resolution of the docs.objectstack.ai links: egress answers CONNECT 403. The anchors #hooks-vs-flows and #the-acting-organization are derived from the public source headings, which ObjectStack's own pages link to.
  • Delivery on a running ObjectOS runtime: no runtime was run.

Proposed, not built: a docs-sample parse gate

The same defect class keeps reaching the site: #301, this card, and the pages listed under Acceptance notes. A gate would catch it on the PR that introduces it and on the spec bump that breaks it.

  • Mechanism. .github/scripts/check-doc-samples.mjs would extract the fenced ts blocks from English content/docs/**/*.mdx and evaluate the ones that opt in. Opt-in would be a fence meta word, such as a ts fence tagged spec-check. That is not an MDX comment, which the llms bodies must not carry.
    • The wraps would be the ones used here: defineView/defineFlow/defineStack as written; a single-view fragment as a list or form; flow-key fragments with the nodes their comment names; plus the executor-contract pass over every builtin node.
    • A block that matches no rule fails, rather than being skipped.
  • Spec version. @objectstack/spec would be pinned as a devDependency of tools/ci-scripts, so a dependabot bump that newly refuses a sample turns its own PR red.
  • House rule. A --self-test with fixtures for each rule, plus this PR's before pages as the negative control that must go red.
  • Cost. One devDependency (spec plus zod) and about 1 s per run. Fragments need their omission comment to follow a convention. A spec release can newly redden old pages, which is the point, but it lands on whoever merges the bump.
  • Rollout. Start with these two pages, then the pages in Acceptance notes as each is fixed. CEL strings stay out of scope: the spec ships no CEL parser.

Acceptance notes

Family members not fixed here, measured on 17.7.0 and left for a family close-out:

  • build/automation/workflows.mdx:104: defineFlow refused — label missing on the flow, nodes.0 and nodes.3.
  • build/automation/approvals.mdx:216: defineFlow refused — nodes.0.label and nodes.4.label missing.
  • build/interface/actions.mdx:119: defineView({ name, object, actions }) refused — unrecognized actions. Lines 63 and 127-133 show the old { type: 'action', action, inputs, record } step.
  • reference/cel.mdx: the "Flow guard" sample, { when: P… }, is the old step shape.

Left alone on views.mdx. These parse, so they are not this defect class:

  • exportOptions: ['csv','xlsx'] is the legacy bare-array spelling; it lifts to { formats }.
  • filterableFields is a "legacy shorthand — prefer userFilters".
  • The "Visibility, ARIA, theming" bullet and See also say visibleOn, which is deprecated in favour of visibleWhen and normalized at parse.

Pre-existing. At 1440, #301's named-views block has one line (quick:) that scrolls inside its box.

Branch. staged/issue-307 is cut from 5d2f837, per the claim. origin/main is now cf449fa (#313, #314). git merge-tree --write-tree origin/main HEAD is clean (tree 0087435), and no path is shared.

Branch


Generated by Claude Code

objectstack-fleet Bot and others added 12 commits October 6, 2026 10:36
…ds or fewer

Adds a `seoTitle:` frontmatter line to every English page whose built
`<title>` 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
…AQ 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
…comments

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ix 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
…ype-check

`next typegen` loads next.config.mjs, and fumadocs-mdx 15.0.7's createMDX()
starts init() without awaiting it (dist/next/index.js:14-20). init rewrites
every .source/*.ts with fs.writeFile (dist/core-DlDe_Eze.js:232-236), which
truncates first. typegen ends in process.exit(0), so it can exit inside that
window and leave .source/server.ts empty for tsc: TS2306, CI run 37478051451.

Running the fumadocs-mdx CLI after typegen makes the CLI, which awaits its
writes, the last writer before tsc. typegen does not read .source.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…ries, zh-Hant 404, Open-in links follow the page shown

- patches/fumadocs-ui@16.8.12.patch: the nine accessible names fumadocs-ui
  16.8.12 hard-codes in English (Open Search, Toggle Theme, Open/Collapse
  Sidebar, Copy Anchor Link, Copy/Copied Text, Toggle Menu, and Radix's
  "Main") read from its i18n context with the old literal as default. Eight
  keys are a backport of upstream 16.9.0's own names; lib/ui-text supplies
  all nine through RootProvider in every locale.
- app/[lang]/docs/layout.tsx: page-tree entries (sidebar, breadcrumb,
  prev/next footer) for pages a locale has no translation of carry
  lang="en", using the docs page's own translatedLocales detection.
- app/not-found.tsx: the 404 copy moves into lib/ui-text (notFound), so the
  zh-Hant string is generated from zh-Hans by gen-zh-hant and checked by
  --check like every other Traditional string.
- Open in ChatGPT / Claude on a translated page sends the assistant to the
  translated page itself; English pages and fallbacks keep the English .mdx.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
…pec 17.7.0

views.mdx: every list-view fragment carries its required top-level
columns, fontWeight is a string, the list view types table states the
required keys the spec declares, and each fragment says what it omits.

flows.mdx: every sample is rewritten from the old trigger/steps shape to
FlowSchema's nodes and edges. Email goes through a notify node with
channels: ['email']; requires lists automation and triggers.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FeA1nwBz1ohH65dvffUGKr
@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 6, 2026 16:29
@hotlong
hotlong merged commit 553ef7b into main Oct 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants