Skip to content

Commit 2095e4e

Browse files
committed
docs(spec): correct the "silent strip" cost statement at all four sites, and re-count clause ② as seven
A CONTRACT_REVIEW_TIER review measured this branch's head and found the cost statement the maintainer signs off on by merging to be factually wrong. Four sites said the strip is silent. It is not: `lintUnknownAuthoringKeys` (#3786) walks every stack collection whose entry schema is strip-mode, and `connectors` is one, so for `connectors[].syncConfig.schedule` — the one deleted position a stack manifest reaches — `os validate` and `os build` NAME the dropped key and `os validate --strict` EXITS 1 on it. Measured at head on an otherwise-clean stack: without the key 0 warnings / exit 0, with it 1 warning / exit 1. The truth is more favourable to authors than what the maintainer was told, but it is not the sentence they were asked to confirm. Corrected at all four sites: the changeset, `migrations/registry.ts`'s step-18 rationale, two comments in `cron-typed-positions-retirement.test.ts`, and the PR body's 维护者速读. The accurate statement is per channel: the PARSE strips silently, `os validate` / `os build` name the key, `--strict` refuses, `os migrate meta` still lists nothing. Same round, two more corrections and no behaviour change: * Clause ② is SEVEN widenings, not two, and THREE of them are required-key removals (`ScheduleState.cronExpression`, `ScheduledExport.schedule.cronExpression`, `ScheduleExportRequest.schedule.cronExpression`). Measured on both legs: the five schema files restored to the merge-base on disk, one probe run, restored to HEAD and re-run, with a byte-identity check after the restore. 14 cases, 14 refusals on the base leg, 14 accept-and-strip on the head leg. * The changeset now names the departure of the required `cronExpression: string` member from `ScheduleExportInput` — a published TS interface on `IExportService`. "No def leaves the public surface" is true and does not cover a member. Sweep: zero consumers outside `packages/spec`. ⛔ No behaviour changed: the seven deletions, the merge and the ADR-0087 disposition are untouched, and the 直接删 ruling is not re-litigated. Claude-Session: https://claude.ai/code/session_01MkQhmuuJAVDjmeWNixwDDH Co-authored-by: Claude <noreply@anthropic.com>
1 parent 1504dd1 commit 2095e4e

3 files changed

Lines changed: 87 additions & 18 deletions

File tree

.changeset/cron-typed-positions-retired.md

Lines changed: 43 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,30 @@ ledger row `cron-declared-unwired` had every one of them `unevaluated`.
2525
| backup / DR testing | `BackupConfig`, `DisasterRecoveryPlan.testing` (`system/disaster-recovery.zod.ts`) | `schedule` (both) | no |
2626

2727
**What an upgrading author actually observes.** None of the five schemas is `.strict()`, so
28-
a bare deletion means Zod DROPS the key: an existing document still parses, still loads, and
29-
the value is discarded without a word. Nothing refuses it, so there is nothing for
28+
a bare deletion means Zod DROPS the key at the PARSE: an existing document still parses and
29+
still loads, and the value is discarded there without a word. There is nothing for
3030
`objectstack migrate meta` to list and nothing for the ADR-0087 chain to replay — the value
31-
was already inert before this change, and it is inert after. The one channel that speaks is
32-
`tsc`: a TypeScript author annotating with `Connector`, `ScheduledExport`, `ScheduleState`,
33-
`CacheWarmup`, `BackupConfig` or `DisasterRecoveryPlan` gets an excess-property error at the
34-
key and deletes it.
31+
was already inert before this change, and it is inert after.
32+
33+
The parse is not the only channel, and the two that speak are worth stating exactly,
34+
because a reader who stops at "non-strict schema" will conclude the opposite:
35+
36+
- **`os validate` / `os build` NAME the dropped key**, for the one deleted position a stack
37+
manifest reaches (`connectors[].syncConfig.schedule`). `os validate` exits 0 and reports
38+
`connectors.<name>.syncConfig.schedule: 'schedule' is not a declared connector key, so its
39+
value is dropped at load.` — in the text face and in `--json`'s `warnings`; `os build`
40+
prints the same line under `Undeclared authoring keys — dropped at load (#3786)`. The
41+
channel is `lintUnknownAuthoringKeys`, which walks every stack collection whose entry
42+
schema is strip-mode, and `connectors` is one. **`os validate --strict` treats that warning
43+
as an error and EXITS 1**, so a pipeline running `--strict` over an otherwise-clean stack
44+
refuses the upgraded manifest until the key is deleted. `os migrate meta` still lists
45+
nothing, in either direction.
46+
- **`tsc`**: a TypeScript author annotating with `Connector`, `ScheduledExport`,
47+
`ScheduleState`, `CacheWarmup`, `BackupConfig` or `DisasterRecoveryPlan` gets an
48+
excess-property error at the key and deletes it.
49+
50+
The other six positions are not reachable from a stack manifest, so no CLI walk visits them:
51+
for those the parse-level strip really is the whole of it.
3552

