Skip to content

Commit 62ca2d6

Browse files
committed
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 <noreply@anthropic.com>
1 parent 79a046f commit 62ca2d6

4 files changed

Lines changed: 69 additions & 19 deletions

File tree

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
---
2+
"@objectstack/spec": patch
3+
---
4+
5+
fix(spec): the Expression contract is stated in the present tense — the M9.1 / M9.2 phase language is dropped (#17849)
6+
7+
Clause-②: no
8+
9+
No accept-set change. `ExpressionSchema` still accepts `source` OR `ast`, every
10+
evaluated slot still requires a non-blank `source`, and no key is added, renamed
11+
or retired. What moves is the text six citation sites carried.
12+
13+
Those docblocks promised a two-phase roadmap — "Phase 1 (M9.1): `source` is the
14+
canonical persisted form … Phase 2 (M9.2+): `ast` becomes required in build
15+
output" — that no ADR ever chartered, and the refusal sentence an author reads
16+
carried the phase id inside it. #17323 ruled the promise removed: `ast` stays an
17+
accepted optional structured value with no promise of becoming required. The
18+
contract is now written as it actually is:
19+
20+
- `source` is the canonical persisted form — it is what the engine evaluates;
21+
- `ast` is accepted beside it as an optional opaque structured value, and
22+
carries no promise of becoming required;
23+
- a slot whose value the engine RUNS requires `source`, which is what
24+
`EvaluatedExpressionSchema` spells out.
25+
26+
**The one published string that moves** is `EVALUATED_EXPRESSION_SOURCE_REQUIRED`,
27+
the sentence an author reads when an evaluated slot refuses a non-evaluable
28+
envelope. It loses four words and nothing else:
29+
30+
> … the expression engine evaluates `source` (the canonical persisted form of
31+
> phase M9.1) and cannot evaluate `ast` alone …
32+
33+
now reads
34+
35+
> … the expression engine evaluates `source` (the canonical persisted form) and
36+
> cannot evaluate `ast` alone …
37+
38+
Nothing parses that sentence for its content: every consumer imports the
39+
constant by name, and the two pending changesets that quote it verbatim
40+
(`flow-edge-condition-evaluated-slot`,
41+
`blank-node-condition-refused-at-registration`) already carry the new wording,
42+
so the quote stays a quote.
43+
44+
The `packages/formula` half of the same ruling — `cel-engine.ts`'s AST-only arm
45+
and `normalize.ts`'s header — is comment-only and publishes nothing from that
46+
package (`@objectstack/formula` ships `dist` alone), so it is not graded here.

‎packages/formula/src/cel-engine.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1715,9 +1715,10 @@ export const celEngine: DialectEngine = {
17151715
const source = expr.source;
17161716
if (typeof source !== 'string' || source.length === 0) {
17171717
// AST-only inputs: cel-js does not currently expose a public API to
1718-
// re-execute a parsed AST without re-serializing. We persist `source`
1719-
// as the canonical form during M9.1 and revisit AST-only execution in
1720-
// M9.7 when we cut the spec persistence over.
1718+
// re-execute a parsed AST without re-serializing. `source` is the
1719+
// canonical persisted form and is what this engine evaluates, so an
1720+
// envelope carrying only `ast` has no evaluable form here and is
1721+
// reported as such rather than run.
17211722
return {
17221723
ok: false,
17231724
error: { kind: 'parse', message: 'AST-only evaluation not yet supported; persist `source`' },

‎packages/formula/src/normalize.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
*
77
* 1. String shorthand input is replaced by `{ dialect: 'cel', source }`.
88
* 2. The persisted envelope carries an `ast` field produced by the dialect
9-
* engine (M9.2 deliverable). Source is retained for round-trip / debug.
9+
* engine, beside the canonical `source` the engine evaluates. `ast` is an
10+
* optional opaque structured value; it never replaces `source`.
1011
*
1112
* Spec layer cannot do step 2 because it must remain dependency-free; this
1213
* package owns the engine import and therefore the AST step.

‎packages/spec/src/shared/expression.zod.ts‎

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,14 @@ export type ExpressionMeta = z.input<typeof ExpressionMetaSchema>;
7777
/**
7878
* Canonical Expression envelope.
7979
*
80-
* Phase 1 (M9.1): `source` is the canonical persisted form. `ast` is reserved
81-
* and accepted as opaque structured value — `objectstack compile` will fill it
82-
* in M9.2 with the engine's parsed AST so the artifact carries an AST-only
83-
* representation.
80+
* `source` is the canonical persisted form: it is what the expression engine
81+
* evaluates. `ast` is accepted beside it as an optional opaque structured
82+
* value — `objectstack compile` fills it with the engine's parsed AST, and
83+
* each engine validates its own shape — and it carries no promise of becoming
84+
* required.
8485
*
85-
* Phase 2 (M9.2+): `ast` becomes required in build output; `source` is kept
86-
* only for round-trip / debug.
86+
* A slot whose value the engine RUNS requires `source`; the envelope that
87+
* spells that out is {@link EvaluatedExpressionSchema}.
8788
*/
8889
export const ExpressionSchema = z.object({
8990
/** Which engine evaluates `source` / `ast`. */
@@ -111,7 +112,7 @@ export type Expression = z.input<typeof ExpressionSchema>;
111112
*/
112113
export const EVALUATED_EXPRESSION_SOURCE_REQUIRED =
113114
'An expression in an evaluated slot needs a non-blank `source`: the expression engine evaluates `source` '
114-
+ '(the canonical persisted form of phase M9.1) and cannot evaluate `ast` alone, so an envelope carrying only '
115+
+ '(the canonical persisted form) and cannot evaluate `ast` alone, so an envelope carrying only '
115116
+ '`ast`, or a `source` that is blank after trimming, would validate and register and then fault at run time. '
116117
+ 'Write `{ dialect: \'cel\', source: \'…\' }`.';
117118

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

0 commit comments

Comments
 (0)