From 62ca2d6c5e408d5f597743ca1eb4c13c6fb9797b Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 01:00:23 +0000 Subject: [PATCH] docs(spec,formula): state the Expression contract in the present tense The M9.1 / M9.2 phase language is dropped from the six citation sites in `packages/spec/src/shared/expression.zod.ts` and from the two in `packages/formula`. `source` is the canonical persisted form; `ast` is an optional opaque structured value with no promise of becoming required; an evaluated slot requires `source`. No accept-set change. `EVALUATED_EXPRESSION_SOURCE_REQUIRED` loses the four words "of phase M9.1" and nothing else, which is exactly the wording the two pending changesets that quote it verbatim already carry. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude --- .../expression-contract-present-tense.md | 46 +++++++++++++++++++ packages/formula/src/cel-engine.ts | 7 +-- packages/formula/src/normalize.ts | 3 +- packages/spec/src/shared/expression.zod.ts | 32 +++++++------ 4 files changed, 69 insertions(+), 19 deletions(-) create mode 100644 .changeset/expression-contract-present-tense.md diff --git a/.changeset/expression-contract-present-tense.md b/.changeset/expression-contract-present-tense.md new file mode 100644 index 00000000000..14b118deaed --- /dev/null +++ b/.changeset/expression-contract-present-tense.md @@ -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. diff --git a/packages/formula/src/cel-engine.ts b/packages/formula/src/cel-engine.ts index 584bcc83e71..3bce1cda815 100644 --- a/packages/formula/src/cel-engine.ts +++ b/packages/formula/src/cel-engine.ts @@ -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`' }, diff --git a/packages/formula/src/normalize.ts b/packages/formula/src/normalize.ts index c69539bf310..b511111c6b9 100644 --- a/packages/formula/src/normalize.ts +++ b/packages/formula/src/normalize.ts @@ -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. diff --git a/packages/spec/src/shared/expression.zod.ts b/packages/spec/src/shared/expression.zod.ts index 16c11ad2e1d..b92012cbb60 100644 --- a/packages/spec/src/shared/expression.zod.ts +++ b/packages/spec/src/shared/expression.zod.ts @@ -77,13 +77,14 @@ export type ExpressionMeta = z.input; /** * 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`. */ @@ -111,7 +112,7 @@ export type Expression = z.input; */ 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: \'…\' }`.'; @@ -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 @@ -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 }), @@ -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] ?? '';