Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions .changeset/runtime-authoring-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
---
"@objectstack/metadata-protocol": minor
"@objectstack/lint": minor
"@objectstack/cli": patch
---

Author-time rules now gate the RUNTIME metadata write path, not just the CLI (#4463)

The 26 author-time rules `os validate` / `os build` / `os lint` share (#4409) ran on
those three commands and nowhere else. Every runtime metadata write — Studio's
designer, REST `/meta` item CRUD, an MCP/AI agent authoring a flow — reaches
`saveMetaItem`, which did a per-type Zod `safeParse` and stopped. For a tenant that
was not the weakest of four doors, it was the **only** door: a `sys_metadata`
overlay row is not in the CLI's config file, so there was no command they could run
instead. An approval flow whose `expression` approver is broken CEL
(`record.owner ==`) is Zod-valid, so it saved, registered, and failed at the node's
entry the first time it fired — the exact body `os lint` had rejected since #4409.

**One shared core, one runtime gate.**

- The rule registry moved from `packages/cli` into `@objectstack/lint`
(`AUTHORING_RULES`), and the CLI now calls it there. Five rule modules moved with
it (`lintFlowPatterns`, `lintLivenessProperties`, `lintAutonumberFormats`,
`lintViewRefs`, `data-model-rules`), unchanged. There is one table; a second one
cannot be introduced without failing `authoring-rule-wiring.test.ts`.
- New kernel-safe subpath export **`@objectstack/lint/runtime`** — the entry the
metadata write path imports. Running the gate loads neither `typescript` nor
`sucrase`, pinned by a new `runtime-lazy-deps.test.ts` alongside the existing
`lazy-deps.test.ts`, which is unchanged.
- Each registry entry now declares `surfaces` (`cli` / `runtime-publish`) plus
either the metadata `runtimeTypes` it judges or a written `surfaceReason`. The
ratchet fails an entry that answers neither.

**Behaviour**

- A `state: 'active'` `saveMetaItem` — and the draft→active promotion in
`publishMetaItem` — of a **flow** runs the flow / approval / expression /
reference rule families. A gating finding is refused with **422
`INVALID_METADATA`**, in the same structured envelope the Zod failure already
used, with `rule` / `path` / `where` / `message` / `hint` per issue.
- **Draft saves are never gated** — a draft is allowed to be half-finished and
cannot execute.
- Only the write is judged: the rules run twice (context with and without the
submitted item) and only findings the item *added* can refuse it, so a
pre-existing violation in a stored row never blocks an unrelated save. Stored
rows keep being read.
- Escape hatch **`OS_ALLOW_UNLINTED_METADATA_WRITES=1`** turns the refusal into a
loud log for a migration window. Unset it once the metadata is fixed — the
runtime executes what it published.

Only `flow` writes are gated in this pass; every other metadata type carries a
recorded reason in the registry.
2 changes: 1 addition & 1 deletion packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
import { loadConfig } from '../utils/config.js';
import { lowerCallables } from '../utils/lower-callables.js';
import { buildAccessMatrix, diffAccessMatrix } from '@objectstack/lint';
import { runAuthoringRules, splitBySeverity, authoringRulesFor } from '../lint/authoring-rules.js';
import { runAuthoringRules, splitBySeverity, authoringRulesFor } from '@objectstack/lint';
import { resolveSduiManifest } from '../utils/sdui-manifest.js';
import { preflightRequiredCapabilities, renderCapabilityMessage } from '../utils/capability-preflight.js';
import { collectAndLintDocs } from '../utils/collect-docs.js';
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import { normalizeStackInput } from '@objectstack/spec';
import { PROTOCOL_MAJOR } from '@objectstack/spec/kernel';
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
import { computeI18nCoverage, type CoverageIssue } from '../utils/i18n-coverage.js';
import { lintDataModel } from '../lint/data-model-rules.js';
import { runAuthoringRules } from '../lint/authoring-rules.js';
import { lintDataModel, runAuthoringRules } from '@objectstack/lint';
import { resolveSduiManifest } from '../utils/sdui-manifest.js';
import { collectAndLintDocs } from '../utils/collect-docs.js';
import { scoreMetadata } from '../lint/score.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
type ConversionNotice,
} from '@objectstack/spec';
import { loadConfig } from '../utils/config.js';
import { runAuthoringRules, splitBySeverity, authoringRulesFor } from '../lint/authoring-rules.js';
import { runAuthoringRules, splitBySeverity, authoringRulesFor } from '@objectstack/lint';
import { resolveSduiManifest } from '../utils/sdui-manifest.js';
import { preflightRequiredCapabilities, renderCapabilityMessage } from '../utils/capability-preflight.js';
import { collectAndLintDocs } from '../utils/collect-docs.js';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/lint/score.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
import { lintConfig } from '../commands/lint.js';
import type { LintIssue, Severity } from './data-model-rules.js';
import type { LintIssue, Severity } from '@objectstack/lint';

/** Penalty weights per issue class. Schema errors are the most severe. */
export const SCORE_WEIGHTS = {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/authoring-rule-command-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { AUTHORING_COMMANDS, runAuthoringRules, type AuthoringCommand } from '../src/lint/authoring-rules.js';
import { AUTHORING_COMMANDS, runAuthoringRules, type AuthoringCommand } from '@objectstack/lint';

const cliBin = join(fileURLToPath(new URL('.', import.meta.url)), '..', 'bin', 'run-dev.js');

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/test/data-model-rules.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { lintDataModel, lintUniqueDeclarations } from '../src/lint/data-model-rules';
import { lintDataModel, lintUniqueDeclarations } from '@objectstack/lint';
import { lintConfig } from '../src/commands/lint';

const rulesOf = (issues: { rule: string }[]) => issues.map((i) => i.rule);
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/test/validate-build-gate-parity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { join } from 'node:path';
* ## What changed, and what this file still guards
*
* The metadata rules the two commands share now come from ONE table
* (`src/lint/authoring-rules.ts`, #4409), and its own ratchet —
* (`@objectstack/lint`'s `authoring-rules.ts`, #4409/#4463), and its own ratchet —
* `src/commands/authoring-rule-wiring.test.ts` — proves all three authoring
* commands run the identical gating set. That is a stronger guarantee than the
* source diff this file used to do, and it covers `os lint` too.
Expand Down Expand Up @@ -60,7 +60,7 @@ const SHARED_NON_REGISTRY_GATES: readonly string[] = [
* Each entry is a deliberate assertion that the check CANNOT be made read-only
* — it needs the emitted artifact, the bundler, or filesystem output. A gate
* that merely *reads* the parsed stack does not belong here; wire it into
* `validate.ts`, or better, register it in `src/lint/authoring-rules.ts` so all
* `validate.ts`, or better, register it in `@objectstack/lint`'s `authoring-rules.ts` so all
* three authoring commands get it at once.
*/
const BUILD_ONLY_GATES: Readonly<Record<string, string>> = {
Expand Down Expand Up @@ -120,7 +120,7 @@ describe('os validate is the read-only superset of os build (#3782, #4409)', ()
expect(
missing,
`os build runs ${missing.length} gate(s) that os validate does not: ${missing.join(', ')}.\n` +
`Register it in packages/cli/src/lint/authoring-rules.ts so all three authoring commands run ` +
`Register it in packages/lint/src/authoring-rules.ts so all three authoring commands run ` +
`it, wire it into validate.ts by hand and add it to SHARED_NON_REGISTRY_GATES, or add it to ` +
`BUILD_ONLY_GATES with a reason.`,
).toEqual([]);
Expand Down
9 changes: 7 additions & 2 deletions packages/lint/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@objectstack/lint",
"version": "17.0.0-rc.1",
"license": "Apache-2.0",
"description": "Static, build-time validation for an ObjectStack metadata graph dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.",
"description": "Static, build-time validation for an ObjectStack metadata graph \u2014 dashboard widget bindings, CEL/predicate expressions, and more. Pure (stack) => Issue[] functions shared by the CLI's `os validate` and any other consumer (e.g. AI authoring). Depends on @objectstack/spec; never on a runtime.",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand All @@ -11,10 +11,15 @@
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./runtime": {
"types": "./dist/runtime.d.ts",
"import": "./dist/runtime.js",
"require": "./dist/runtime.cjs"
}
},
"scripts": {
"build": "tsup --config ../../tsup.config.ts",
"build": "tsup",
"dev": "tsc -w",
"test": "vitest run",
"typecheck": "tsc --noEmit"
Expand Down
Loading
Loading