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
46 changes: 46 additions & 0 deletions .changeset/expression-contract-present-tense.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
---
"@objectstack/spec": patch
---

fix(spec): the Expression contract is stated in the present tense — the M9.1 / M9.2 phase language is dropped (#17849)

Clause-②: no

No accept-set change. `ExpressionSchema` still accepts `source` OR `ast`, every
evaluated slot still requires a non-blank `source`, and no key is added, renamed
or retired. What moves is the text six citation sites carried.

Those docblocks promised a two-phase roadmap — "Phase 1 (M9.1): `source` is the
canonical persisted form … Phase 2 (M9.2+): `ast` becomes required in build
output" — that no ADR ever chartered, and the refusal sentence an author reads
carried the phase id inside it. #17323 ruled the promise removed: `ast` stays an
accepted optional structured value with no promise of becoming required. The
contract is now written as it actually is:

- `source` is the canonical persisted form — it is what the engine evaluates;
- `ast` is accepted beside it as an optional opaque structured value, and
carries no promise of becoming required;
- a slot whose value the engine RUNS requires `source`, which is what
`EvaluatedExpressionSchema` spells out.

**The one published string that moves** is `EVALUATED_EXPRESSION_SOURCE_REQUIRED`,
the sentence an author reads when an evaluated slot refuses a non-evaluable
envelope. It loses four words and nothing else:

> … the expression engine evaluates `source` (the canonical persisted form of
> phase M9.1) and cannot evaluate `ast` alone …

now reads

> … the expression engine evaluates `source` (the canonical persisted form) and
> cannot evaluate `ast` alone …

Nothing parses that sentence for its content: every consumer imports the
constant by name, and the two pending changesets that quote it verbatim
(`flow-edge-condition-evaluated-slot`,
`blank-node-condition-refused-at-registration`) already carry the new wording,
so the quote stays a quote.

The `packages/formula` half of the same ruling — `cel-engine.ts`'s AST-only arm
and `normalize.ts`'s header — is comment-only and publishes nothing from that
package (`@objectstack/formula` ships `dist` alone), so it is not graded here.
7 changes: 4 additions & 3 deletions packages/formula/src/cel-engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1715,9 +1715,10 @@ export const celEngine: DialectEngine = {
const source = expr.source;
if (typeof source !== 'string' || source.length === 0) {
// AST-only inputs: cel-js does not currently expose a public API to
// re-execute a parsed AST without re-serializing. We persist `source`
// as the canonical form during M9.1 and revisit AST-only execution in
// M9.7 when we cut the spec persistence over.
// re-execute a parsed AST without re-serializing. `source` is the
// canonical persisted form and is what this engine evaluates, so an
// envelope carrying only `ast` has no evaluable form here and is
// reported as such rather than run.
return {
ok: false,
error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },
Expand Down
3 changes: 2 additions & 1 deletion packages/formula/src/normalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
*
* 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.
* 2. The persisted envelope carries an `ast` field produced by the dialect
* engine (M9.2 deliverable). Source is retained for round-trip / debug.
* engine, beside the canonical `source` the engine evaluates. `ast` is an
* optional opaque structured value; it never replaces `source`.
*
* Spec layer cannot do step 2 because it must remain dependency-free; this
* package owns the engine import and therefore the AST step.
Expand Down
32 changes: 17 additions & 15 deletions packages/spec/src/shared/expression.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,14 @@ export type ExpressionMeta = z.input<typeof ExpressionMetaSchema>;
/**
* Canonical Expression envelope.
*
* Phase 1 (M9.1): `source` is the canonical persisted form. `ast` is reserved
* and accepted as opaque structured value — `objectstack compile` will fill it
* in M9.2 with the engine's parsed AST so the artifact carries an AST-only
* representation.
* `source` is the canonical persisted form: it is what the expression engine
* evaluates. `ast` is accepted beside it as an optional opaque structured
* value — `objectstack compile` fills it with the engine's parsed AST, and
* each engine validates its own shape — and it carries no promise of becoming
* required.
*
* Phase 2 (M9.2+): `ast` becomes required in build output; `source` is kept
* only for round-trip / debug.
* A slot whose value the engine RUNS requires `source`; the envelope that
* spells that out is {@link EvaluatedExpressionSchema}.
*/
export const ExpressionSchema = z.object({
/** Which engine evaluates `source` / `ast`. */
Expand Down Expand Up @@ -111,7 +112,7 @@ export type Expression = z.input<typeof ExpressionSchema>;
*/
export const EVALUATED_EXPRESSION_SOURCE_REQUIRED =
'An expression in an evaluated slot needs a non-blank `source`: the expression engine evaluates `source` '
+ '(the canonical persisted form of phase M9.1) and cannot evaluate `ast` alone, so an envelope carrying only '
+ '(the canonical persisted form) and cannot evaluate `ast` alone, so an envelope carrying only '
+ '`ast`, or a `source` that is blank after trimming, would validate and register and then fault at run time. '
+ 'Write `{ dialect: \'cel\', source: \'…\' }`.';

Expand All @@ -134,11 +135,12 @@ export const EVALUATED_EXPRESSION_SOURCE_REQUIRED =
* parses it untrimmed and faults).
*
* `ExpressionSchema` itself is NOT narrowed: it is the persistence contract,
* and its docblock declares that `ast` becomes required in build output at
* phase M9.2. When AST-only evaluation lands, this schema is the one place to
* revisit — relax `source` and require "`source` or `ast`, whichever the
* engine evaluates" — and every evaluated slot composes it, so that flip is
* one edit rather than a per-slot unwinding.
* and its docblock declares `ast` an optional opaque structured value that
* carries no promise of becoming required. If AST-only evaluation is ever
* chartered, this schema is the one place to revisit — relax `source` and
* require "`source` or `ast`, whichever the engine evaluates" — and every
* evaluated slot composes it, so that flip is one edit rather than a per-slot
* unwinding.
*
* Spelled as a property override rather than an object-level `.refine`, for a
* measured reason: Zod runs an object's refinements even after a property has
Expand All @@ -160,7 +162,7 @@ export const EVALUATED_EXPRESSION_SOURCE_REQUIRED =
export const EvaluatedExpressionSchema = ExpressionSchema.safeExtend({
/**
* Surface syntax — required and non-blank in an evaluated slot: it is what
* the engine evaluates (M9.1), and `ast` alone cannot be run.
* the engine evaluates, and `ast` alone cannot be run.
*/
source: z.string({ error: () => EVALUATED_EXPRESSION_SOURCE_REQUIRED })
.refine((source) => source.trim().length > 0, { message: EVALUATED_EXPRESSION_SOURCE_REQUIRED }),
Expand Down Expand Up @@ -420,8 +422,8 @@ export function expression(source: string, dialect: ExpressionDialect = 'cel', m
*
* Each helper produces an {@link Expression} envelope with `dialect: 'cel'`
* and the rendered template string as `source`. The CLI `objectstack compile`
* step (M9.2) parses these into ASTs at build time so the persisted artifact
* is dialect-AST only.
* step parses these into ASTs at build time, so the persisted envelope carries
* `ast` beside the canonical `source`.
*/
function renderTemplate(strings: TemplateStringsArray, values: readonly unknown[]): string {
if (values.length === 0) return strings[0] ?? '';
Expand Down
Loading