Skip to content

Commit dc9daca

Browse files
committed
spec: absorb the same-step evaluated-slot D3 entry, and name all four defs that change projection direction
Contract review F1 (blocking): the evaluated-expression-slots-source-required semantic entry sits in the same unpublished step 18 as this retirement, and still enumerated the two retired slots among "the 36 declaring positions" while telling the upgrader to give a sampling condition a dialect and a non-blank source — the exact envelope this head now refuses. The playbook's same-major absorption rule applies to the published D3 record exactly as it applied to the census test and the helper docblock: that entry now reads 34 positions, names the two absentees and the retirement that took them, and routes a hit at either slot to observability-cel-predicates-retired instead of to its own repair. F2: four published JSON Schemas change projection direction, not two. system/MetricsConfig and system/TracingConfig lose x-io input and gain a default; the nested system/ServiceLevelIndicator and system/TraceSamplingConfig lose x-io input and gain a required member (enabled, rules) with no default to declare, so no ratchet row can hold them — stated in the changeset and in both default-change reasons instead. F3: the new entry's acceptance proof claimed tsc refuses a string or an envelope at both slots. Measured: tsc catches the string at both, and the envelope only at successCriteria; the condition envelope is structurally admitted by the record arm and is refused at parse. The proof now separates the two channels and says which spelling each one catches. Also states the nuance the review asked for: both error-map precedents this retirement copies its mechanism from also registered a D2 conversion because a mechanical rewrite existed, and here none does. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8968c29 commit dc9daca

5 files changed

Lines changed: 141 additions & 81 deletions

File tree

.changeset/18118-retire-observability-cel-arms.md

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,45 @@ a wrong number is worse than an absent one.
6161
- **ADR-0087 disposition: a D3 SEMANTIC entry**, `observability-cel-predicates-retired`,
6262
not a D2 conversion. A predicate is an intent that no threshold/operator pair or
6363
attribute filter records; a mechanical strip would delete what the author meant and leave
64-
no trace of which SLI or which sampling branch lost it. The prescriptions therefore carry
65-
**no** `os migrate meta` sentence — that sentence is owed only where a conversion covers
66-
the surface.
64+
no trace of which SLI or which sampling branch lost it — and it would not even be lossless
65+
in the weak sense, because `successCriteria` is REQUIRED (a strip leaves an SLI that no
66+
longer parses) and a composite branch stripped of its `condition` declares no condition at
67+
all. That is the one place this retirement parts company with the two precedents it copies
68+
its MECHANISM from: `crypto.hash` on `HookBodyCapability` and `managedBy: 'system'` both
69+
ALSO registered a D2 conversion, because for each of them a mechanical rewrite existed.
70+
Here none does, which is what makes D3 the right disposition rather than merely an
71+
available one. The prescriptions therefore carry **no** `os migrate meta` sentence — that
72+
sentence is owed only where a conversion covers the surface.
73+
- **The same-major D3 record is absorbed, per the playbook's 「同 major 记账」.** The
74+
`evaluated-expression-slots-source-required` entry landed into this same unpublished step,
75+
and it enumerated these two slots among its 36 declaring positions while instructing the
76+
upgrader to give a sampling `condition` a dialect and a non-blank `source` — the exact
77+
envelope this head now refuses. Both entries first ship together, so the composite of the
78+
two changes is the retirement alone: that entry now reads 34 positions, names the two
79+
absentees and why, and routes them to this retirement instead of to its own repair.
6780
- **The surviving accept sets are pinned beside the refusals.** `successCriteria` still
6881
takes `{ threshold, operator, percentile? }`; a composite `condition` still takes any
6982
filter object carrying no `dialect` key — `{ source: 'x' }` included, because `source`
7083
alone is an ordinary filter key and the retirement narrowed the `dialect` door only.
71-
- **Two published JSON Schemas change projection direction**, and it is mechanical rather
72-
than chosen: the retired arm held the last `.transform()` in the `system/MetricsConfig`
73-
and `system/TracingConfig` subtrees, so both defs now project in output mode instead of
74-
falling back to the input shape. `system/MetricsConfig:slis` and
75-
`system/TracingConfig:sampling` therefore publish the `default` the parser has always
76-
applied, declared in `DEFAULT_CHANGES_BY_MAJOR`; and the nested type cells of both
77-
reference pages lose the `?` from their default-bearing keys. **No runtime default moves**
78-
— measured twice, by byte-identity of the untouched `.default(…)` and by parsing a
84+
- **FOUR published JSON Schemas change projection direction**, and it is mechanical rather
85+
than chosen: the retired arm held the last `.transform()` in each of these subtrees, so
86+
each def now projects in output mode instead of falling back to the input shape. All four
87+
lose `x-io: input`, and what each gains differs:
88+
89+
| published schema | gains |
90+
| --- | --- |
91+
| `system/MetricsConfig` | `default: []` on `slis`, plus 8 `required` members |
92+
| `system/TracingConfig` | `default: {"type":"always_on","rules":[]}` on `sampling`, plus 4 `required` members |
93+
| `system/ServiceLevelIndicator` | one `required` member, `enabled` |
94+
| `system/TraceSamplingConfig` | one `required` member, `rules` |
95+
96+
Only the first two carry a `default` move, so only those two are declarable in
97+
`DEFAULT_CHANGES_BY_MAJOR` — the nested pair's `required` growth has no ratchet row to
98+
live in and is stated here instead. A `required` that lists defaulted keys is this repo's
99+
existing output-mode convention, not a new one, and the same-category control
100+
`system/CacheConfig` is untouched. The reference pages show the same signature: the nested
101+
type cells of both pages lose the `?` from their default-bearing keys. **No runtime default
102+
moves** — measured twice, by byte-identity of the untouched `.default(…)` and by parsing a
79103
minimal config on the built package.
80104

