spec: retire the CEL expression arms of SLI successCriteria and composite trace-sampling condition - #19084
Conversation
…sampling condition
Both slots were z.union([<a structured arm>, EvaluatedExpressionInputSchema]).
The expression arm parsed, normalized a bare string to { dialect: 'cel', source },
registered and was served back, and nothing anywhere evaluated it — ADR-0049
enforce-or-remove. The arms are removed; the structured arms are untouched and
measured on their own card.
The prescription hangs on the surviving schema's own error map (dispatched on
issue.input), because the KEY survives and only one of its two arms went away:
retiredKey() and an ADR-0087 D2 strip both retire a key, neither retires an arm.
The disposition is a D3 semantic entry, observability-cel-predicates-retired, so
neither prescription carries an `os migrate meta` sentence.
Mechanical consequences, all declared: the retired arm held the last transform in
the system/MetricsConfig and system/TracingConfig subtrees, so both defs project
in output mode and publish the defaults the parser always applied
(DEFAULT_CHANGES_BY_MAJOR); dropped-refinements sites move off the union option
path; the ADR-0058 D7 ledger row cel-declared-unwired-observability closes with
the removal and the inline scan floor drops 3 to 1, naming both positions.
Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2
Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check8 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 136 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0326b87160643cfca37924365c815403bc48b09a && git checkout 0326b87160643cfca37924365c815403bc48b09a
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b7eaf6a617b7353825935631f73b5bdcf7b78f90 425912a3bb33e1ba5de502e4c0dc7b5ffcdf09b3 && git checkout -B drift-repro b7eaf6a617b7353825935631f73b5bdcf7b78f90 && git merge --no-ff 425912a3bb33e1ba5de502e4c0dc7b5ffcdf09b3
node scripts/docs-audit/affected-docs.mjs --json b7eaf6a617b7353825935631f73b5bdcf7b78f90 |
Contract reviewServed-tier: Two worktrees: base (1) Derived judgmentsAccept set, measured on the BUILT head package. Reverse verification, three legs against the REBUILT The card's zero — re-derived with the reviewer's own control, and it HOLDS. Identity scan plus a second pass by property access; same-instrument controls lit ( ⛔ But the dev's "separate positive fact" does NOT close the radius and must not be re-used. The published JSON Schema — the "second axis", measured. Base→head: exactly four defs differ and all four lose The four widened consumers — each genuinely FORCED, verified one by one: The tool choice — RIGHT, by declaration site. A surviving KEY losing one ARM is the class the playbook says none of the three routes fits; the prescription hangs on the schema's own F1 — BLOCKING. The same unreleased step carries a contradicting D3 entry the playbook requires absorbing. F2 — non-blocking. Changeset, PR body and both F3 — non-blocking. The new entry's acceptance proof says authoring a retired spelling "is a (2) Semver level
(3) Boundary flags
Implemented-by: VERDICT: FAIL — one BLOCKING (F1: absorb the same-step D3 entry, one file + registry regen), two non-blocking wording findings to fold into the same patch round. Everything else measured here holds at this head. After the patch the head moves, so the record is re-issued for the new head. Generated by Claude Code Generated by Claude Code |
… 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>
Contract review — re-review roundServed-tier: Re-review after the patch to the FAIL record on head What moved ( ⭐ Carry-forward basis, verified by BLOB SHA — not by diff silence. CI at this head: 46 runs = 39 success, 7 skipped, 0 non-green, all completed at read time. (1) Derived judgmentsF1 (was BLOCKING) — RESOLVED, and the absorption is COMPLETE. In the generated output: F2 — RESOLVED; accounting right, one phrase loose. The FOUR table matches the measured projection exactly (+8 / +4 / +1 / +1 F3 — RESOLVED, wording verified verbatim including «⛔ Do not read a clean The D2-precedent nuance is present in the entry's
Gates re-run at this head: ⭐ Correction to my own The card's zero — the dev's replacement readings, reproduced. objectui zeros identical; its control counts 340/652 are the with-CHANGELOG figures (318/633 without) — the zero is the same either way. hotcrm controls match exactly; all four ⭐ The audibility argument, judged: it is a BOUND on the cost of a wrong zero, not a closure of it — the PR body's «what covers them» overstates by one word. What it guarantees for any consumer reaching these slots through the spec's parse: an author of either retired spelling is refused with the prescription, and a stored row carrying one fails at the load seam naming the slot. So a wrong zero for (2) Semver levelUnchanged and re-verified: (3) Boundary flags
Implemented-by: VERDICT: PASS — F1 resolved and verified in the generated output and across all of step 18; F2 and F3 resolved; the carry-forward accepted on blob-sha byte-identity; every gate at this head exits 0; CI 39/7/0. One new non-blocking finding (F4) recorded for the seat's disposition. Generated by Claude Code Generated by Claude Code |
…d tighten one phrase Contract review F4: the FOUR paragraph was inserted BEFORE the last clause of each reason string rather than after it, so both reasons rendered with two sentences cut in half — "…what he now reads is what the⚠️ FOUR published JSON Schemas change projection direction…" and "…not a new one. parser has always applied." The gate's own text says the reason is printed by every build that accepts the change and must be written for the consumer who is about to be surprised; that consumer was being handed broken sentences. The paragraph now sits at the end of each string, and both reasons were read back as rendered from the module and from the accepting build's own output. Also tightens the phrase the review found loose. It said the two nested defs gain a required member "with no default to declare". They do carry defaults — enabled is true, rules is [] — and both were already published at the base: measured, not inherited, at authorable-defaults/system.json lines 206 and 248 of the base blob, whose base..head diff is exactly +slis and +sampling, two insertions and no deletions. The wording is now the changeset's own: only the first two carry a default MOVE, so only those two are declarable here, because this ratchet records default VALUES per key and is blind to required growth by construction. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Contract review — re-issue roundServed-tier: Re-issue after the F4 fix to the PASS record on head What moved ( CI at this head: 35 runs = 33 success, 2 skipped, 0 non-green, all completed. (1) Derived judgmentsF4 — RESOLVED, verified in THREE channels. (a) Source: (b) The module, rendered with tsx and split into sentences: (c) ⭐ The channel the finding was actually about — the accepting gate's own printed output. F2 precision — now a READING, confirmed off the base blob. The other consumers that one file could disturb, re-run at this head under the lock: ⭐ The dev's gate reconciliation (108 run / 1 NOT MEASURED, up from 107/2) — judged: it changes nothing I concluded. The two newly measured gates and the one still unmeasured all live in CI jobs that were green at every head of this card: (2) Semver levelUnchanged and carried: (3) Boundary flags
Implemented-by: VERDICT: PASS — F4 resolved and verified in source, in the rendered module and in the accepting gate's own printed output; the one moved file's every consumer re-run at this head exits 0; all 17 other files carry identical blobs to the Generated by Claude Code Generated by Claude Code |
`#19084` (`ee5812a5e3`) retired the CEL expression arm of `TraceSamplingConfigSchema.composite[].condition` at the very slot this branch projects. Both intents stack: main's side of the slot is taken whole — the record-only `condition` and its retirement prescription — and its `!('dialect' in value)` predicate is declared through this branch's `bannedKeys(['dialect'])` arm. The two renamed ledger rows go, because the arm projects the site they name. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
`#19084` collapsed `TraceSamplingConfig.composite[].condition` to a record, so the ban lands on `condition` itself rather than on a union arm, the ledger rows are spelled `composite.element.condition`, and a CEL envelope is now refused by the runtime too. The live-seam pins and the changeset's accept-set sentence are re-derived on that tree. Also: the changeset declares `Clause-②: yes`, matching the corrected claim and the ruling; and the empty-key-list branch records the real reason it drops — `enum: []` is an invalid schema, not a vacuous rule. Claude-Session: https://claude.ai/code/session_01AmH9bKvGoLjiY86Q4Z3og2 Co-authored-by: Claude <noreply@anthropic.com>
Fixes #18118
Clause-②: yes — retiring a published authorable surface. Carrier: the changeset
.changeset/18118-retire-observability-cel-arms.md, which declares the same line and theADR-0087 disposition. ⛔ The
needs:contract-reviewlabel is the seat's act; this PR neitherhangs nor clears it.
Ruling: batch #160 item 3, letter A, maintainer 「同意」 2026-09-18T11:59Z — retire the two CEL
expression arms under ADR-0049 enforce-or-remove, by the
spec-property-retirementplaybook.What was removed
Two union arms, not two keys:
ServiceLevelIndicatorSchema.successCriteriaz.union([{ threshold, operator, percentile? }, EvaluatedExpressionInputSchema])TraceSamplingConfigSchema.composite[].conditionz.union([ StructuredFilterRecord, EvaluatedExpressionInputSchema ])EvaluatedExpressionInputSchemaitself is untouched —packages/spec/src/shared/expression.zod.tsis not in this diff at all (it is held by PR #18985, which is not addressed here). What left is two
references to it.
The card's zero, re-derived — and the instrument's radius
Re-derived on this branch's base
176b03582e600ee5628d21bff9422073c5a5530c, not inherited.git grep -nover the whole tracked tree forsuccessCriteria,ServiceLevelIndicatorandTraceSamplingConfig. Every hit outsidepackages/spec/srcis a generated artefact(
api-surface*,authorable-surface*,authorable-defaults,declaration-map,export-origins,json-schema.manifest,dropped-refinements.baseline.json), a reference page,a changelog or changeset, or the shipped skill's prose row. Inside
packages/spec/srcthe readersare: the two schemas' own unit tests,
shared/evaluated-slot-population.test.ts(a census), themigration registry's prose, and one docblock in
shared/evaluated-slot-union.ts. Outside the specpackage the only reader is
packages/qa/dogfood/test/expression-conformance.ledger.ts— aclassification ledger, not an evaluator. No service, plugin, runtime or CLI path reads either key.
Reachable radius of the instrument: tracked files in THIS checkout at THIS commit. It does not
reach untracked or ignored build output, another repository, or a published npm tarball.
One known target outside it: the sibling repository
objectstack-ai/objectui, which is on thisbox but is a different git repository, so no
git grephere can see it. It is named rather thanwaved at, because the
template-title-formatrow of the same ledger records exactly this limit fora different key: an interpolation site that lives there and cannot be measured from here. ⛔ The argument that used to stand here was REJECTED by at-tier contract review and is withdrawn.
It claimed the radius was closed by a positive fact — that nothing in a sibling could evaluate these
slots without importing the symbols naming them, whose consumers
export-origins/and theConsole Pin Gateenumerate. That is wrong on three counts the review measured:export-origins/records by its own description which SOURCE DECLARATION each exported name resolves to — origins,
NOT consumers; the
Console Pin Gateis path-filtered and was SKIPPED on this very PR; and anevaluator need not import either symbol, since a REST-served metrics config can be read by key.
What closes the radius instead is direct measurement outside it, with a lit control in each repo.
objectui @
3e4f6324f7:successCriteria0 files,ServiceLevelIndicator0,TraceSamplingConfig0;controls in the same run —
visibleWhen340 files,ObjectSchema652. hotcrm @087b7c5dc4(887 tracked files; public, served by this session's git proxy — an earlier dispatch's 「unreachable」
was the seat's error): the same three at 0 plus
slis0; controls —visibleWhen15,defineStack47,@objectstack/spec269.samplingshows 4 files there and every one was read (an MCP capabilitytable row, two prose sentences, a CHANGELOG line — no trace-sampling config), so that zero stands on
inspection and not on the count.
Known targets still OUTSIDE the radius, named rather than waved at:
objectstack-ai/cloud(access denied to this session) and any third-party npm consumer of
@objectstack/spec. Neither wasmeasured, by anyone. What BOUNDS the cost of a wrong zero there is the retirement's audibility (a bound, ⛔ not a closure — at-tier review's wording): a surviving predicate is a
tscerror or a parse refusal carrying the prescription, never a silent change.Every symbol was located by its declaration site, and a literal inside a
//or/** */commentwas counted as prose, not as a reader — that is why
shared/evaluated-slot-union.tsis listed as adocblock and
migrations/registry.tsas prose. Exit codes were captured before any pipe.The retirement kit
errormap, dispatched onissue.input(the
HookBodyCapability/object.managedBy: 'system'pattern).retiredKey()and an ADR-0087D2 strip both retire a KEY; neither retires an ARM, and the keys survive here.
errormap is consulted for the top-level
invalid_typea NON-OBJECT raises and not for the child issuesa wrong-shaped OBJECT raises. So on
successCriteriathe bare-string spelling carries theprescription and the
{ dialect, source }envelope is refused by the structured arm's ownmissing-key issues; on
conditionboth spellings carry it, because the record arm's abortingdialectrefine sees the object itself. The negative is pinned too: a value refused for a reasonthat is NOT the retirement must not borrow its sentence.
observability-cel-predicates-retired(
packages/spec/src/migrations/entries/semantic/18.observability-cel-predicates-retired.ts, withregistry.tsregenerated, never hand-edited between the markers). A predicate is an intent nothreshold/operator pair or attribute filter records. Both prescriptions therefore carry no
os migrate metasentence — owed only where a conversion covers the surface.@objectstack/specminor with theBREAKING banner, per the ruling's Execution section.
api-surface-declarations/,authorable-defaults/, the twocontent/docs/references/system/*.mdxpages.Acceptance notes
last
.transform()in thesystem/MetricsConfigandsystem/TracingConfigsubtrees, so both defsnow project in output mode instead of falling back to the input shape. Consequences, all declared
in-diff:
system/MetricsConfig:slisandsystem/TracingConfig:samplingpublish thedefaulttheparser has always applied (declared in
DEFAULT_CHANGES_BY_MAJORwith theai/KnowledgeSource:refreshrow as the precedent, that mechanism run backwards), and the nested type cells of both reference
pages lose the
?from their default-bearing keys — the output-mode signature, and the sameconvention every transform-free def in the repo already publishes under. No runtime default
moves: measured by byte-identity of the untouched
.default(…)and by parsing a minimal configon the built package.
evaluated-slot-population.test.tscensus drops 36 positions over 34 declaring lines to 34 over 32,naming both departures rather than subtracting them; the
evaluated-slot-union.tsdocblock dropsfive of 36 to three of 34; the ADR-0058 D7 ledger row
cel-declared-unwired-observabilitycloseswith the removal, and its companion test's
inlinescan floor drops 3 to 1 with both positionsnamed, exactly as that floor's own instruction requires.
Clause-②: no; the dispatch claim comment saysClause-②: yes. This PR carries the claim's line, because the claim comment is the carrier theclause-② check reads and the two must agree. Flagged rather than silently chosen. The
yesreadingalso has independent support in this diff: two published JSON Schemas change projection direction.
(a defaulted key reads as required) while the expanded
Nested Shape:sections below them read thezod node and say
optional (default: …). The two disagree for every output-mode def in the repo,not only these; it predates this card and this diff does not widen it. Carrier for anyone who picks
it up:
packages/spec/scripts/build-docs.ts.skills/objectstack-formula/SKILL.md, whosestructured | celrow formetrics/tracingis theskills lane's at tier; and
packages/spec/src/shared/expression.zod.ts.Verification
Run under the shared verify lock; the judged line of each is quoted in the report on the card.
Generated by Claude Code
Same-major absorption (added after at-tier contract review found it missing — the round's one BLOCKING finding).
The same unpublished step 18 carried
entries/semantic/18.evaluated-expression-slots-source-required.ts,which still enumerated these two slots among 「the 36 declaring positions」 and still told an upgrader to
give a sampling
conditiona dialect and a non-blanksource— the exact envelope this head refuses.The playbook's same-major 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, drops the two slots and the
condition-specificsweep clause, and routes a hit at either slot to
observability-cel-predicates-retired. Verified in theGENERATED output, not only the input:
registry.tscarries34 declaring positionsonce and36 declaring positionszero times.Why D3 is right rather than merely available. Both error-map precedents this retirement copies its
MECHANISM from also registered a D2 conversion, because for them a mechanical rewrite existed. Here none
does: a strip leaves a REQUIRED
successCriteriamissing (the SLI stops parsing) and a composite branchwith no condition at all.
The four defs, named (the two nested ones were disclosed nowhere before):
system/MetricsConfig(
defaultonslis, plus 8requiredmembers) ·system/TracingConfig(defaultonsampling, plus 4)·
system/ServiceLevelIndicator(onerequiredmember,enabled) ·system/TraceSamplingConfig(one
requiredmember,rules). ⭐ The last two are invisible to thedefault-changes.tstable by the ratchet's construction, not for lack of adefault: that table records default VALUES per key, and
enabled/rulesalready carried theirs (true,[])published at the base and unmoved here, so no row of it can express a
requiredgrowth. ⛔ Corrected from anearlier wording of mine that said they had 「no default to declare」 — at-tier contract review measured that as
loose; the exact form is the changeset's own: only the first two carry a
defaultMOVE.Body edits above made by the
domain:spec#4seat after at-tier contract review; the dev writes the body once, at creation.Generated by Claude Code