From 73f1fa5de550ebc52115e815564797dc6248fd78 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:31:15 +0800 Subject: [PATCH 1/2] feat(cli,create-objectstack): make `os validate` the author-time gate; steer scaffolds/skills/examples/docs to it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ObjectStack metadata mistakes fail silently at runtime (bare-field predicates, dangling widget bindings). `os build` caught them; `os validate` did not — yet the scaffold's Next steps steered users to `validate`. Close the gap. - cli: `os validate` now runs the ADR-0032 CEL/predicate gate (CEL syntax + `record.` existence) in addition to the protocol schema and widget bindings — a read-only superset of `os build`'s checks, no artifact emitted. Catches bare-field predicates (#2183/#2185). - create-objectstack: emit `AGENTS.md` + `.github/copilot-instructions.md` into every generated project telling coding agents to run `npm run validate` after editing metadata; align the blank template `dev`/`start` scripts with the example apps; sharpen the post-create "Next steps". Removes the previously dead copilot-instructions template (superseded by the emitted guide). - skills: add a "Verify your work" section to all 9 objectstack-* skills — authoritative in objectstack-platform, predicate/binding-focused in data/ui/automation/formula, tight pointers in api/query/i18n/ai. - examples: add a `validate` script to each app (folded into app-showcase's `verify`) + a single examples/AGENTS.md. - docs: new guide content/docs/guides/validating-metadata.mdx; correct the `os validate` reference in getting-started/cli.mdx. Verified: cli builds clean (tsc); `os validate` passes app-todo and fails a bare `visible: '!is_completed'` with a corrective message; scaffolder smoke test emits AGENTS.md with placeholders substituted. Co-Authored-By: Claude Opus 4.8 --- .changeset/cli-validate-author-gate.md | 20 ++++ content/docs/getting-started/cli.mdx | 31 +++++- content/docs/guides/meta.json | 1 + content/docs/guides/validating-metadata.mdx | 104 ++++++++++++++++++ examples/AGENTS.md | 33 ++++++ examples/app-crm/package.json | 1 + examples/app-showcase/package.json | 3 +- examples/app-todo/package.json | 1 + packages/cli/src/commands/validate.ts | 40 ++++++- packages/create-objectstack/src/index.ts | 32 +++++- .../{copilot-instructions.md => AGENTS.md} | 31 +++++- .../src/templates/blank/README.md | 14 +++ .../src/templates/blank/package.json | 4 +- skills/objectstack-ai/SKILL.md | 17 +++ skills/objectstack-api/SKILL.md | 17 +++ skills/objectstack-automation/SKILL.md | 21 ++++ skills/objectstack-data/SKILL.md | 22 ++++ skills/objectstack-formula/SKILL.md | 13 +++ skills/objectstack-i18n/SKILL.md | 16 +++ skills/objectstack-platform/SKILL.md | 35 +++++- skills/objectstack-query/SKILL.md | 17 +++ skills/objectstack-ui/SKILL.md | 25 +++++ 22 files changed, 485 insertions(+), 13 deletions(-) create mode 100644 .changeset/cli-validate-author-gate.md create mode 100644 content/docs/guides/validating-metadata.mdx create mode 100644 examples/AGENTS.md rename packages/create-objectstack/src/templates/{copilot-instructions.md => AGENTS.md} (68%) diff --git a/.changeset/cli-validate-author-gate.md b/.changeset/cli-validate-author-gate.md new file mode 100644 index 0000000000..b114307f93 --- /dev/null +++ b/.changeset/cli-validate-author-gate.md @@ -0,0 +1,20 @@ +--- +"@objectstack/cli": minor +"create-objectstack": minor +--- + +Make `os validate` the author-time verification gate and steer scaffolds toward it. + +- **`os validate`** now runs the same CEL/predicate gate as `os build`/`os compile` + (ADR-0032): every `visible`/`disabled`/`requiredWhen`/validation/flow/sharing + predicate is checked for CEL syntax and `record.` existence on the target + object. It already ran the protocol schema and widget-binding checks; the + expression gate closes the gap so a bare field ref (`done` instead of + `record.done`) — which silently hides an action on every record at runtime + (#2183/#2185) — fails validation instead of shipping. `os validate` is now a + read-only superset of the build's checks (no artifact emitted). +- **`create-objectstack`** now emits an `AGENTS.md` (and `.github/copilot-instructions.md`) + into every generated project instructing coding agents to run `npm run validate` + after editing metadata, aligns the blank template's `dev`/`start` scripts with the + example apps (`objectstack dev`/`objectstack start`), and sharpens the post-create + "Next steps" output. diff --git a/content/docs/getting-started/cli.mdx b/content/docs/getting-started/cli.mdx index b269c1e174..ac06445c91 100644 --- a/content/docs/getting-started/cli.mdx +++ b/content/docs/getting-started/cli.mdx @@ -47,7 +47,7 @@ Open [http://localhost:3000/_console/](http://localhost:3000/_console/) — you' ### Validate & Build ```bash -os validate # Check schema correctness +os validate # Check schema + CEL predicates + widget bindings (no artifact) os compile # Build production artifact → dist/objectstack.json ``` @@ -303,7 +303,7 @@ shape they accept. See [Source vs Artifact](#source-vs-artifact) below. | Command | Description | |---------|-------------| | `os compile [config]` | Compile configuration to a JSON artifact (`dist/objectstack.json`) | -| `os validate [config]` | Validate configuration against the ObjectStack Protocol schema | +| `os validate [config]` | Validate schema, CEL predicates, and widget bindings — the same gates as `os compile`/`os build`, no artifact emitted | | `os info [config]` | Display metadata summary (objects, fields, apps, agents, etc.) | #### `os compile` @@ -346,7 +346,9 @@ over HTTP from another runtime. See [`os start`](#os-start) and #### `os validate` -Standalone schema validation with rich error output. Use to check your configuration without compiling. +The fast, artifact-free verification gate. It runs the **same structural and +semantic checks as `os compile`/`os build`** but writes no `dist/`, so it is the +command to run after every metadata edit. Use it before reporting a change done. ```bash os validate # Validate current directory @@ -355,16 +357,37 @@ os validate --json # JSON output for CI os validate path/to/config # Validate specific file ``` +**Gates run (each exits non-zero with a located, corrective message):** +1. **Protocol schema** — the stack conforms to `ObjectStackDefinitionSchema` + (`@objectstack/spec`). +2. **CEL / predicate validation (ADR-0032)** — every `visible` / `disabled` / + `requiredWhen` / validation rule / flow condition / sharing rule is parsed + for CEL syntax **and** checked that each `record.` reference exists on + the target object. This catches a **bare field ref** (`done` instead of + `record.done`) that would otherwise evaluate to `null` and silently hide an + action on every record (#2183/#2185). +3. **Widget-binding integrity (ADR-0021)** — every dashboard widget's + `dataset` / `dimensions` / `values` resolves to a declared dataset/field, so + a dangling binding fails here instead of rendering an empty chart. + **Options:** - `--strict` — Treat warnings as errors (exit code 1) - `--json` — Output results as JSON -**Warnings checked:** +**Warnings checked (advisory, non-blocking unless `--strict`):** - Missing `manifest.id` (required for deployment) - Missing `manifest.namespace` (required for multi-app hosting) - No objects defined - No apps or plugins defined + +`os validate` and `os build` share one validator, so a config that passes +`os validate` will not fail the build on schema/predicate/binding grounds. In a +scaffolded project these are wired as `npm run validate` and `npm run build`; +your `AGENTS.md` tells coding agents to run `npm run validate` after editing +metadata. See [Validating metadata](/docs/guides/validating-metadata). + + #### `os info` Displays a summary of your metadata without compilation or validation: diff --git a/content/docs/guides/meta.json b/content/docs/guides/meta.json index 95d0783df7..6652612fb7 100644 --- a/content/docs/guides/meta.json +++ b/content/docs/guides/meta.json @@ -12,6 +12,7 @@ "seed-data", "common-patterns", "formula", + "validating-metadata", "analytics-datasets", "airtable-dashboard-analysis", "---Building---", diff --git a/content/docs/guides/validating-metadata.mdx b/content/docs/guides/validating-metadata.mdx new file mode 100644 index 0000000000..43ef8df25b --- /dev/null +++ b/content/docs/guides/validating-metadata.mdx @@ -0,0 +1,104 @@ +--- +title: Validating Metadata +description: Why ObjectStack metadata mistakes fail silently at runtime, and the one command that catches them at author time — run it after every metadata edit. +--- + +# Validating Metadata + +ObjectStack metadata is data, not code paths — so most mistakes are **not** caught +by the TypeScript compiler. They pass `tsc`, load fine, and then fail **silently +at runtime**. The fix is one command you run after every metadata edit: + +```bash +os validate # schema + CEL predicates + widget bindings — no artifact +``` + +In a scaffolded project this is wired as `npm run validate`. Your generated +`AGENTS.md` instructs coding agents (Claude Code, Cursor, Copilot) to run it +after editing metadata. + +## Why typecheck isn't enough + +Two classes of bug type-check cleanly but break at runtime: + +### 1. Bare-field predicates + +Predicates — an action's `visible`/`disabled`, a field's `requiredWhen`, a +validation rule, a flow condition, a sharing rule — are **CEL expressions**, and +they reference record fields through the `record.` scope: + +```ts +// ✗ Wrong — `done` is a bare reference. It type-checks (it's just a string), +// but at runtime it resolves to null → the action is hidden on EVERY record. +{ name: 'mark_done', visible: '!done' } + +// ✓ Right +{ name: 'mark_done', visible: '!record.done' } +``` + +This is the trap behind the recurring "the button never shows / the rule never +fires" bugs (#2183/#2185). `os validate` parses every predicate and checks that +each `record.` exists on the target object, so the bare ref fails the +gate with a located, did-you-mean message instead of shipping. + +### 2. Dangling widget bindings + +A dashboard widget points at a `dataset` and reads `dimensions`/`values` from it. +If a name doesn't resolve, the chart renders **empty** — no error (ADR-0021). +`os validate` resolves every binding against the declared datasets and fails on a +dangling one. + +## The one gate, two entry points + +`os validate` and `os build` (alias `os compile`) run the **same** validator: + +| | `os validate` | `os build` | +|---|---|---| +| Protocol schema (Zod) | ✓ | ✓ | +| CEL / predicate validation | ✓ | ✓ | +| Widget-binding integrity | ✓ | ✓ | +| Emits `dist/objectstack.json` | — | ✓ | + +So `os validate` is the fast inner-loop check (no artifact); `os build` is what +you run when you need the deployable artifact. A config that passes `os validate` +will not fail `os build` on schema/predicate/binding grounds. + + +`os lint` is a **separate** pass — style and convention checks (snake_case +naming, required labels, namespace prefixes, data-model patterns). Run it too, +but it does not replace `os validate`, and `os validate` does not replace it. + + +## The workflow + +```bash +# after editing any *.object.ts / *.view.ts / *.action.ts / *.flow.ts / *.dashboard.ts +npm run validate # os validate — schema + predicates + bindings +npm run typecheck # tsc --noEmit — types against @objectstack/spec +``` + +**Rule of thumb: never report a metadata change as done until `npm run validate` +passes.** In the example apps the equivalent is `pnpm --filter validate` +(`pnpm verify` in `app-showcase`, which chains validate + typecheck + test). + +### Checking a single expression + +To validate one CEL expression *before* you write it into a file — for example +inside an AI build loop — call the `validate_expression` agent tool, which runs +the same predicate validator inline. See the +[objectstack-formula skill](/docs/guides/formula). + +## In CI + +Both commands support `--json` and exit non-zero on failure: + +```yaml +- name: Validate ObjectStack metadata + run: npx objectstack validate --strict --json +``` + +## See also + +- [Command Line Interface](/docs/getting-started/cli) — full `os validate` / `os build` reference +- [Formula & Predicates](/docs/guides/formula) — the CEL surface and `record.` scope +- [Data Modeling](/docs/guides/data-modeling) — objects, fields, validations, sharing diff --git a/examples/AGENTS.md b/examples/AGENTS.md new file mode 100644 index 0000000000..ed77edd6e4 --- /dev/null +++ b/examples/AGENTS.md @@ -0,0 +1,33 @@ +# Examples — Agent Instructions + +These are reference ObjectStack apps (`app-crm`, `app-todo`, `app-showcase`). +They double as the canonical shape of a user-scaffolded project, so the metadata +here should always be correct. + +## Verify after every metadata change + +ObjectStack metadata mistakes fail **silently at runtime**, not at edit time: + +- a bare field ref in a predicate (`done` instead of `record.done`) evaluates to + `null` and silently hides the action/validation on every record (#2183/#2185); +- a dangling dashboard widget binding renders an empty chart (ADR-0021). + +`objectstack validate` catches both at author time. It runs the same gates as +`objectstack build` — Zod protocol schema, CEL/predicate validation with +`record.` existence checks, and widget-binding integrity — but emits no +`dist/`, so it is the fast inner-loop check. It exits non-zero with a located, +corrective message. + +After editing any `*.object.ts` / `*.view.ts` / `*.action.ts` / `*.flow.ts` / +`*.dashboard.ts`, run the gate for that example: + +```bash +pnpm --filter @objectstack/example-crm validate # + typecheck, test +pnpm --filter @objectstack/example-todo validate +pnpm --filter @objectstack/example-showcase verify # validate + typecheck + test +``` + +**Do not report a metadata change as done until `validate` passes.** When in +doubt about how to express a predicate or binding, consult the relevant +`objectstack-*` skill (e.g. `objectstack-formula` for CEL, `objectstack-ui` for +views/actions/dashboards). diff --git a/examples/app-crm/package.json b/examples/app-crm/package.json index eeeb79505d..579d0a0967 100644 --- a/examples/app-crm/package.json +++ b/examples/app-crm/package.json @@ -14,6 +14,7 @@ "dev": "objectstack dev", "start": "objectstack start", "build": "objectstack build", + "validate": "objectstack validate", "typecheck": "tsc --noEmit", "test": "vitest run" }, diff --git a/examples/app-showcase/package.json b/examples/app-showcase/package.json index 380caf9f84..a812034003 100644 --- a/examples/app-showcase/package.json +++ b/examples/app-showcase/package.json @@ -15,9 +15,10 @@ "dev": "objectstack dev --seed-admin", "start": "objectstack start", "build": "objectstack build", + "validate": "objectstack validate", "typecheck": "tsc --noEmit", "test": "vitest run", - "verify": "pnpm typecheck && pnpm test", + "verify": "pnpm validate && pnpm typecheck && pnpm test", "test:smoke": "playwright test --config=playwright.config.ts" }, "dependencies": { diff --git a/examples/app-todo/package.json b/examples/app-todo/package.json index 931c645672..1cc808ac67 100644 --- a/examples/app-todo/package.json +++ b/examples/app-todo/package.json @@ -14,6 +14,7 @@ "dev": "objectstack dev", "start": "objectstack start", "build": "objectstack build", + "validate": "objectstack validate", "typecheck": "tsc --noEmit", "test": "objectstack test", "test:ai": "tsx test/ai.test.ts", diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index 7039e4bedd..a9597b9057 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -5,6 +5,7 @@ import chalk from 'chalk'; import { ZodError } from 'zod'; import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; +import { validateStackExpressions } from '../utils/validate-expressions.js'; import { validateWidgetBindings } from '../utils/validate-widget-bindings.js'; import { printHeader, @@ -19,7 +20,8 @@ import { } from '../utils/format.js'; export default class Validate extends Command { - static override description = 'Validate ObjectStack configuration against the protocol schema'; + static override description = + 'Validate ObjectStack configuration against the protocol schema, CEL expressions, and widget bindings (no artifact emitted)'; static override args = { config: Args.string({ description: 'Configuration file path', required: false }), @@ -70,6 +72,37 @@ export default class Validate extends Command { this.exit(1); } + // 2b. Expression validation (ADR-0032 §1a/1b) — the same gate `os build` + // runs, brought to the read-only check so authors catch it without + // emitting an artifact. CEL predicates in actions/validations/flows/ + // sharing/hooks are checked for syntax AND that `record.` + // references resolve on the target object. This is what catches a + // BARE field ref (`done` instead of `record.done`) that would + // otherwise silently hide an action on every record (#2183/#2185). + if (!flags.json) printStep('Validating expressions (ADR-0032)...'); + const exprIssues = validateStackExpressions(result.data as Record); + const exprErrors = exprIssues.filter((i) => i.severity !== 'warning'); + const exprWarnings = exprIssues.filter((i) => i.severity === 'warning'); + + if (exprErrors.length > 0) { + if (flags.json) { + console.log(JSON.stringify({ + valid: false, + errors: exprErrors, + warnings: exprWarnings, + duration: timer.elapsed(), + }, null, 2)); + this.exit(1); + } + console.log(''); + printError(`Expression validation failed (${exprErrors.length} issue${exprErrors.length > 1 ? 's' : ''})`); + for (const i of exprErrors.slice(0, 50)) { + console.log(` • ${i.where}: ${i.message}`); + console.log(chalk.dim(` source: \`${i.source}\``)); + } + this.exit(1); + } + // 3. Dashboard widget reference integrity (issue #1721) — a semantic // cross-reference pass the protocol schema cannot express: every // widget's `dataset`/`dimensions`/`values` and chartConfig @@ -108,7 +141,7 @@ export default class Validate extends Command { valid: true, manifest: config.manifest, stats, - warnings: widgetWarnings, + warnings: [...exprWarnings, ...widgetWarnings], duration: timer.elapsed(), }, null, 2)); return; @@ -117,6 +150,9 @@ export default class Validate extends Command { // 5. Warnings (non-blocking) const warnings: string[] = []; + for (const i of exprWarnings) { + warnings.push(`${i.where}: ${i.message}`); + } for (const f of widgetWarnings) { warnings.push(`${f.where}: ${f.message}`); } diff --git a/packages/create-objectstack/src/index.ts b/packages/create-objectstack/src/index.ts index 29a770ae2a..6ff9bcf4cf 100644 --- a/packages/create-objectstack/src/index.ts +++ b/packages/create-objectstack/src/index.ts @@ -335,6 +335,35 @@ function rewriteProjectIdentity( md = md.replace(/^#\s+.*$/m, `# ${title}`); fs.writeFileSync(readmePath, md); } + + writeAgentGuides(targetDir, title, projectName); +} + +// Emit the cross-agent guidance file (AGENTS.md) and the GitHub Copilot variant +// (.github/copilot-instructions.md) from the shared template. This is what tells +// the coding agent to run `npm run validate` after editing metadata — the gate +// that catches bare-field predicates and dangling bindings that otherwise fail +// silently at runtime. Skip either file if the template already shipped its own, +// so a curated template can override the default. +function writeAgentGuides(targetDir: string, title: string, projectName: string) { + const templatePath = path.join(BUNDLED_TEMPLATES_DIR, 'AGENTS.md'); + if (!fs.existsSync(templatePath)) return; + + const rendered = fs + .readFileSync(templatePath, 'utf8') + .replace(/\{\{PROJECT_TITLE\}\}/g, title) + .replace(/\{\{PROJECT_NAME\}\}/g, projectName); + + const agentsPath = path.join(targetDir, 'AGENTS.md'); + if (!fs.existsSync(agentsPath)) { + fs.writeFileSync(agentsPath, rendered); + } + + const copilotPath = path.join(targetDir, '.github', 'copilot-instructions.md'); + if (!fs.existsSync(copilotPath)) { + fs.mkdirSync(path.dirname(copilotPath), { recursive: true }); + fs.writeFileSync(copilotPath, rendered); + } } // ─── CLI Program ──────────────────────────────────────────────────── @@ -450,7 +479,8 @@ const program = new Command() console.log(chalk.dim(' npm install')); } console.log(chalk.dim(' npm run dev # Start development server')); - console.log(chalk.dim(' npm run validate # Check configuration')); + console.log(chalk.dim(' npm run validate # Verify metadata: schema + predicates + bindings')); + console.log(chalk.dim(' # (run after every metadata edit — see AGENTS.md)')); if (options.skipInstall || options.skipSkills) { console.log(''); console.log(chalk.bold(' AI Skills (recommended):')); diff --git a/packages/create-objectstack/src/templates/copilot-instructions.md b/packages/create-objectstack/src/templates/AGENTS.md similarity index 68% rename from packages/create-objectstack/src/templates/copilot-instructions.md rename to packages/create-objectstack/src/templates/AGENTS.md index 9fdfb7bf2a..c8fad71059 100644 --- a/packages/create-objectstack/src/templates/copilot-instructions.md +++ b/packages/create-objectstack/src/templates/AGENTS.md @@ -1,6 +1,8 @@ -# {{PROJECT_TITLE}} — Copilot Instructions +# {{PROJECT_TITLE}} — Agent Instructions -> Auto-generated by `create-objectstack`. Customise freely. +> Auto-generated by `create-objectstack`. Read by Claude Code, Cursor, Codex +> (`AGENTS.md`) and GitHub Copilot (`.github/copilot-instructions.md`). +> Customise freely. ## Project Context @@ -10,6 +12,27 @@ that defines business objects, views, automations, and AI agents in TypeScript. - **Entry point:** `objectstack.config.ts` (uses `defineStack()`) - **Spec package:** `@objectstack/spec` (Zod-first schemas and types) +## Verify your work (do this after every metadata change) + +Metadata mistakes fail **silently at runtime**, not at edit time — a bare field +reference in a predicate (`done` instead of `record.done`) evaluates to `null` +and silently hides an action on every record; a dangling widget binding renders +an empty chart. Catch them before you ship: + +```bash +npm run validate # schema + CEL predicates + widget bindings (no artifact) +npm run typecheck # TypeScript types against @objectstack/spec +``` + +`npm run validate` runs the **same gates as `npm run build`** (Zod protocol +schema, CEL/predicate validation with `record.` existence checks, and +dashboard widget-binding integrity) but emits no `dist/` — so it is the fast +inner-loop check after editing an `*.object.ts` / `*.view.ts` / `*.action.ts` / +`*.flow.ts`. Both exit non-zero with a located, corrective message on failure. +Run `npm run build` when you need the compiled `dist/objectstack.json` artifact. + +**Never report a metadata change as done until `npm run validate` passes.** + ## Naming Conventions | Context | Convention | Example | @@ -25,6 +48,10 @@ that defines business objects, views, automations, and AI agents in TypeScript. 2. `defineStack()` is the single configuration entry point in `objectstack.config.ts`. 3. Use `Object.values()` barrel pattern for metadata arrays. 4. Import from `@objectstack/spec` — never use relative paths into the spec package. +5. **Predicates are CEL** — `visible`, `disabled`, `requiredWhen`, validation rules, + flow conditions and sharing rules reference record fields as `record.`, + never bare ``. A bare reference is a silent runtime bug (`npm run validate` + now rejects it). ## Project Structure diff --git a/packages/create-objectstack/src/templates/blank/README.md b/packages/create-objectstack/src/templates/blank/README.md index ae866005af..919c08fa33 100644 --- a/packages/create-objectstack/src/templates/blank/README.md +++ b/packages/create-objectstack/src/templates/blank/README.md @@ -16,6 +16,20 @@ The REST API is served at `http://localhost:3000/api`. - `objectstack.config.ts` — environment manifest (objects, API, plugins) - `src/objects/` — object definitions (one file per object) +## Verify your changes + +After editing any metadata, run: + +```bash +pnpm validate # schema + CEL predicates + widget bindings (no artifact) +pnpm typecheck # TypeScript types against @objectstack/spec +``` + +`pnpm validate` runs the same gates as `pnpm build` and catches mistakes that +otherwise fail *silently at runtime* — e.g. a bare `done` (instead of +`record.done`) in an action predicate that would hide the action on every +record. See `AGENTS.md` for the full convention. + ## Next steps - Add an object: see the `objectstack-data` skill. diff --git a/packages/create-objectstack/src/templates/blank/package.json b/packages/create-objectstack/src/templates/blank/package.json index f0bcd926bf..13db4acb6b 100644 --- a/packages/create-objectstack/src/templates/blank/package.json +++ b/packages/create-objectstack/src/templates/blank/package.json @@ -4,8 +4,8 @@ "private": true, "type": "module", "scripts": { - "dev": "objectstack serve --watch", - "start": "objectstack serve", + "dev": "objectstack dev", + "start": "objectstack start", "build": "objectstack build", "validate": "objectstack validate", "typecheck": "tsc --noEmit" diff --git a/skills/objectstack-ai/SKILL.md b/skills/objectstack-ai/SKILL.md index 4529f5d675..ae1b768271 100644 --- a/skills/objectstack-ai/SKILL.md +++ b/skills/objectstack-ai/SKILL.md @@ -469,6 +469,23 @@ logic into **skills**, and wire domain knowledge through **RAG pipelines**. --- +## Verify your work + +After authoring a `*.agent.ts` / `*.tool.ts` / `*.skill.ts` or a model-registry +entry, run the author-time gate before reporting done: + +```bash +os validate # Zod schema + CEL predicate validation + bindings (no artifact) +# or: os build # the same gates, plus emits dist/ +``` + +It confirms the agent/tool/model metadata conforms to the protocol and that any +CEL predicate (e.g. a tool's availability condition) parses and resolves. In a +scaffolded project the gate is `npm run validate`. See objectstack-platform → +**Verify your work**. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod diff --git a/skills/objectstack-api/SKILL.md b/skills/objectstack-api/SKILL.md index d885b04a2f..f76aaeb531 100644 --- a/skills/objectstack-api/SKILL.md +++ b/skills/objectstack-api/SKILL.md @@ -364,6 +364,23 @@ const aiService = kernel.resolve('ai'); --- +## Verify your work + +After adding a `*.endpoint.ts`, a custom route, or an auth provider, run the +author-time gate before reporting done: + +```bash +os validate # Zod schema + CEL predicate validation + bindings (no artifact) +# or: os build # the same gates, plus emits dist/ +``` + +Route-guard and auth predicates are CEL; the gate parses them and fails +non-zero with a located message instead of letting a malformed guard fall +through at runtime. In a scaffolded project the gate is `npm run validate`. See +objectstack-platform → **Verify your work** for the full gate list. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod diff --git a/skills/objectstack-automation/SKILL.md b/skills/objectstack-automation/SKILL.md index 1ef8d23ca7..ad0d8a9a19 100644 --- a/skills/objectstack-automation/SKILL.md +++ b/skills/objectstack-automation/SKILL.md @@ -611,6 +611,27 @@ metadata first; reserve custom code for edge-case integrations. --- +## Verify your work + +Flow/workflow predicates fail **silently at runtime** when malformed: a bare +field ref in a `start`/`decision` condition or an edge guard (`status == 'open'` +instead of `record.status == 'open'`) resolves to `null`/`false`, so the flow +"fires" but does nothing — and nothing errors at edit time. Catch it at author +time before reporting a flow done: + +```bash +os validate # CEL/predicate validation (record. existence) + schema +# or: os build # the same gates, plus emits dist/ +``` + +This runs the ADR-0032 expression gate over every flow condition, edge guard, +workflow predicate, validation rule and sharing rule, exiting non-zero with a +located, corrective message. Remember conditions are **bare CEL** +(`record.status == 'x'`); only string node fields use `{…}` templates — see +objectstack-formula. In a scaffolded project this is `npm run validate`. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod diff --git a/skills/objectstack-data/SKILL.md b/skills/objectstack-data/SKILL.md index d542fd3bc5..0326fc44c2 100644 --- a/skills/objectstack-data/SKILL.md +++ b/skills/objectstack-data/SKILL.md @@ -878,6 +878,28 @@ on parents, `select` options, and a name/title field per object. --- +## Verify your work + +After authoring or editing any `*.object.ts` / `*.seed.ts`, run the author-time +gate before reporting done: + +```bash +os validate # Zod schema + CEL predicates (record. existence) + bindings +# or: os build # the same gates, plus emits dist/ +``` + +It catches what otherwise fails **silently at runtime**: a bare field ref in a +`requiredWhen` / `readonlyWhen` / `visibleWhen`, a validation rule, a formula, or +a row-level-security/sharing predicate (`done` instead of `record.done`) that +evaluates to `null` and never fires (#2183/#2185). `os lint` is a *separate* +pass that additionally checks the data model against the conventions in this +skill (relationships, master-detail, roll-ups) — run it too, but it does **not** +replace `os validate`. (Reminder: two consecutive `os build` runs with no source +change must be byte-identical — see the determinism gate above.) In a scaffolded +project the gate is `npm run validate`. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod diff --git a/skills/objectstack-formula/SKILL.md b/skills/objectstack-formula/SKILL.md index 8eabbcd7d8..57d6092c9b 100644 --- a/skills/objectstack-formula/SKILL.md +++ b/skills/objectstack-formula/SKILL.md @@ -421,6 +421,19 @@ CI runs `objectstack build` twice and asserts SHA-1 match. --- +## Verify your work + +A malformed expression no longer fails silently (ADR-0032, see the note near the +top of this skill): both `os validate` and `os build` run the shared validator +over every formula and predicate in the stack — CEL syntax **plus** +`record.` existence on the target object — and fail non-zero with a +did-you-mean. Use `os validate` as the fast post-edit check (no artifact emitted; +`npm run validate` in a scaffolded project). To check a *single* expression +before saving it, call the `validate_expression` agent tool, which runs the same +validator inline. + +--- + ## See also - [`content/docs/guides/formula.mdx`](../../content/docs/guides/formula.mdx) — human-facing guide diff --git a/skills/objectstack-i18n/SKILL.md b/skills/objectstack-i18n/SKILL.md index 5d86da329f..a370ae0106 100644 --- a/skills/objectstack-i18n/SKILL.md +++ b/skills/objectstack-i18n/SKILL.md @@ -639,6 +639,22 @@ export default { --- +## Verify your work + +After editing a `*.translation.ts` bundle: + +```bash +os i18n check # translation coverage vs the default locale (missing-key report) +os validate # the bundle conforms to the protocol schema (no artifact) +# or: os build # the same schema gate, plus emits dist/ +``` + +`os i18n check` lists keys missing per locale; `os lint --i18n-strict` turns +coverage gaps into hard errors. In a scaffolded project the schema gate is +`npm run validate`. See objectstack-platform → **Verify your work**. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod diff --git a/skills/objectstack-platform/SKILL.md b/skills/objectstack-platform/SKILL.md index 69b32418c0..0e0256f09c 100644 --- a/skills/objectstack-platform/SKILL.md +++ b/skills/objectstack-platform/SKILL.md @@ -1072,7 +1072,7 @@ be re-run when commands are added. | `os init` | Scaffold a new project (alternative to `npx create-objectstack`) | | `os dev` | Start the dev server with hot metadata reload. `--fresh` = ephemeral clean DB + auto `--seed-admin`, which POSTs a sign-up after boot (default `admin@objectos.ai` / `admin123`; override with `--admin-email` / `--admin-password`). The seeded human is auto-promoted to **platform admin**, so Setup/Studio work on first login. | | `os studio` | Launch Studio UI against the local stack | -| `os validate` | Validate `objectstack.config.ts` (Zod + cross-reference checks) | +| `os validate` | Validate `objectstack.config.ts` — Zod protocol schema, CEL/predicate validation (`record.` existence), and widget-binding integrity. Same gates as `os build`, no artifact emitted. See [Verify your work](#verify-your-work). | | `os lint` | Style/convention lint on metadata files | | `os info` | Print resolved stack info (env, drivers, adapter, plugin list) | | `os doctor` | Diagnose common setup issues | @@ -1087,6 +1087,39 @@ be re-run when commands are added. | `os start` | Production-grade boot (validates env, applies migrations, starts adapter) | | `os generate ` | Scaffold an object / view / flow / agent from a template | +## Verify your work + +ObjectStack metadata mistakes fail **silently at runtime**, not at edit time: +a bare field ref in a predicate (`done` instead of `record.done`) evaluates to +`null` and silently hides an action/validation on every record (#2183/#2185); a +dangling dashboard widget binding renders an empty chart (ADR-0021). Both are +caught at author time by one command: + +```bash +os validate # Zod schema + CEL predicates + widget bindings — no artifact +# or +os build # the same three gates, plus emits dist/objectstack.json +``` + +`os validate` and `os build` run the **same** structural + semantic gates: + +1. **Zod protocol schema** — the stack conforms to `@objectstack/spec`. +2. **CEL / predicate validation (ADR-0032)** — every `visible` / `disabled` / + `requiredWhen` / validation rule / flow condition / sharing rule is parsed + for CEL syntax *and* checked that each `record.` reference exists on + the target object. A bare `field` (missing `record.`) fails here. +3. **Widget-binding integrity (ADR-0021)** — every dashboard widget's + `dataset` / `dimensions` / `values` resolves to a declared dataset/field. + +Both exit non-zero with a located, corrective message; `os build` additionally +emits the artifact. Use `os validate` as the fast inner-loop check after editing +metadata and `os build` when you need `dist/`. In a scaffolded project these are +`npm run validate` / `npm run build`. + +**Rule of thumb: never report a metadata change as done until `os validate` +passes.** (`os lint` is a *separate* style/convention pass — naming, labels, +namespace prefixes — and does not replace `os validate`.) + ## Ports & networking Port resolution is the same for `os dev` and `os start` (both spawn `os serve`): diff --git a/skills/objectstack-query/SKILL.md b/skills/objectstack-query/SKILL.md index 004f1e5de3..ecb0c7de23 100644 --- a/skills/objectstack-query/SKILL.md +++ b/skills/objectstack-query/SKILL.md @@ -539,6 +539,23 @@ only fall back to custom query code when schema limits require it. --- +## Verify your work + +Most queries run at runtime (smoke-test them with `os data query` or a vitest +test), but query *metadata* — list-view filter specs and report/dashboard +datasets — is validated statically. After editing those, run: + +```bash +os validate # schema + CEL predicates + widget/dataset bindings (no artifact) +# or: os build # the same gates, plus emits dist/ +``` + +A dashboard widget whose `dataset` / `dimensions` / `values` don't resolve fails +here instead of rendering an empty chart (ADR-0021). In a scaffolded project the +gate is `npm run validate`. See objectstack-platform → **Verify your work**. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod diff --git a/skills/objectstack-ui/SKILL.md b/skills/objectstack-ui/SKILL.md index 928eb597f5..6b57ce55a8 100644 --- a/skills/objectstack-ui/SKILL.md +++ b/skills/objectstack-ui/SKILL.md @@ -1290,6 +1290,31 @@ the selected row in `list_item` contexts. --- +## Verify your work + +After authoring any `*.view.ts` / `*.action.ts` / `*.dashboard.ts`, run the +author-time gate before reporting done: + +```bash +os validate # CEL predicates (record.) + widget bindings + schema +# or: os build # the same gates, plus emits dist/ +``` + +Two UI-specific traps it catches, both **silent at runtime** otherwise: + +- **Action / field predicate** — a bare field ref in an action `visible` / + `disabled` or a field `visibleWhen` (`done` instead of `record.done`) + evaluates to `null` and hides the control on *every* record (the #2183/#2185 + "button never shows" trap). +- **Dashboard widget binding** — a widget `dataset` / `dimensions` / `values` + that doesn't resolve to a declared dataset/field renders an empty chart + (ADR-0021). + +Don't report a view/action/dashboard done until `os validate` passes. In a +scaffolded project the gate is `npm run validate`. + +--- + ## References See [references/_index.md](./references/_index.md) for the full list of Zod From afacdacba0fe26b60850d377d46636a99bef2177 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:43:44 +0800 Subject: [PATCH 2/2] fix(create-objectstack): atomic exclusive-create for emitted guides (CodeQL js/file-system-race) CodeQL flagged the existsSync()+writeFileSync() check-then-write in writeAgentGuides as a TOCTOU file-system race (2 high alerts). Replace with an atomic `wx` (exclusive-create) write via a writeIfAbsent() helper that swallows EEXIST, and read the bundled template with try/catch ENOENT instead of a separate existence check. Same "don't clobber a template's own guide" semantics, no check-then-act gap. Co-Authored-By: Claude Opus 4.8 --- packages/create-objectstack/src/index.ts | 34 +++++++++++++++++------- 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/packages/create-objectstack/src/index.ts b/packages/create-objectstack/src/index.ts index 6ff9bcf4cf..e94a90c6e1 100644 --- a/packages/create-objectstack/src/index.ts +++ b/packages/create-objectstack/src/index.ts @@ -347,22 +347,36 @@ function rewriteProjectIdentity( // so a curated template can override the default. function writeAgentGuides(targetDir: string, title: string, projectName: string) { const templatePath = path.join(BUNDLED_TEMPLATES_DIR, 'AGENTS.md'); - if (!fs.existsSync(templatePath)) return; + let template: string; + try { + template = fs.readFileSync(templatePath, 'utf8'); + } catch (err: any) { + if (err?.code === 'ENOENT') return; // bundled template absent — nothing to emit + throw err; + } - const rendered = fs - .readFileSync(templatePath, 'utf8') + const rendered = template .replace(/\{\{PROJECT_TITLE\}\}/g, title) .replace(/\{\{PROJECT_NAME\}\}/g, projectName); - const agentsPath = path.join(targetDir, 'AGENTS.md'); - if (!fs.existsSync(agentsPath)) { - fs.writeFileSync(agentsPath, rendered); - } + // Atomic exclusive-create (the `wx` flag) instead of existsSync()+writeFileSync(): + // it fails with EEXIST if the file already exists, so a curated template that + // ships its own guide is preserved — without the check-then-write TOCTOU race a + // separate existence check introduces. + writeIfAbsent(path.join(targetDir, 'AGENTS.md'), rendered); const copilotPath = path.join(targetDir, '.github', 'copilot-instructions.md'); - if (!fs.existsSync(copilotPath)) { - fs.mkdirSync(path.dirname(copilotPath), { recursive: true }); - fs.writeFileSync(copilotPath, rendered); + fs.mkdirSync(path.dirname(copilotPath), { recursive: true }); + writeIfAbsent(copilotPath, rendered); +} + +// Create a file only if it does not already exist, atomically — no time-of-check +// to time-of-use gap between an existence test and the write. +function writeIfAbsent(filePath: string, contents: string) { + try { + fs.writeFileSync(filePath, contents, { flag: 'wx' }); + } catch (err: any) { + if (err?.code !== 'EEXIST') throw err; } }