81105
## What is deliberately NOT in this change

packages/spec/scripts/lib/default-changes.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,15 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly<Record<number, readonly Declared
381381
+ 'present — and those cells carried `?` at the base. Nothing '
382382
+ 'deployed changes behaviour; the consumer affected is one outside this repo who reads '
383383
+ 'the published JSON Schema\'s `default` himself, and what he now reads is what the '
384+
+ '⚠️ FOUR published JSON Schemas change projection direction in this diff, not the two this '
385+
+ 'table has rows for, and the other two are disclosed nowhere else: `system/MetricsConfig` and '
386+
+ '`system/TracingConfig` lose `x-io: input` AND gain a `default` (these two rows), while the '
387+
+ 'nested `system/ServiceLevelIndicator` and `system/TraceSamplingConfig` lose `x-io: input` '
388+
+ 'and gain a `required` MEMBER with no default to declare — `enabled` and `rules` respectively — '
389+
+ 'so this ratchet never sees them. Measured at head: all four carry no `x-io` and `required` '
390+
+ 'lists the defaulted keys; ten other `system` defs are still input-mode, and the same-category '
391+
+ 'control `system/CacheConfig` is untouched. A `required` that lists defaulted keys is this '
392+
+ 'repo\'s existing output-mode convention, not a new one. '
384393
+ 'parser has always applied. To keep the old value there is nothing to write — the key, '
385394
+ 'its type and its default are unchanged.',
386395
},
@@ -403,6 +412,15 @@ export const DEFAULT_CHANGES_BY_MAJOR: Readonly<Record<number, readonly Declared
403412
+ 'key of every `system/TracingConfig` nested type cell (`parentBased`, `propagation`, '
404413
+ '`spanLimits`, `performance`, `instrumentation`), the output-mode signature. '
405414
+ 'Nothing deployed changes behaviour; the consumer affected is one outside this '
415+
+ '⚠️ FOUR published JSON Schemas change projection direction in this diff, not the two this '
416+
+ 'table has rows for, and the other two are disclosed nowhere else: `system/MetricsConfig` and '
417+
+ '`system/TracingConfig` lose `x-io: input` AND gain a `default` (these two rows), while the '
418+
+ 'nested `system/ServiceLevelIndicator` and `system/TraceSamplingConfig` lose `x-io: input` '
419+
+ 'and gain a `required` MEMBER with no default to declare — `enabled` and `rules` respectively — '
420+
+ 'so this ratchet never sees them. Measured at head: all four carry no `x-io` and `required` '
421+
+ 'lists the defaulted keys; ten other `system` defs are still input-mode, and the same-category '
422+
+ 'control `system/CacheConfig` is untouched. A `required` that lists defaulted keys is this '
423+
+ 'repo\'s existing output-mode convention, not a new one. '
406424
+ 'repo who reads the published JSON Schema\'s `default` himself. To keep the old value '
407425
+ 'there is nothing to write — the key, its type and its default are unchanged.',
408426
},

