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
1 change: 1 addition & 0 deletions .changeset/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@objectstack/objectql",
"@objectstack/observability",
"@objectstack/formula",
"@objectstack/lint",
"@objectstack/platform-objects",
"@objectstack/studio",
"@objectstack/setup",
Expand Down
28 changes: 28 additions & 0 deletions .changeset/objectstack-lint-extraction.md
Original file line number Diff line number Diff line change
@@ -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.
13 changes: 7 additions & 6 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,20 @@
"@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:*",
"@objectstack/plugin-approvals": "workspace:*",
"@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:^",
Expand All @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/compile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/commands/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions packages/lint/README.md
Original file line number Diff line number Diff line change
@@ -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');
```
56 changes: 56 additions & 0 deletions packages/lint/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
27 changes: 27 additions & 0 deletions packages/lint/src/index.ts
Original file line number Diff line number Diff line change
@@ -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';
9 changes: 9 additions & 0 deletions packages/lint/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.spec.ts", "**/*.test.ts"]
}
22 changes: 22 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.