From 3c99f62c3ebf2aba7716bb1d153cdf68b7fd22ce Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:53:58 +0800 Subject: [PATCH 1/2] feat(lint): extract static metadata validators into @objectstack/lint (ADR-0019 P3) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New public package @objectstack/lint holds the pure, build-time metadata validators as (stack) => Finding[] functions, so the same rules run wherever a stack can be assembled — the CLI's `os validate`/`compile` and any other consumer (notably AI authoring) — instead of being trapped in CLI internals. First release moves the two the AI build needs: - validateWidgetBindings (ADR-0021 widget→dataset→measure/dimension + aggregation coherence) - validateStackExpressions (ADR-0032 CEL/predicate validity) @objectstack/cli imports both from @objectstack/lint (was ./utils/*) — pure move, no behavior change (cli builds, 62 moved tests pass). Dependency direction is one-way lint→spec; never a runtime dep, never bundled into a frontend — which is why these do NOT live in the frontend-facing @objectstack/spec. Filesystem-coupled (lint-liveness-properties) and command-coupled (score→lintConfig) checks stay in the CLI for a later increment. --- .changeset/objectstack-lint-extraction.md | 28 ++++++++++ packages/cli/package.json | 13 +++-- packages/cli/src/commands/compile.ts | 4 +- packages/cli/src/commands/doctor.ts | 2 +- packages/cli/src/commands/lint.ts | 2 +- packages/cli/src/commands/validate.ts | 4 +- packages/lint/README.md | 31 ++++++++++ packages/lint/package.json | 56 +++++++++++++++++++ packages/lint/src/index.ts | 27 +++++++++ .../src}/validate-expressions.test.ts | 0 .../src}/validate-expressions.ts | 0 .../src}/validate-widget-bindings.test.ts | 0 .../src}/validate-widget-bindings.ts | 0 packages/lint/tsconfig.json | 9 +++ pnpm-lock.yaml | 22 ++++++++ 15 files changed, 186 insertions(+), 12 deletions(-) create mode 100644 .changeset/objectstack-lint-extraction.md create mode 100644 packages/lint/README.md create mode 100644 packages/lint/package.json create mode 100644 packages/lint/src/index.ts rename packages/{cli/src/utils => lint/src}/validate-expressions.test.ts (100%) rename packages/{cli/src/utils => lint/src}/validate-expressions.ts (100%) rename packages/{cli/src/utils => lint/src}/validate-widget-bindings.test.ts (100%) rename packages/{cli/src/utils => lint/src}/validate-widget-bindings.ts (100%) create mode 100644 packages/lint/tsconfig.json diff --git a/.changeset/objectstack-lint-extraction.md b/.changeset/objectstack-lint-extraction.md new file mode 100644 index 0000000000..7cceec6dba --- /dev/null +++ b/.changeset/objectstack-lint-extraction.md @@ -0,0 +1,28 @@ +--- +"@objectstack/lint": minor +"@objectstack/cli": patch +--- + +feat(lint): extract static metadata validators into @objectstack/lint (ADR-0019 P3) + +New public package `@objectstack/lint` holds the pure, build-time metadata +validators as `(stack) => Finding[]` functions, so the same rules run wherever a +stack can be assembled — the CLI's `os validate`/`compile` and any other +consumer (notably AI-driven authoring), instead of being trapped in CLI +internals where only the CLI could reach them. + +First release moves the two validators the AI build needs: + +- `validateWidgetBindings` — dashboard widget → dataset → measure/dimension + reference integrity + measure-aggregation coherence (ADR-0021). +- `validateStackExpressions` — CEL/predicate validity for field conditionals, + sharing rules, action visible/disabled, lifecycle hooks (ADR-0032). + +`@objectstack/cli` now imports both from `@objectstack/lint` (was `./utils/*`); +pure move, no behavior change. Dependency direction is one-way `lint → spec`; +the package never depends on a runtime and is never bundled into a frontend +(that is why the validators do NOT live in the frontend-facing `@objectstack/spec`). + +Filesystem-coupled checks (`lint-liveness-properties`) and CLI-command-coupled +ones (`score` → `lintConfig`) deliberately stay in the CLI for now; they can +move in a later increment. diff --git a/packages/cli/package.json b/packages/cli/package.json index 44963e1459..eff0bc0c47 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -53,6 +53,8 @@ "@objectstack/driver-sql": "workspace:^", "@objectstack/driver-sqlite-wasm": "workspace:^", "@objectstack/formula": "workspace:*", + "@objectstack/lint": "workspace:*", + "@objectstack/mcp": "workspace:*", "@objectstack/objectql": "workspace:^", "@objectstack/observability": "workspace:^", "@objectstack/platform-objects": "workspace:*", @@ -60,17 +62,11 @@ "@objectstack/plugin-audit": "workspace:*", "@objectstack/plugin-auth": "workspace:*", "@objectstack/plugin-email": "workspace:*", - "@objectstack/setup": "workspace:*", - "@objectstack/studio": "workspace:*", "@objectstack/plugin-hono-server": "workspace:*", - "@objectstack/mcp": "workspace:*", "@objectstack/plugin-org-scoping": "workspace:*", "@objectstack/plugin-reports": "workspace:*", "@objectstack/plugin-security": "workspace:*", "@objectstack/plugin-sharing": "workspace:*", - "@objectstack/trigger-record-change": "workspace:*", - "@objectstack/trigger-api": "workspace:*", - "@objectstack/trigger-schedule": "workspace:*", "@objectstack/plugin-webhooks": "workspace:*", "@objectstack/rest": "workspace:*", "@objectstack/runtime": "workspace:^", @@ -86,7 +82,12 @@ "@objectstack/service-realtime": "workspace:*", "@objectstack/service-settings": "workspace:*", "@objectstack/service-storage": "workspace:*", + "@objectstack/setup": "workspace:*", "@objectstack/spec": "workspace:*", + "@objectstack/studio": "workspace:*", + "@objectstack/trigger-api": "workspace:*", + "@objectstack/trigger-record-change": "workspace:*", + "@objectstack/trigger-schedule": "workspace:*", "@objectstack/types": "workspace:*", "@objectstack/verify": "workspace:*", "@oclif/core": "^4.11.9", diff --git a/packages/cli/src/commands/compile.ts b/packages/cli/src/commands/compile.ts index 6dd2eee856..1a41194ec8 100644 --- a/packages/cli/src/commands/compile.ts +++ b/packages/cli/src/commands/compile.ts @@ -8,8 +8,8 @@ import { ZodError } from 'zod'; import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec'; import { loadConfig } from '../utils/config.js'; import { lowerCallables } from '../utils/lower-callables.js'; -import { validateStackExpressions } from '../utils/validate-expressions.js'; -import { validateWidgetBindings } from '../utils/validate-widget-bindings.js'; +import { validateStackExpressions } from '@objectstack/lint'; +import { validateWidgetBindings } from '@objectstack/lint'; import { lintFlowPatterns } from '../utils/lint-flow-patterns.js'; import { lintAutonumberFormats } from '../utils/lint-autonumber-formats.js'; import { lintLivenessProperties } from '../utils/lint-liveness-properties.js'; diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 4f02634278..9daf391662 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -8,7 +8,7 @@ import path from 'path'; import { normalizeStackInput } from '@objectstack/spec'; import { printHeader, printSuccess, printWarning, printError, printStep, printInfo } from '../utils/format.js'; import { loadConfig, configExists } from '../utils/config.js'; -import { validateWidgetBindings } from '../utils/validate-widget-bindings.js'; +import { validateWidgetBindings } from '@objectstack/lint'; interface HealthCheckResult { name: string; diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 9af099b3ae..6228a5ab89 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -7,7 +7,7 @@ import { normalizeStackInput } from '@objectstack/spec'; import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js'; import { computeI18nCoverage } from '../utils/i18n-coverage.js'; import { lintDataModel } from '../lint/data-model-rules.js'; -import { validateWidgetBindings } from '../utils/validate-widget-bindings.js'; +import { validateWidgetBindings } from '@objectstack/lint'; import { collectAndLintDocs } from '../utils/collect-docs.js'; import { scoreMetadata } from '../lint/score.js'; import { runMetadataEval } from '../lint/metadata-eval.js'; diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index a9597b9057..2bd5bca0a3 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -5,8 +5,8 @@ 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 { validateStackExpressions } from '@objectstack/lint'; +import { validateWidgetBindings } from '@objectstack/lint'; import { printHeader, printKV, diff --git a/packages/lint/README.md b/packages/lint/README.md new file mode 100644 index 0000000000..3089c43379 --- /dev/null +++ b/packages/lint/README.md @@ -0,0 +1,31 @@ +# @objectstack/lint + +Static, build-time validation for an ObjectStack metadata graph. + +Every rule is a pure `(stack) => Finding[]` function — no I/O, no runtime, no +filesystem. It operates on an in-memory, schema-parsed stack object, so the +same rules run wherever a stack can be assembled: the CLI's `os validate` / +`compile`, and any other consumer (e.g. AI-driven authoring) that wants to hold +generated metadata to the same bar as hand-authored metadata. + +Dependency direction is one-way — `lint` → `@objectstack/spec` (the contract). +It never depends on a runtime and is never bundled into a frontend. + +## API + +- `validateWidgetBindings(stack)` — dashboard widget → dataset → measure/dimension + reference integrity, chart-config bindings, and measure-aggregation coherence + (e.g. SUM of a `percent` field is meaningless). +- `validateStackExpressions(stack)` — CEL/predicate validity for field + conditionals, sharing rules, action `visible`/`disabled`, lifecycle hooks + (ADR-0032). + +```ts +import { validateWidgetBindings, validateStackExpressions } from '@objectstack/lint'; + +const findings = [ + ...validateWidgetBindings(stack), + ...validateStackExpressions(stack), +]; +const errors = findings.filter((f) => f.severity === 'error'); +``` diff --git a/packages/lint/package.json b/packages/lint/package.json new file mode 100644 index 0000000000..025040eed3 --- /dev/null +++ b/packages/lint/package.json @@ -0,0 +1,56 @@ +{ + "name": "@objectstack/lint", + "version": "10.0.0", + "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.", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "scripts": { + "build": "tsup --config ../../tsup.config.ts", + "dev": "tsc -w", + "lint": "eslint src", + "test": "vitest run" + }, + "dependencies": { + "@objectstack/spec": "workspace:*", + "@objectstack/formula": "workspace:*" + }, + "devDependencies": { + "@types/node": "^26.0.0", + "typescript": "^6.0.3", + "vitest": "^4.1.9" + }, + "keywords": [ + "objectstack", + "lint", + "validation", + "metadata", + "static-analysis" + ], + "author": "ObjectStack", + "repository": { + "type": "git", + "url": "https://github.com/objectstack-ai/framework.git", + "directory": "packages/lint" + }, + "homepage": "https://objectstack.ai/docs", + "bugs": "https://github.com/objectstack-ai/framework/issues", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist", + "README.md" + ], + "engines": { + "node": ">=18.0.0" + } +} diff --git a/packages/lint/src/index.ts b/packages/lint/src/index.ts new file mode 100644 index 0000000000..b88a6fe96a --- /dev/null +++ b/packages/lint/src/index.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. +// +// @objectstack/lint — public API. +// +// Static, build-time validation over an ObjectStack metadata graph. Every rule +// is a pure `(stack) => Finding[]` function: no I/O, no runtime, no filesystem +// — it operates on an in-memory, schema-parsed stack object. Shared by the +// CLI's `os validate`/`compile` AND any other consumer (e.g. AI authoring) so +// hand-authored and generated apps are held to the same bar (ADR-0019). +// +// Dependency direction is one-way: lint → @objectstack/spec (the contract). +// It never depends on a runtime, and it is never bundled into a frontend. + +export { + validateWidgetBindings, + WIDGET_DATASET_UNKNOWN, + WIDGET_DIMENSION_UNKNOWN, + WIDGET_MEASURE_UNKNOWN, + CHART_FIELD_UNKNOWN, + CHART_CONFIG_MISSING, + TABLE_COUNT_ONLY, + MEASURE_AGGREGATE_INCOHERENT, +} from './validate-widget-bindings.js'; +export type { WidgetBindingFinding, WidgetBindingSeverity } from './validate-widget-bindings.js'; + +export { validateStackExpressions } from './validate-expressions.js'; +export type { ExprIssue } from './validate-expressions.js'; diff --git a/packages/cli/src/utils/validate-expressions.test.ts b/packages/lint/src/validate-expressions.test.ts similarity index 100% rename from packages/cli/src/utils/validate-expressions.test.ts rename to packages/lint/src/validate-expressions.test.ts diff --git a/packages/cli/src/utils/validate-expressions.ts b/packages/lint/src/validate-expressions.ts similarity index 100% rename from packages/cli/src/utils/validate-expressions.ts rename to packages/lint/src/validate-expressions.ts diff --git a/packages/cli/src/utils/validate-widget-bindings.test.ts b/packages/lint/src/validate-widget-bindings.test.ts similarity index 100% rename from packages/cli/src/utils/validate-widget-bindings.test.ts rename to packages/lint/src/validate-widget-bindings.test.ts diff --git a/packages/cli/src/utils/validate-widget-bindings.ts b/packages/lint/src/validate-widget-bindings.ts similarity index 100% rename from packages/cli/src/utils/validate-widget-bindings.ts rename to packages/lint/src/validate-widget-bindings.ts diff --git a/packages/lint/tsconfig.json b/packages/lint/tsconfig.json new file mode 100644 index 0000000000..5e3095a8a4 --- /dev/null +++ b/packages/lint/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src" + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 628a520471..25f03e47fc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -449,6 +449,9 @@ importers: '@objectstack/formula': specifier: workspace:* version: link:../formula + '@objectstack/lint': + specifier: workspace:* + version: link:../lint '@objectstack/mcp': specifier: workspace:* version: link:../mcp @@ -891,6 +894,25 @@ importers: specifier: ^4.1.9 version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.0.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/lint: + dependencies: + '@objectstack/formula': + specifier: workspace:* + version: link:../formula + '@objectstack/spec': + specifier: workspace:* + version: link:../spec + devDependencies: + '@types/node': + specifier: ^26.0.0 + version: 26.0.0 + typescript: + specifier: ^6.0.3 + version: 6.0.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@26.0.0)(@vitest/coverage-v8@4.1.9)(happy-dom@20.10.2)(msw@2.14.6(@types/node@26.0.0)(typescript@6.0.3))(vite@8.0.16(@types/node@26.0.0)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + packages/mcp: dependencies: '@modelcontextprotocol/sdk': From 875e4393cd02f35cf3a183097776f70eca365446 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Mon, 22 Jun 2026 22:58:05 +0800 Subject: [PATCH 2/2] chore(changeset): add @objectstack/lint to the fixed version group New public package must be in the changesets 'fixed' lockstep group (CI gate 'Validate Package Dependencies'). --- .changeset/config.json | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/config.json b/.changeset/config.json index 65418a2c58..d1d4724640 100644 --- a/.changeset/config.json +++ b/.changeset/config.json @@ -23,6 +23,7 @@ "@objectstack/objectql", "@objectstack/observability", "@objectstack/formula", + "@objectstack/lint", "@objectstack/platform-objects", "@objectstack/studio", "@objectstack/setup",