Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
16 changes: 16 additions & 0 deletions .changeset/adr-0058-expression-compiler.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
"@objectstack/formula": minor
"@objectstack/plugin-security": minor
"@objectstack/plugin-sharing": minor
---

ADR-0058 — expression & predicate surface unification. Adds the canonical
CEL→FilterCondition pushdown compiler in `@objectstack/formula`
(`compileCelToFilter`, `isPushdownableCel`, `lowerCelAst`) plus an in-memory
`matchesFilterCondition` backend (one AST, three backends). `plugin-security`
(RLS `using`, via a SQL bridge) and `plugin-sharing` (`celToFilter`) cut over to
it, retiring the bespoke regex/field-equality front-ends. Compound sharing
conditions now compile and enforce end-to-end (closes #1887). The RLS `check`
clause is now enforced on the write post-image (insert/by-id update), fail-closed.
Non-pushdownable predicates (arithmetic, functions, subqueries, cross-object) are
an authoring compile error, never silently dropped (ADR-0049/0055).
3 changes: 3 additions & 0 deletions packages/dogfood/test/authz-conformance.matrix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@ export const AUTHZ_CONFORMANCE: AuthzPrimitive[] = [
enforcement: 'plugin-security/security-plugin.ts computeRlsFilter (AND-injected)', proof: 'rls-fixture.dogfood.test.ts' },
{ id: 'rls-by-id-write', summary: 'by-id write enforcement (#1994)', state: 'enforced',
enforcement: 'plugin-security/security-plugin.ts pre-image re-read', proof: 'rls-fixture.dogfood.test.ts' },
{ id: 'rls-write-check', summary: 'RLS `check` write post-image validation (ADR-0058 D4)', state: 'enforced',
enforcement: 'plugin-security/security-plugin.ts step 3.6 — compileCelToFilter + matchesFilterCondition against the post-image (fail-closed)',
note: 'Unit-proven in plugin-security/security-plugin.test.ts (RLS check enforcement); see ADR-0058 D7 ledger.' },
{ id: 'owd-private', summary: 'OWD private (owner-only)', state: 'enforced',
enforcement: 'plugin-sharing/sharing-service.ts effectiveSharingModel=private', proof: 'showcase-private-owd.dogfood.test.ts' },
{ id: 'owd-public-read', summary: 'OWD public_read (everyone reads, owner writes)', state: 'enforced',
Expand Down
148 changes: 148 additions & 0 deletions packages/dogfood/test/expression-conformance.ledger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0058 D7 — Expression Surface Conformance ledger.
//
// The durable encoding of the ADR-0058 audit: ONE classification per expression-
// holding declaration across the spec. Every surface is in exactly one honest
// state, names its evaluator/compiler site, declares its fail-policy (ADR-0058
// D5), and — for the security-critical COMPILE surfaces — references a proof.
//
// `mode` is a property of the surface (ADR-0058 D6): RLS `using`/`check` and the
// sharing `condition` are COMPILE (pushdown via the canonical
// @objectstack/formula compiler); everything else is INTERPRET (per-record, via
// the celEngine). There is NO silent fallback from compile to interpret.
//
// The companion test (`expression-conformance.test.ts`) RE-DISCOVERS every
// `ExpressionInputSchema` field declaration in `packages/spec/src` (plus the RLS
// `using`/`check` string predicates) and asserts each is `covers`-ed by exactly
// one row. A NEW expression surface that nobody classified — the #1887 class of
// "declared-but-unwired predicate" — breaks the build.

export type ExprMode = 'compile' | 'interpret';
export type ExprDialect = 'cel' | 'cron' | 'template' | 'js';
export type ExprState = 'enforced' | 'experimental' | 'removed';
/** ADR-0058 D5 fail-policy tiers. */
export type FailPolicy = 'compile-error' | 'fail-closed' | 'fail-soft-log' | 'throw';

export interface ExprSurface {
id: string;
summary: string;
dialect: ExprDialect;
mode: ExprMode;
state: ExprState;
failPolicy: FailPolicy;
/** Runtime evaluator / compiler site. */
site: string;
/** `file:field` surfaces (relative to packages/spec/src) this row classifies — the ratchet keys. */
covers: string[];
/** Proof path (repo-root-relative). Required for ENFORCED COMPILE (security) rows. */
proof?: string;
/** Rationale for experimental/removed, or a roadmap pointer. */
note?: string;
}

export const EXPRESSION_SURFACE: ExprSurface[] = [
// ── COMPILE (pushdown) — security-critical; canonical-compiler-reachable + proven ──
{
id: 'rls-using',
summary: 'RLS `using` read / pre-image predicate',
dialect: 'cel', mode: 'compile', state: 'enforced', failPolicy: 'fail-closed',
site: 'plugin-security/rls-compiler.ts → @objectstack/formula compileCelToFilter (legacy SQL bridged); AND-injected by security-plugin computeRlsFilter + service-analytics read-scope-sql',
covers: ['security/rls.zod.ts:using'],
proof: 'packages/dogfood/test/rls-fixture.dogfood.test.ts',
},
{
id: 'rls-check',
summary: 'RLS `check` write post-image validation (ADR-0058 D4)',
dialect: 'cel', mode: 'compile', state: 'enforced', failPolicy: 'fail-closed',
site: 'plugin-security/security-plugin.ts step 3.6 → compileCelToFilter + @objectstack/formula matchesFilterCondition',
covers: ['security/rls.zod.ts:check'],
proof: 'packages/plugins/plugin-security/src/security-plugin.test.ts',
},
{
id: 'sharing-condition',
summary: 'sharing-rule `condition` → criteria_json (ADR-0058 D3, closes #1887)',
dialect: 'cel', mode: 'compile', state: 'enforced', failPolicy: 'fail-closed',
site: 'plugin-sharing/bootstrap-declared-sharing-rules.ts celToFilter → compileCelToFilter; matched by sharing-rule-service findMatchingRecords',
covers: ['security/sharing.zod.ts:condition'],
proof: 'packages/plugins/plugin-sharing/src/sharing-rule.test.ts',
},

// ── INTERPRET (per-record) — classified, fail-soft per ADR-0058 D5 ──
{
id: 'cel-validation',
summary: 'object validation predicate (condition / when)',
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log',
site: '@objectstack/formula celEngine (interpret) via the validation runner',
covers: ['data/validation.zod.ts:condition', 'data/validation.zod.ts:when'],
},
{
id: 'cel-hook',
summary: 'hook gate condition',
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log',
site: '@objectstack/formula celEngine (interpret) via the hook runner',
covers: ['data/hook.zod.ts:condition'],
},
{
id: 'cel-formula',
summary: 'computed / formula field + mapping / graphql / feature expressions',
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log',
site: '@objectstack/formula celEngine (interpret)',
covers: [
'data/field.zod.ts:expression',
'shared/mapping.zod.ts:expression',
'api/graphql.zod.ts:expression',
'kernel/feature.zod.ts:expression',
],
},
{
id: 'cel-field-rule',
summary: 'field UI rules (requiredWhen / readonlyWhen / visibleWhen / conditionalRequired)',
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log',
site: '@objectstack/formula celEngine (interpret) — console (objectui) + server',
covers: [
'data/field.zod.ts:requiredWhen',
'data/field.zod.ts:readonlyWhen',
'data/field.zod.ts:visibleWhen',
'data/field.zod.ts:conditionalRequired',
],
},
{
id: 'cel-ui',
summary: 'UI visibility / routing / submit predicates',
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'fail-soft-log',
site: 'console (objectui) SchemaRenderer + server celEngine (interpret)',
covers: [
'data/object.zod.ts:visibleOn',
'ui/action.zod.ts:visible',
'ui/app.zod.ts:visible',
'ui/page.zod.ts:visibility',
'ui/view.zod.ts:condition',
'ui/view.zod.ts:visibleOn',
'ui/component.zod.ts:onSubmit',
'system/settings-manifest.zod.ts:visible',
],
},
{
id: 'cel-flow',
summary: 'flow / sync / loader branching + filter predicates',
dialect: 'cel', mode: 'interpret', state: 'enforced', failPolicy: 'throw',
site: '@objectstack/formula celEngine (interpret) via the automation runtime',
covers: [
'automation/flow.zod.ts:condition',
'automation/sync.zod.ts:condition',
'kernel/metadata-loader.zod.ts:filter',
],
},
{
id: 'cel-advanced-policy',
summary: 'advanced security / versioning policy conditions',
dialect: 'cel', mode: 'interpret', state: 'experimental', failPolicy: 'fail-closed',
site: '(no runtime consumer yet)',
covers: [
'kernel/plugin-security-advanced.zod.ts:condition',
'kernel/plugin-versioning.zod.ts:condition',
],
note: 'EXPERIMENTAL — declared policy conditions with no runtime evaluator yet (ADR-0056 D8 / ADR-0049 tracking).',
},
];
117 changes: 117 additions & 0 deletions packages/dogfood/test/expression-conformance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
//
// ADR-0058 D7 — the Expression Surface Conformance ledger is a CHECKED artifact.
// These assertions make "every expression-holding declaration is classified in
// exactly one honest state, every COMPILE security surface is reachable by the
// canonical compiler and proven, and no new surface slips in unclassified" a
// green CI gate. A new ExpressionInputSchema field with no ledger row — the
// #1887 class of declared-but-unwired predicate — breaks the build.

import { describe, it, expect } from 'vitest';
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative } from 'node:path';
import { EXPRESSION_SURFACE, type ExprSurface } from './expression-conformance.ledger.js';

const HERE = dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = join(HERE, '../../..');
const SPEC_SRC = join(REPO_ROOT, 'packages/spec/src');

const MODES = new Set(['compile', 'interpret']);
const STATES = new Set(['enforced', 'experimental', 'removed']);
const FAIL_POLICIES = new Set(['compile-error', 'fail-closed', 'fail-soft-log', 'throw']);
const DIALECTS = new Set(['cel', 'cron', 'template', 'js']);

/** Re-discover every expression surface in the spec — the SAME scan the ledger encodes. */
function discoverSurfaces(): Set<string> {
const found = new Set<string>();
const walk = (dir: string) => {
for (const e of readdirSync(dir)) {
const p = join(dir, e);
if (statSync(p).isDirectory()) walk(p);
else if (e.endsWith('.zod.ts')) {
const rel = relative(SPEC_SRC, p);
for (const line of readFileSync(p, 'utf8').split('\n')) {

Check failure

Code scanning / CodeQL

Potential file system race condition High test

The file may have changed since it
was checked
.
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
const m = line.match(/^\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*ExpressionInputSchema\b/);
if (m) found.add(`${rel}:${m[1]}`);
}
}
}
};
walk(SPEC_SRC);
// RLS using/check are expression predicates too (legacy z.string() fields, not
// ExpressionInputSchema) — classify them explicitly so they cannot drift.
found.add('security/rls.zod.ts:using');
found.add('security/rls.zod.ts:check');
return found;
}

describe('ADR-0058 D7 — expression surface conformance ledger', () => {
it('has no duplicate ids', () => {
const ids = EXPRESSION_SURFACE.map((s) => s.id);
expect(new Set(ids).size).toBe(ids.length);
});

it('every row has a valid mode / state / dialect / fail-policy', () => {
for (const s of EXPRESSION_SURFACE) {
expect(MODES.has(s.mode), `${s.id}: mode '${s.mode}'`).toBe(true);
expect(STATES.has(s.state), `${s.id}: state '${s.state}'`).toBe(true);
expect(DIALECTS.has(s.dialect), `${s.id}: dialect '${s.dialect}'`).toBe(true);
expect(FAIL_POLICIES.has(s.failPolicy), `${s.id}: failPolicy '${s.failPolicy}'`).toBe(true);
expect(s.site && s.site.length > 0, `${s.id}: missing site`).toBe(true);
expect(Array.isArray(s.covers) && s.covers.length > 0, `${s.id}: empty covers`).toBe(true);
}
});

it('every COMPILE row is security fail-closed, names the canonical compiler, and is proven', () => {
for (const s of EXPRESSION_SURFACE.filter((x) => x.mode === 'compile')) {
expect(s.failPolicy, `${s.id}: a compile/security surface must fail closed`).toBe('fail-closed');
// Compiler-reachable: the site must reference the canonical compiler entry.
expect(/compileCelToFilter|celToFilter|matchesFilterCondition/.test(s.site), `${s.id}: site does not name the canonical compiler`).toBe(true);
expect(s.proof, `${s.id}: an enforced compile surface must carry a proof`).toBeTruthy();
}
});

it('every referenced proof FILE EXISTS (the proof ratchet)', () => {
const broken: string[] = [];
for (const s of EXPRESSION_SURFACE as ExprSurface[]) {
if (s.proof && !existsSync(join(REPO_ROOT, s.proof))) broken.push(`${s.id} → ${s.proof}`);
}
expect(broken, `ledger proofs missing on disk: ${broken.join(', ')}`).toEqual([]);
});

it('every experimental/removed row carries a note (honest rationale)', () => {
const missing = EXPRESSION_SURFACE.filter((s) => s.state !== 'enforced' && !s.note).map((s) => s.id);
expect(missing, `non-enforced rows missing a note: ${missing.join(', ')}`).toEqual([]);
});

// ── THE RATCHET ──────────────────────────────────────────────────────────
it('classifies EVERY expression surface in the spec — no unclassified declaration', () => {
const discovered = discoverSurfaces();
const covered = new Set(EXPRESSION_SURFACE.flatMap((s) => s.covers));

// (a) every discovered surface is classified by some row
const unclassified = [...discovered].filter((s) => !covered.has(s)).sort();
expect(
unclassified,
`NEW unclassified expression surface(s) — add a row to expression-conformance.ledger.ts ` +
`(ADR-0058 D7): ${unclassified.join(', ')}`,
).toEqual([]);

// (b) no stale `covers` entry that no longer exists in the spec
const stale = [...covered].filter((s) => !discovered.has(s)).sort();
expect(stale, `STALE ledger covers (surface removed from spec): ${stale.join(', ')}`).toEqual([]);
});

it('each surface is covered by EXACTLY ONE row (no double classification)', () => {
const seen = new Map<string, string>();
const dup: string[] = [];
for (const s of EXPRESSION_SURFACE) {
for (const c of s.covers) {
if (seen.has(c)) dup.push(`${c} (in ${seen.get(c)} and ${s.id})`);
else seen.set(c, s.id);
}
}
expect(dup, `surfaces classified by more than one row: ${dup.join(', ')}`).toEqual([]);
});
});
Loading
Loading