3653
**What stays, byte-identical:** every other key of the five schemas and every export — no def
3754
leaves the public surface. `ScheduledExport.schedule` / `ScheduleExportRequest.schedule` keep
@@ -40,6 +57,19 @@ their `timezone` (still defaulting to `UTC`); `ScheduleState` keeps `timezone`,
4057
key); `CacheWarmup.strategy` keeps its `scheduled` member — a value, not a position the
4158
ruling names, and exactly as inert as before.
4259

60+
**One published TS MEMBER does leave, and "no def leaves" does not cover it.** The required
61+
`cronExpression: string` member is deleted from `ScheduleExportInput` in
62+
`contracts/export-service.ts` — the input type of `IExportService.scheduleExport`, a
63+
published runtime TS interface (both names are in `api-surface/contracts.json`). It follows
64+
the two spec positions it mirrored: with `ScheduledExport.schedule.cronExpression` gone, an
65+
input demanding the key would ask a provider for a cadence it cannot store. The interface,
66+
the method and every other member stay. Measured blast radius: no source outside
67+
`packages/spec` names `ScheduleExportInput` or `IExportService` — 0 hits in this repo
68+
(positive control: a symbol of the same class resolves outside `packages/spec` in the same
69+
sweep) and 0 in `objectui` (control: 1326 files there import `@objectstack/spec`). An
70+
implementor that *does* exist off-tree drops the member from its object literal; a caller
71+
constructing a `ScheduleExportInput` drops it from the literal it passes.
72+
4373
**Not in scope, deliberately:** `CronSchedule.expression` (`system/job.zod.ts`, read by
4474
`croner` — the ONE cron slot the platform evaluates), `KnowledgeRefreshPolicy.cron`
4575
(experimental by design), `Object.titleFormat`, and the `PromptTemplate` pair (marked, not
@@ -48,11 +78,13 @@ retired, on its sibling card).
4878
## This change states no before/after rewrite, because there is none
4979

5080
A breaking changeset in this repo normally states the old spelling beside the new one.
51-
This one has no such pair to state: the same document parses before and after, the value
52-
was inert in both, and nothing refuses it — so a metadata upgrader has no edit to make and
53-
`os migrate meta` has nothing to list. The one party with work to do is a TypeScript
54-
author, and the compiler names the key and the line for them. What follows is guidance for
55-
authoring a cadence going forward, not a rewrite of an existing document.
81+
This one has no such pair to state: the same document PARSES before and after, the value
82+
was inert in both, and no conversion can be written for it — so a metadata upgrader has no
83+
edit to make and `os migrate meta` has nothing to list. That is a statement about the
84+
migration chain, not about silence: `os validate` / `os build` do name the dropped
85+
connector key and `os validate --strict` refuses on it (above), and `tsc` names the key and
86+
the line for a TypeScript author. What follows is guidance for authoring a cadence going
87+
forward, not a rewrite of an existing document.
5688

5789
## What to write instead
5890

packages/spec/src/cron-typed-positions-retirement.test.ts