packages/spec/src/migrations/entries/semantic/18.evaluated-expression-slots-source-required.ts

Lines changed: 24 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,28 @@ import type { SemanticMigration } from '../../types.js';
77
export const entry: SemanticMigration = {
88
id: 'evaluated-expression-slots-source-required',
99
surface:
10-
'every EVALUATED expression slot in the spec — the 36 declaring positions the #15811 census '
11-
+ 'enumerated by identity, not by a name scan: the formula Field.expression; the predicate '
10+
'every EVALUATED expression slot in the spec — the 34 declaring positions of the #15811 census '
11+
+ 'that survive into this major, enumerated by identity and not by a name scan: the formula '
12+
+ 'Field.expression; the predicate '
1213
+ 'keys visibleWhen / visibleOn / readonlyWhen / requiredWhen / visibility / disabledWhen / '
1314
+ 'visible / disabled / condition / when, on Field, SelectOption, InlineGridColumn, '
1415
+ 'ScriptValidation, CrossFieldValidation, ConditionalValidation, Hook, ObjectFieldGroup, '
1516
+ 'RowCrudActionOverride, CriteriaSharingRule, PluginPermission.filter, MultiVersionSupport '
1617
+ 'routing, Action and ActionParam (including each param option), BaseNavItem, BulkActionDef, '
17-
+ 'PageComponent, PageTabs items, RecordAlert, ListViewShape, FormFieldBase, FormSection, the '
18-
+ 'settings-manifest Specifier and manifest visible, the ServiceLevelIndicator successCriteria '
19-
+ 'union member and the TraceSamplingConfig composite condition union member — authored either '
18+
+ 'PageComponent, PageTabs items, RecordAlert, ListViewShape, FormFieldBase, FormSection and the '
19+
+ 'settings-manifest Specifier and manifest visible — authored either '
2020
+ 'as an expression envelope carrying only ast ({ dialect: \'cel\', ast: … } with no source), '
2121
+ 'or with a source that is blank after trimming, through the envelope key '
22-
+ '({ dialect: \'cel\', source: \' \' }) or the bare-string shorthand for it. ⚠️ At the '
23-
+ 'TraceSamplingConfig composite condition slot the reachable set is WIDER than those two '
24-
+ 'spellings, because the structured-filter arm beside the expression arm narrowed with it: a '
25-
+ 'bare record accepted { dialect: \'cel\', ast } as an ordinary filter, which made the rule '
26-
+ 'inert there, so that arm now declines ANY object carrying a dialect key. Six shapes the '
27-
+ 'base accepted through that arm alone are refused at that slot: { dialect: \'cel\' }, '
28-
+ '{ dialect: \'js\', source: \'x\' }, { dialect: \'nope\', source: \'x\' }, '
29-
+ '{ dialect: \'cel\', source: 5 }, '
30-
+ '{ dialect: \'cel\', source: \'x\', meta: { rationale: 5 } } and '
31-
+ '{ dialect: \'zzz\', foo: 1 }. A structured filter carrying no dialect key is accepted '
32-
+ 'exactly as before. The published '
22+
+ '({ dialect: \'cel\', source: \' \' }) or the bare-string shorthand for it. ⚠️ The census '
23+
+ 'this entry was written against counted 36, and the two that are deliberately absent here are '
24+
+ 'the ServiceLevelIndicator successCriteria and TraceSamplingConfig composite condition '
25+
+ 'expression arms. They are not lost: they were RETIRED OUTRIGHT in this same unpublished '
26+
+ 'major by the observability-cel-predicates-retired entry of this step, under ADR-0049 '
27+
+ 'enforce-or-remove, because nothing evaluated either. Both entries first ship together, so an '
28+
+ 'upgrader never meets those two slots under THIS rule — the composite of the two changes is '
29+
+ 'the retirement alone, and stating the narrowing for a slot that no longer accepts an '
30+
+ 'expression at all would send the upgrader to author one. That absorption is the only reason '
31+
+ 'the count here is not the census figure the #15811 card records. The published '
3332
+ 'TypeScript interface RowCrudPredicates narrows with the two slots it mirrors. Reachable '
3433
+ 'wherever metadata is authored or stored: defineStack sources, an exported stack passed to '
3534
+ 'objectstack validate, a POST body on any of these metadata types, and a row already sitting '
@@ -94,19 +93,15 @@ export const entry: SemanticMigration = {
9493
+ 'the slots named in `surface`: an expression envelope with no `source` key, and a `source` '
9594
+ '(or bare-string shorthand) that is empty after trimming. ⚠️ Sweep by SLOT, not by key name '
9695
+ '— `visible` is on this list for actions, action params, nav items, bulk actions, record '
97-
+ 'alerts and settings manifests, and is NOT an expression slot elsewhere; and three of the '
98-
+ 'positions are union members (`RecordAlertProps.visible`, '
99-
+ '`ServiceLevelIndicator.successCriteria`, `TraceSamplingConfig.composite[].condition`). On '
100-
+ 'TWO of those three the sibling arm is untouched: `RecordAlertProps.visible` still takes a '
101-
+ 'boolean literal, and `ServiceLevelIndicator.successCriteria` still takes its structured '
102-
+ '`{ threshold, operator, percentile? }` object — including one that carries a `dialect` '
103-
+ 'key. ⚠️ On the third, `TraceSamplingConfig.composite[].condition`, the sibling arm '
104-
+ 'narrows WITH the expression arm and that is deliberate: a bare record accepted '
105-
+ '`{ dialect: \'cel\', ast }` as an ordinary filter, so the rule was inert there. The sweep '
106-
+ 'at that ONE slot is therefore wider — flag every `condition` object carrying a `dialect` '
107-
+ 'key, not only the two spellings above; if it is a structured filter, drop the `dialect` '
108-
+ 'key, and if it is an expression it needs a dialect this platform evaluates AND a non-blank '
109-
+ '`source`. For each hit: if it carries an `ast`, run '
96+
+ 'alerts and settings manifests, and is NOT an expression slot elsewhere; and ONE of the '
97+
+ 'positions is a union member, `RecordAlertProps.visible`, whose sibling arm is untouched: it '
98+
+ 'still takes a boolean literal, so a boolean there is not a hit. ⚠️ The two OTHER union '
99+
+ 'members the #15811 census listed — `ServiceLevelIndicator.successCriteria` and '
100+
+ '`TraceSamplingConfig.composite[].condition` — are deliberately NOT on this sweep, because '
101+
+ 'their expression arms were retired outright in this same major (see `surface`). Sweep those '
102+
+ 'two under `observability-cel-predicates-retired` instead, whose instruction is the opposite '
103+
+ 'of this one: there, an expression is not repaired, it is replaced by the structured shape or '
104+
+ 'moved out of application metadata. For each hit: if it carries an `ast`, run '
110105
+ '`printCelAst(ast)`; a string result IS the migration and needs no judgment beyond reading '
111106
+ 'it back. A `null` result, or a blank `source`, is the hand-migration case — decide per the '
112107
+ '`replacement` note whether the slot was meant to carry a predicate (author the `source`) or '

packages/spec/src/migrations/entries/semantic/18.observability-cel-predicates-retired.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,14 @@ export const entry: SemanticMigration = {
4545
+ 'predicate is an intent no threshold/operator pair or attribute filter records, so '
4646
+ 'stripping the key would delete what the author meant and leave no trace of which SLI or '
4747
+ 'which sampling branch lost it — exactly the judgment a semantic TODO exists to hand back. '
48+
+ '⚠️ And a strip here is not merely lossy, it is INVALID: `successCriteria` is a REQUIRED '
49+
+ 'key, so removing it leaves an SLI that no longer parses, and a composite sampling branch '
50+
+ 'that loses its `condition` declares no condition at all — inert today, and the moment a '
51+
+ 'sampler is wired it reads as UNCONDITIONAL. That is the difference from the '
52+
+ 'two error-map precedents this retirement copies its MECHANISM from — `crypto.hash` on '
53+
+ 'HookBodyCapability and `managedBy: \'system\'` — both of which also registered a D2 '
54+
+ 'conversion, because for each of them a mechanical rewrite existed. Here none does, which is '
55+
+ 'what makes D3 the right disposition rather than merely an available one. '
4856
+ '⚠️ The structured arm of each union is NOT decided here: it is equally unread today, and '
4957
+ 'it is measured on its own card. ADR-0087, ADR-0058 D7, ADR-0049.',
5058
acceptanceCriteria:
@@ -56,12 +64,18 @@ export const entry: SemanticMigration = {
5664
+ 'predicate into a threshold by guessing the number — nothing was evaluating it, so there is '
5765
+ 'no behaviour to preserve and a wrong number is worse than an absent one. Two proofs. '
5866
+ '(1) `objectstack validate` is clean on a stack authored in config files: a surviving '
59-
+ 'predicate is refused at the slot with the retirement prescription, and authoring one is a '
60-
+ '`tsc` error at the call site because the slot\'s input type no longer admits a string or '
61-
+ 'an envelope. ⚠️ The prescription reaches the author for the STRING spelling; the envelope '
62-
+ 'spelling on `successCriteria` is refused by the structured arm\'s own missing-key issues '
63-
+ '(`threshold`, `operator`) rather than by the prescription — measured, and pinned both ways '
64-
+ 'in the schema\'s unit tests. (2) For stored rows, load the tenant and confirm every '
67+
+ 'predicate is refused at the slot with the retirement prescription. ⚠️ TWO CHANNELS, and '
68+
+ 'they do not cover the same set — measured, not assumed. `tsc` catches the BARE-STRING '
69+
+ 'spelling at both slots, and the `{ dialect, source }` envelope at `successCriteria` only '
70+
+ '(the structured arm is a closed object literal, so the envelope is an excess-property '
71+
+ 'error). It does NOT catch the envelope at `condition`: the surviving arm there is a record '
72+
+ 'of string to unknown, which admits `{ dialect, source }` structurally, so that one spelling '
73+
+ 'compiles and is refused at PARSE by the arm\'s `dialect` rule. ⛔ Do not read a clean '
74+
+ '`tsc` as a clean sweep of `condition`. The PRESCRIPTION divides differently again: it '
75+
+ 'reaches the author for every refused spelling at `condition`, and for the string spelling '
76+
+ 'only at `successCriteria`, where the envelope is refused by the structured arm\'s own '
77+
+ 'missing-key issues (`threshold`, `operator`). All three legs are pinned in the schemas\' '
78+
+ 'unit tests. (2) For stored rows, load the tenant and confirm every '
6579
+ 'metrics and tracing config still rehydrates: a row carrying a predicate at either slot now '
6680
+ 'fails its parse at the load seam and is reported there, naming the slot. A row whose '
6781
+ '`successCriteria` is a structured rule and whose sampling `condition` objects carry no '

0 commit comments

Comments
 (0)