Skip to content

Commit 63f3219

Browse files
feat(lint): extract static metadata validators into @objectstack/lint (ADR-0019 P3) (#2215)
* 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 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. * 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'). --------- Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com>
1 parent b496498 commit 63f3219

16 files changed

Lines changed: 187 additions & 12 deletions

.changeset/config.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"@objectstack/objectql",
2424
"@objectstack/observability",
2525
"@objectstack/formula",
26+
"@objectstack/lint",
2627
"@objectstack/platform-objects",
2728
"@objectstack/studio",
2829
"@objectstack/setup",
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
"@objectstack/lint": minor
3+
"@objectstack/cli": patch
4+
---
5+
6+
feat(lint): extract static metadata validators into @objectstack/lint (ADR-0019 P3)
7+
8+
New public package `@objectstack/lint` holds the pure, build-time metadata
9+
validators as `(stack) => Finding[]` functions, so the same rules run wherever a
10+
stack can be assembled — the CLI's `os validate`/`compile` and any other
11+
consumer (notably AI-driven authoring), instead of being trapped in CLI
12+
internals where only the CLI could reach them.
13+
14+
First release moves the two validators the AI build needs:
15+
16+
- `validateWidgetBindings` — dashboard widget → dataset → measure/dimension
17+
reference integrity + measure-aggregation coherence (ADR-0021).
18+
- `validateStackExpressions` — CEL/predicate validity for field conditionals,
19+
sharing rules, action visible/disabled, lifecycle hooks (ADR-0032).
20+
21+
`@objectstack/cli` now imports both from `@objectstack/lint` (was `./utils/*`);
22+
pure move, no behavior change. Dependency direction is one-way `lint → spec`;
23+
the package never depends on a runtime and is never bundled into a frontend
24+
(that is why the validators do NOT live in the frontend-facing `@objectstack/spec`).
25+
26+
Filesystem-coupled checks (`lint-liveness-properties`) and CLI-command-coupled
27+
ones (`score``lintConfig`) deliberately stay in the CLI for now; they can
28+
move in a later increment.

packages/cli/package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -53,24 +53,20 @@
5353
"@objectstack/driver-sql": "workspace:^",
5454
"@objectstack/driver-sqlite-wasm": "workspace:^",
5555
"@objectstack/formula": "workspace:*",
56+
"@objectstack/lint": "workspace:*",
57+
"@objectstack/mcp": "workspace:*",
5658
"@objectstack/objectql": "workspace:^",
5759
"@objectstack/observability": "workspace:^",
5860
"@objectstack/platform-objects": "workspace:*",
5961
"@objectstack/plugin-approvals": "workspace:*",
6062
"@objectstack/plugin-audit": "workspace:*",
6163
"@objectstack/plugin-auth": "workspace:*",
6264
"@objectstack/plugin-email": "workspace:*",
63-
"@objectstack/setup": "workspace:*",
64-
"@objectstack/studio": "workspace:*",
6565
"@objectstack/plugin-hono-server": "workspace:*",
66-
"@objectstack/mcp": "workspace:*",
6766
"@objectstack/plugin-org-scoping": "workspace:*",
6867
"@objectstack/plugin-reports": "workspace:*",
6968
"@objectstack/plugin-security": "workspace:*",
7069
"@objectstack/plugin-sharing": "workspace:*",
71-
"@objectstack/trigger-record-change": "workspace:*",
72-
"@objectstack/trigger-api": "workspace:*",
73-
"@objectstack/trigger-schedule": "workspace:*",
7470
"@objectstack/plugin-webhooks": "workspace:*",
7571
"@objectstack/rest": "workspace:*",
7672
"@objectstack/runtime": "workspace:^",
@@ -86,7 +82,12 @@
8682
"@objectstack/service-realtime": "workspace:*",
8783
"@objectstack/service-settings": "workspace:*",
8884
"@objectstack/service-storage": "workspace:*",
85+
"@objectstack/setup": "workspace:*",
8986
"@objectstack/spec": "workspace:*",
87+
"@objectstack/studio": "workspace:*",
88+
"@objectstack/trigger-api": "workspace:*",
89+
"@objectstack/trigger-record-change": "workspace:*",
90+
"@objectstack/trigger-schedule": "workspace:*",
9091
"@objectstack/types": "workspace:*",
9192
"@objectstack/verify": "workspace:*",
9293
"@oclif/core": "^4.11.9",

packages/cli/src/commands/compile.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@ import { ZodError } from 'zod';
88
import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
99
import { loadConfig } from '../utils/config.js';
1010
import { lowerCallables } from '../utils/lower-callables.js';
11-
import { validateStackExpressions } from '../utils/validate-expressions.js';
12-
import { validateWidgetBindings } from '../utils/validate-widget-bindings.js';
11+
import { validateStackExpressions } from '@objectstack/lint';
12+
import { validateWidgetBindings } from '@objectstack/lint';
1313
import { lintFlowPatterns } from '../utils/lint-flow-patterns.js';
1414
import { lintAutonumberFormats } from '../utils/lint-autonumber-formats.js';
1515
import { lintLivenessProperties } from '../utils/lint-liveness-properties.js';

packages/cli/src/commands/doctor.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import path from 'path';
88
import { normalizeStackInput } from '@objectstack/spec';
99
import { printHeader, printSuccess, printWarning, printError, printStep, printInfo } from '../utils/format.js';
1010
import { loadConfig, configExists } from '../utils/config.js';
11-
import { validateWidgetBindings } from '../utils/validate-widget-bindings.js';
11+
import { validateWidgetBindings } from '@objectstack/lint';
1212

1313
interface HealthCheckResult {
1414
name: string;

packages/cli/src/commands/lint.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { normalizeStackInput } from '@objectstack/spec';
77
import { loadConfig, BUNDLE_REQUIRE_EXTERNALS } from '../utils/config.js';
88
import { computeI18nCoverage } from '../utils/i18n-coverage.js';
99
import { lintDataModel } from '../lint/data-model-rules.js';
10-
import { validateWidgetBindings } from '../utils/validate-widget-bindings.js';
10+
import { validateWidgetBindings } from '@objectstack/lint';
1111
import { collectAndLintDocs } from '../utils/collect-docs.js';
1212
import { scoreMetadata } from '../lint/score.js';
1313
import { runMetadataEval } from '../lint/metadata-eval.js';

packages/cli/src/commands/validate.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import chalk from 'chalk';
55
import { ZodError } from 'zod';
66
import { ObjectStackDefinitionSchema, normalizeStackInput } from '@objectstack/spec';
77
import { loadConfig } from '../utils/config.js';
8-
import { validateStackExpressions } from '../utils/validate-expressions.js';
9-
import { validateWidgetBindings } from '../utils/validate-widget-bindings.js';
8+
import { validateStackExpressions } from '@objectstack/lint';
9+
import { validateWidgetBindings } from '@objectstack/lint';
1010
import {
1111
printHeader,
1212
printKV,

packages/lint/README.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# @objectstack/lint
2+
3+
Static, build-time validation for an ObjectStack metadata graph.
4+
5+
Every rule is a pure `(stack) => Finding[]` function — no I/O, no runtime, no
6+
filesystem. It operates on an in-memory, schema-parsed stack object, so the
7+
same rules run wherever a stack can be assembled: the CLI's `os validate` /
8+
`compile`, and any other consumer (e.g. AI-driven authoring) that wants to hold
9+
generated metadata to the same bar as hand-authored metadata.
10+
11+
Dependency direction is one-way — `lint``@objectstack/spec` (the contract).
12+
It never depends on a runtime and is never bundled into a frontend.
13+
14+
## API
15+
16+
- `validateWidgetBindings(stack)` — dashboard widget → dataset → measure/dimension
17+
reference integrity, chart-config bindings, and measure-aggregation coherence
18+
(e.g. SUM of a `percent` field is meaningless).
19+
- `validateStackExpressions(stack)` — CEL/predicate validity for field
20+
conditionals, sharing rules, action `visible`/`disabled`, lifecycle hooks
21+
(ADR-0032).
22+
23+
```ts
24+
import { validateWidgetBindings, validateStackExpressions } from '@objectstack/lint';
25+
26+
const findings = [
27+
...validateWidgetBindings(stack),
28+
...validateStackExpressions(stack),
29+
];
30+
const errors = findings.filter((f) => f.severity === 'error');
31+
```

packages/lint/package.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"name": "@objectstack/lint",
3+
"version": "10.0.0",
4+
"license": "Apache-2.0",
5+
"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.",
6+
"type": "module",
7+
"main": "dist/index.js",
8+
"types": "dist/index.d.ts",
9+
"exports": {
10+
".": {
11+
"types": "./dist/index.d.ts",
12+
"import": "./dist/index.js",
13+
"require": "./dist/index.cjs"
14+
}
15+
},
16+
"scripts": {
17+
"build": "tsup --config ../../tsup.config.ts",
18+
"dev": "tsc -w",
19+
"lint": "eslint src",
20+
"test": "vitest run"
21+
},
22+
"dependencies": {
23+
"@objectstack/spec": "workspace:*",
24+
"@objectstack/formula": "workspace:*"
25+
},
26+
"devDependencies": {
27+
"@types/node": "^26.0.0",
28+
"typescript": "^6.0.3",
29+
"vitest": "^4.1.9"
30+
},
31+
"keywords": [
32+
"objectstack",
33+
"lint",
34+
"validation",
35+
"metadata",
36+
"static-analysis"
37+
],
38+
"author": "ObjectStack",
39+
"repository": {
40+
"type": "git",
41+
"url": "https://github.com/objectstack-ai/framework.git",
42+
"directory": "packages/lint"
43+
},
44+
"homepage": "https://objectstack.ai/docs",
45+
"bugs": "https://github.com/objectstack-ai/framework/issues",
46+
"publishConfig": {
47+
"access": "public"
48+
},
49+
"files": [
50+
"dist",
51+
"README.md"
52+
],
53+
"engines": {
54+
"node": ">=18.0.0"
55+
}
56+
}

packages/lint/src/index.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
//
3+
// @objectstack/lint — public API.
4+
//
5+
// Static, build-time validation over an ObjectStack metadata graph. Every rule
6+
// is a pure `(stack) => Finding[]` function: no I/O, no runtime, no filesystem
7+
// — it operates on an in-memory, schema-parsed stack object. Shared by the
8+
// CLI's `os validate`/`compile` AND any other consumer (e.g. AI authoring) so
9+
// hand-authored and generated apps are held to the same bar (ADR-0019).
10+
//
11+
// Dependency direction is one-way: lint → @objectstack/spec (the contract).
12+
// It never depends on a runtime, and it is never bundled into a frontend.
13+
14+
export {
15+
validateWidgetBindings,
16+
WIDGET_DATASET_UNKNOWN,
17+
WIDGET_DIMENSION_UNKNOWN,
18+
WIDGET_MEASURE_UNKNOWN,
19+
CHART_FIELD_UNKNOWN,
20+
CHART_CONFIG_MISSING,
21+
TABLE_COUNT_ONLY,
22+
MEASURE_AGGREGATE_INCOHERENT,
23+
} from './validate-widget-bindings.js';
24+
export type { WidgetBindingFinding, WidgetBindingSeverity } from './validate-widget-bindings.js';
25+
26+
export { validateStackExpressions } from './validate-expressions.js';
27+
export type { ExprIssue } from './validate-expressions.js';

0 commit comments

Comments
 (0)