Lines changed: 36 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,34 @@ import {
3636
// a `retiredKey()` tombstone, a `RETIRED_KEYS_BY_MAJOR[18]` entry, an ADR-0087 D2
3737
// conversion or a D3 semantic entry.
3838
//
39-
// That makes the observable consequence a SILENT STRIP, not a refusal: none of
39+
// That makes the PARSE-layer consequence a SILENT STRIP, not a refusal: none of
4040
// the five schemas is `.strict()`, so zod drops an authored value and answers
4141
// `success: true` (ADR-0104's shape). These pins record exactly that — what an
42-
// author who keeps writing one of these keys actually gets — so the day someone
43-
// changes the route, the change is loud here rather than invisible in the field.
42+
// author who keeps writing one of these keys gets from the SCHEMA — so the day
43+
// someone changes the route, the change is loud here rather than invisible in
44+
// the field.
45+
//
46+
// ⚠️ The parse is NOT the whole channel, and the difference is measured rather
47+
// than reasoned. Above the parse, `lintUnknownAuthoringKeys` (#3786) walks every
48+
// `PLURAL_TO_SINGULAR` collection whose entry schema is strip-mode, and
49+
// `connectors: 'connector'` is one of them — so for the ONE of the seven a stack
50+
// manifest reaches, the CLI NAMES the dropped key:
51+
//
52+
// • `os validate` — exit 0, and prints (`--json` carries the same string in
53+
// `warnings`):
54+
// connectors.sap_erp.syncConfig.schedule: 'schedule' is not a declared
55+
// connector key, so its value is dropped at load.
56+
// • `os validate --strict` — exit 1. Measured on an otherwise-clean stack:
57+
// the same manifest WITHOUT the key is 0 warnings / exit 0, WITH it is
58+
// 1 warning / exit 1. A CI running `--strict` REFUSES the upgraded manifest.
59+
// • `os build` — the same line, under `Undeclared authoring keys (1) —
60+
// dropped at load (#3786)`.
61+
// • `os migrate meta` — still lists nothing, in either direction. There is no
62+
// prescription to make, which is the half the bare deletion really does own.
63+
//
64+
// ⇒ ⛔ Do not read these pins as "the author is never told". They pin the schema
65+
// layer. The author-facing loss is louder than a bare `safeParse` suggests, and
66+
// it is louder than the ruling comment's cost statement assumed.
4467

4568
const CRON = '0 6 * * MON';
4669
/** The envelope the old schema normalized the bare string into — dropped just the same. */
@@ -276,9 +299,17 @@ describe('[#16320] the one manifest-reachable position — what an upgrading sta
276299
// ⚠️ THE CONSEQUENCE OF THE 直接删 RULING, pinned. `DataSyncConfig.schedule`
277300
// is the only one of the seven a stack manifest reaches (`stack.zod.ts`
278301
// `connectors[]` → `connector.zod.ts` `syncConfig` → `schedule`). With no
279-
// tombstone the manifest still LOADS, and the cadence the author wrote is
280-
// discarded without a word — the ADR-0104 silent-strip shape, accepted
302+
// tombstone the manifest still LOADS and the cadence the author wrote is
303+
// dropped — the ADR-0104 silent-strip shape at the PARSE, accepted
281304
// deliberately by the ruling.
305+
//
306+
// ⛔ Silent at the parse is not silent to the author, and the module
307+
// docblock carries the measurement: on this exact path `os validate` prints
308+
// `connectors.<name>.syncConfig.schedule: 'schedule' is not a declared
309+
// connector key, so its value is dropped at load.`, `os build` prints it
310+
// under its undeclared-keys block, and `os validate --strict` EXITS 1 on it.
311+
// This assertion is about `ObjectStackSchema` alone; it does not measure —
312+
// and must not be quoted as — what the CLI tells the author.
282313
const { ObjectStackSchema } = await import('./stack.zod');
283314
const parsed = ObjectStackSchema.safeParse({
284315
connectors: [{ ...CONNECTOR_WELL_FORMED, syncConfig: { ...SYNC_WELL_FORMED, schedule: CRON } }],

packages/spec/src/migrations/registry.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5410,8 +5410,14 @@ const step18: MigrationStep = {
54105410
'(the D7 ledger row `cron-declared-unwired`). All seven are DELETED OUTRIGHT — no ' +
54115411
'retiredKey tombstone, no RETIRED_KEYS_BY_MAJOR[18] entry, no D2 conversion and no D3 ' +
54125412
'semantic entry — so this step replays nothing for them and `migrate meta` lists no ' +
5413-
'edit: the keys simply stop existing, and the six positions on non-strict schemas are ' +
5414-
'stripped in silence rather than refused. That is the maintainer ruling of 2026-09-10 ' +
5413+
'edit: the keys simply stop existing. That the chain is silent does NOT make the ' +
5414+
'deletion silent to an author: the PARSE strips (no schema here is `.strict()`), but ' +
5415+
'above it `lintUnknownAuthoringKeys` (#3786) names the dropped key for the one ' +
5416+
'position a stack manifest reaches — `os validate` and `os build` both print ' +
5417+
'`connectors.<name>.syncConfig.schedule: \'schedule\' is not a declared connector ' +
5418+
'key, so its value is dropped at load.`, and `os validate --strict` EXITS 1 on that ' +
5419+
'warning. The other six positions are unreachable from a manifest, so for those the ' +
5420+
'parse-level strip is the whole of it. That is the maintainer ruling of 2026-09-10 ' +
54155421
'on the retirement PR, taken over the seat recommendation to keep the connector D2, on ' +
54165422
'the reading that customers do not upgrade major by major in order.',
54175423
conversionIds: [

0 commit comments

Comments
 (0)