Repository navigation
Commit 2882528
fix(spec): the authorable-surface reachability roots include the unregistered kind schemas (#18131)
Fixes #17356
Clause-②: yes
## What was wrong
`computeSurfaceReachability()` in
`packages/spec/scripts/build-schemas.ts` — the whole of check (c)
**proof 2** — built its BFS root set from
`listMetadataTypeSchemaTypes()`. That function unions
`BUILTIN_METADATA_TYPE_SCHEMAS` with the `EXTRA_METADATA_TYPE_SCHEMAS`
overlay and, per **#6245**, pointedly does **not** enumerate
`UNREGISTERED_KIND_SCHEMAS`.
`connector` lives in that third map. So the BFS never started from it,
`integration/DataSyncConfig` — two hops away, through
`connector.syncConfig` unwrapped once through `optional` — answered
`null`, and a bare deletion of one of its baseline lines was waived as
"an over-collected entry, never parsed against a metadata document".
`stack.connectors[]` and `PUT /api/v1/meta/connector/:name` parse a real
metadata document through that def on every boot.
The gate's own docblock names this as the dangerous direction: a false
"reachable" demands a tombstone too many, **a false "unreachable" would
waive one silently**. The tree held exactly that false unreachable.
## What this does NOT do
It does **not** add `connector` to `listMetadataTypeSchemaTypes()`. Two
different questions wear the same words, and the gate conflated them:
- **"is this a REGISTERED metadata type?"** — what
`listMetadataTypeSchemaTypes()` answers, correctly. Enrolling the
unregistered kinds there would grant a status #6245 deliberately
withheld (enum member, registry entry, create seed, a place in the #4001
campaign count), and #2657's B/C decision stays open and unprejudged.
- **"is there an AUTHOR who could be authoring against this def?"** —
the only question a *reachability* root set asks, because the sole
consequence of `null` is waiving a tombstone on the grounds that nobody
can receive the prescription.
So the gate now enumerates its own reachability root union —
`reachabilityRootTypes()`, reading `listUnregisteredKindSchemaTypes()`,
which exists (#6931) so a check can ENUMERATE that map and for nothing
else, and whose listing grants nothing. `listMetadataTypeSchemaTypes()`
is unchanged, and `packages/spec/src/kernel/metadata-type-schemas.ts` is
untouched by this PR.
## Measured, on `c548dea2`
**The waiver, both ways.** The card's literal repro key
(`integration/DataSyncConfig:schedule`) already landed on `main` with
the #16320 retirement, so the demonstration deletes a live sibling key
under the same def — `timestampField`, removed from
`DataSyncConfigSchema` and from `authorable-surface/integration.json`,
no tombstone, no registry entry. The gate judges the DEF half of the key
(`key.slice(0, key.indexOf(':'))`), so this is the identical code path.
| run | `check:authorable-surface` | `gen:schema` | verdict printed |
|:---|:---|:---|:---|
| before (`ca78860`) | exit **0** | exit **0** |
`integration/DataSyncConfig:timestampField — def not reachable from the
26 metadata-type roots` |
| after | exit **1** | exit **1** |
`integration/DataSyncConfig:timestampField — def reachable from the
metadata-type roots; the entry at ca78860 was LIVE (never
tombstoned).` |
Both legs restored from `HEAD` and proven byte-identical with `git
hash-object` against the `HEAD` blob, with `git diff HEAD` empty.
**Root-set delta.** 26 roots to 30; the four added are exactly
`analytics_cube`, `connector`, `sharing_rule`, `webhook`; none dropped.
**Closure delta (the positive control, and more than the card scoped).**
Over the 1523 emitted defs, comparing the verdict map computed from the
old root set against the new one:
- **0** defs went from reachable to `null` — a root set that grows never
shrinks a closure. `ObjectSchema` (`data/Object`) stays `root-graph`.
- **17** defs stop being waivable (`null` verdicts 1035 to 1018): 16 to
`root-graph` and `shared/FieldMapping` to `derived-clone`. So the answer
to "is `integration/DataSyncConfig` the whole of the gap" is **no** — it
is 1 of 17. The other 16 are the connector / sharing-rule /
analytics-cube families: `data/CubeJoin`, `data/Dimension`,
`data/Metric`, `integration/CircuitBreakerConfig`,
`integration/ConnectorAction`, `integration/ConnectorFieldMapping`,
`integration/ConnectorHealth`, `integration/ConnectorInstanceAuth` and
its four auth branches, `integration/ConnectorTrigger`,
`integration/HealthCheckConfig`, `integration/RetryConfig`.
- **7** more sharpen from `derived-clone` to `root-graph`
(`automation/Webhook`, `data/Cube`, `integration/Connector`,
`integration/DeclarativeConnectorEntry`, `integration/WebhookConfig`,
`security/CriteriaSharingRule`, `security/SharingRule`) — already
refused before, refused now, with a truer reason printed.
Nothing in the committed baselines moves: the waiver only fires on a
deletion, so `check:authorable-surface` on the pristine tree is green
before and after, and `pnpm --filter @objectstack/spec build` leaves the
working tree clean.
**Ablation.** With `reachabilityRootTypes()` swapped back for
`listMetadataTypeSchemaTypes()` at the one call site — mutation proven
on disk by anchor counts (1 to 0 and 0 to 1 on the two spellings) before
the run, and the gate is executed by `tsx` from source with no `dist` in
between — the new pin goes red on `expect(status).toBe(1)` receiving
`0`: the deletion is waived again. Restored from `HEAD`, hash-verified.
## The pin
`packages/spec/scripts/build-schemas-check-mode.test.ts` gets one case
reading **both directions off one seeded state**, because either alone
is satisfiable by a gate that is simply wrong in the other: "always
reachable" passes the first assertion and destroys proof 2; "always
unreachable" passes the second and restores the defect.
It is read **twice**. Once with `OS_EAGER_SCHEMAS=1` — the way
`gen:schema` and `check:authorable-surface` actually run, and the only
regime where `reachableVia()` can answer `root-graph` at all; without
the flag `lazySchema()` hands back a Proxy, `zodByDefKey` holds the
Proxy while the walk visits the resolved target, and the same verdict
arrives through the derived-clone bridge. The verdict is what the gate
acts on, so the verdict is pinned in both regimes and the wording only
in the eager one.
The `beforeAll` fixture guard asserts `connector` is still absent from
`listMetadataTypeSchemaTypes()` and still present in
`listUnregisteredKindSchemaTypes()` — acceptance 4 stated where it fails
rather than where it is believed, and the thing that keeps this pin
discriminating: reverse #6245 and the case would pass while asserting
nothing.
## Acceptance notes
Observations from the surrounding code, noted and deliberately not acted
on here:
- `packages/spec/scripts/liveness/check-liveness.mts` builds its
governance denominator from `listMetadataTypeSchemaTypes()` too, under
the comment "i.e. exactly the set of authorable metadata types" — the
same sentence #17356 falsified for the reachability gate. `webhook` is
patched in by hand (an `EXTRA_SCHEMAS` row plus
`liveness/webhook.json`); `connector`, `sharing_rule` and
`analytics_cube` are in neither `GOVERNED` nor `PENDING_GOVERNANCE`, so
`report.ungoverned` cannot name them — they are not in its denominator.
Filed separately rather than fixed here: whether those three should
carry liveness ledgers is the governance question #2657 leaves open, not
a root-enumeration bug.
- `packages/spec/src/ui/door-reachability.testkit.ts` runs the same
enumeration and is **not** blind, because it pushes `ObjectStackSchema`
as an extra root and reaches `connector` through `stack.connectors[]`.
Corroborating, and the reason the fix here reads the kind map rather
than adding the stack root: the kind map is the set of authoring doors,
the stack schema is one of them.
- The docblock on `listUnregisteredKindSchemaTypes()` still counts five
kinds ("webhook / connector / sharing_rule / theme / analytics_cube");
the map holds four since #10485 retired `theme`. One line of prose in a
file this PR's declared surface marks read-only, so it is left alone
rather than folded in.
## Scope
Two files, both under `packages/spec/scripts/`. Neither is in the
package's `files[]`, so nothing published moves — measured:
`reachabilityRootTypes` has 0 hits across `dist`, `json-schema`,
`liveness`, `prompts`, `api-surface`, `llms.txt`, `spec-changes.json`,
`README.md` and `src/**/*.zod.ts`, against a positive control
(`listUnregisteredKindSchemaTypes`) that has 7. Hence `skip-changeset`.
---
## The ruling this PR executes, and the shape it finally took
Card #17356's options went to the maintainer twice. The governing ruling
is the second one, comment `5696910871` — **甲**:
> Letter **A** lands FIRST, in its own anchor-only PR. Then PR #18131
merges main.
> **B** is refused: for `integration/DataSyncConfig:schedule` it would
reverse the maintainer's 2026-09-10 retirement ruling recorded in the
source.
> **C** is filed: **#18301** — check (c) gains a fourth proof so a
guidance-route retirement on a reachable def proves itself;
`data/Metric:filters` is its specimen.
> **D** is refused.
That supersedes the earlier batch #135 reply (「135 同意」 on 「A 本 PR 内独立提交
+ C 另立卡」), which had letter A as a second commit inside this PR. This
body described that older shape until the merge landed; it is corrected
here rather than left to become the squash commit message.
**What actually happened, in order:**
1. **Letter A landed alone**, as PR #18485 — one file,
`packages/spec/authorable-surface.base.json`, +1024/-1056 — merged
2026-09-16T16:37:10Z as squash `fed4a15ab5`. The anchor advanced to
`baseRev 85c6d76…`, 7804 keys.
2. **This PR then merged main** through `scripts/pm/os-regen-merge.sh` —
never a bare `git merge`, because `.gitattributes:143` routes the anchor
to `merge=os-regen`, a driver that exits 0 with no conflict markers
while dropping one side. Merge commit `59c50319` (parents `dc98ee22a3` +
`fed4a15ab5`), then the wrapper's step-3 commit `251d76a2`, which takes
main's side of the anchor and changes nothing else.
3. **Letter C (#18301) is untouched here** — no fourth proof is added to
check (c) — and the #16320 retirement of
`integration/DataSyncConfig:schedule` is not reversed: the key is absent
from the emitted surface and from the anchor.
**The anchor is no longer part of this PR.** At head `251d76a2` the
committed anchor is byte-identical to `origin/main`'s (`cmp` exit 0 at
both `8cf527f8` and `fb6b2c36`), and `authorable-surface.base.json` does
not appear in this PR's diff at all. The earlier branch-local advance to
`b9598e9cab9d` (7772 keys) is history inside commit `dc98ee22a3`; the
head carries main's `85c6d76e` (7804 keys). Anything this body said
about "7772 vs 7772" belonged to that superseded shape.
**The diff at head is exactly the two scripts** — `build-schemas.ts`
+56/-4 and `build-schemas-check-mode.test.ts` +136/-2, 192+/6- —
byte-identical to the original implementation diff
`ceb66899^..c548dea`, with main having touched neither file between the
original base and `fed4a15ab5`, nor since. Nothing was swallowed by the
merge in either direction.
**The seven reds are gone.** `Build Core`, `Dogfood Verify CLI`,
`Dogfood Regression Gate` and its three shards, and `Temporal
Conformance (live PG + MySQL)` were all `failure` on the pre-merge head
`dc98ee22a3`, every one carrying the same annotation — `command
(…/packages/spec) …/pnpm run build exited (1)` — from one root cause:
this PR's own widened root set meeting a **stale committed anchor**,
under which two keys retired on main after `53ef05744f37` surfaced as
deletions this branch appeared to make. Advancing the anchor on main is
what removed the premise. On head `251d76a2`, latest run per check NAME:
**31 success, 4 skipped** (`Build Docs`, `Check Changeset`, `Console Pin
Gate`, `Packed-tarball smoke (opt-in)` — no verdict, not green), 0
failure, 0 in progress.
**`skip-changeset`, re-measured at head.** The two diff paths are under
`packages/spec/scripts/` and are matched by none of the package's ten
`files[]` entries; the anchor is not in the diff; `gen:schema` and
`check:authorable-surface` on the pristine head both exit 0 and leave
`git status --porcelain` empty. Nothing published moves, so no changeset
is owed.
**Contract review.** At-tier review on this exact head returned
**PASS**; the record and the seat's adjudication of its escalated flags
are in the PR thread. The `Clause-②: yes` declaration above is the
conservative route into that review, not a claim that this diff
publishes bytes — the review measured the bytes and found none.
---
_Generated by [Claude Code](https://claude.ai/code)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 97233b9 commit 2882528
2 files changed
Lines changed: 192 additions & 6 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
65 | 71 | | |
66 | 72 | | |
67 | 73 | | |
| |||
496 | 502 | | |
497 | 503 | | |
498 | 504 | | |
499 | | - | |
| 505 | + | |
500 | 506 | | |
501 | 507 | | |
502 | 508 | | |
| |||
505 | 511 | | |
506 | 512 | | |
507 | 513 | | |
508 | | - | |
| 514 | + | |
509 | 515 | | |
510 | 516 | | |
511 | 517 | | |
512 | 518 | | |
| 519 | + | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
| 523 | + | |
| 524 | + | |
| 525 | + | |
| 526 | + | |
| 527 | + | |
| 528 | + | |
| 529 | + | |
| 530 | + | |
| 531 | + | |
| 532 | + | |
| 533 | + | |
| 534 | + | |
| 535 | + | |
| 536 | + | |
513 | 537 | | |
514 | 538 | | |
515 | 539 | | |
| |||
957 | 981 | | |
958 | 982 | | |
959 | 983 | | |
| 984 | + | |
| 985 | + | |
| 986 | + | |
| 987 | + | |
| 988 | + | |
| 989 | + | |
| 990 | + | |
| 991 | + | |
| 992 | + | |
| 993 | + | |
| 994 | + | |
| 995 | + | |
| 996 | + | |
| 997 | + | |
| 998 | + | |
| 999 | + | |
| 1000 | + | |
| 1001 | + | |
| 1002 | + | |
| 1003 | + | |
| 1004 | + | |
| 1005 | + | |
| 1006 | + | |
960 | 1007 | | |
961 | 1008 | | |
962 | 1009 | | |
| |||
990 | 1037 | | |
991 | 1038 | | |
992 | 1039 | | |
| 1040 | + | |
993 | 1041 | | |
994 | 1042 | | |
995 | 1043 | | |
| |||
1023 | 1071 | | |
1024 | 1072 | | |
1025 | 1073 | | |
| 1074 | + | |
| 1075 | + | |
| 1076 | + | |
| 1077 | + | |
| 1078 | + | |
| 1079 | + | |
| 1080 | + | |
| 1081 | + | |
| 1082 | + | |
| 1083 | + | |
| 1084 | + | |
| 1085 | + | |
| 1086 | + | |
| 1087 | + | |
| 1088 | + | |
| 1089 | + | |
| 1090 | + | |
| 1091 | + | |
| 1092 | + | |
| 1093 | + | |
1026 | 1094 | | |
1027 | 1095 | | |
1028 | 1096 | | |
| |||
1138 | 1206 | | |
1139 | 1207 | | |
1140 | 1208 | | |
| 1209 | + | |
| 1210 | + | |
| 1211 | + | |
| 1212 | + | |
| 1213 | + | |
| 1214 | + | |
| 1215 | + | |
| 1216 | + | |
| 1217 | + | |
| 1218 | + | |
| 1219 | + | |
| 1220 | + | |
| 1221 | + | |
| 1222 | + | |
| 1223 | + | |
| 1224 | + | |
| 1225 | + | |
| 1226 | + | |
| 1227 | + | |
| 1228 | + | |
| 1229 | + | |
| 1230 | + | |
| 1231 | + | |
| 1232 | + | |
| 1233 | + | |
| 1234 | + | |
| 1235 | + | |
| 1236 | + | |
| 1237 | + | |
| 1238 | + | |
| 1239 | + | |
| 1240 | + | |
| 1241 | + | |
| 1242 | + | |
| 1243 | + | |
| 1244 | + | |
| 1245 | + | |
| 1246 | + | |
| 1247 | + | |
| 1248 | + | |
| 1249 | + | |
| 1250 | + | |
| 1251 | + | |
| 1252 | + | |
| 1253 | + | |
| 1254 | + | |
| 1255 | + | |
| 1256 | + | |
| 1257 | + | |
| 1258 | + | |
| 1259 | + | |
| 1260 | + | |
| 1261 | + | |
| 1262 | + | |
| 1263 | + | |
| 1264 | + | |
| 1265 | + | |
| 1266 | + | |
| 1267 | + | |
| 1268 | + | |
| 1269 | + | |
| 1270 | + | |
| 1271 | + | |
| 1272 | + | |
| 1273 | + | |
| 1274 | + | |
1141 | 1275 | | |
1142 | 1276 | | |
1143 | 1277 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
85 | 85 | | |
86 | 86 | | |
87 | 87 | | |
| 88 | + | |
88 | 89 | | |
89 | 90 | | |
90 | 91 | | |
| |||
1142 | 1143 | | |
1143 | 1144 | | |
1144 | 1145 | | |
| 1146 | + | |
| 1147 | + | |
| 1148 | + | |
| 1149 | + | |
| 1150 | + | |
| 1151 | + | |
| 1152 | + | |
| 1153 | + | |
| 1154 | + | |
| 1155 | + | |
| 1156 | + | |
| 1157 | + | |
| 1158 | + | |
| 1159 | + | |
| 1160 | + | |
| 1161 | + | |
| 1162 | + | |
| 1163 | + | |
| 1164 | + | |
| 1165 | + | |
| 1166 | + | |
| 1167 | + | |
| 1168 | + | |
| 1169 | + | |
| 1170 | + | |
| 1171 | + | |
| 1172 | + | |
| 1173 | + | |
| 1174 | + | |
| 1175 | + | |
| 1176 | + | |
| 1177 | + | |
| 1178 | + | |
| 1179 | + | |
| 1180 | + | |
| 1181 | + | |
| 1182 | + | |
| 1183 | + | |
| 1184 | + | |
| 1185 | + | |
| 1186 | + | |
| 1187 | + | |
| 1188 | + | |
| 1189 | + | |
| 1190 | + | |
| 1191 | + | |
| 1192 | + | |
1145 | 1193 | | |
1146 | 1194 | | |
1147 | 1195 | | |
1148 | | - | |
1149 | | - | |
| 1196 | + | |
| 1197 | + | |
| 1198 | + | |
| 1199 | + | |
| 1200 | + | |
1150 | 1201 | | |
1151 | 1202 | | |
1152 | 1203 | | |
| |||
1163 | 1214 | | |
1164 | 1215 | | |
1165 | 1216 | | |
1166 | | - | |
| 1217 | + | |
1167 | 1218 | | |
1168 | 1219 | | |
1169 | 1220 | | |
| |||
2202 | 2253 | | |
2203 | 2254 | | |
2204 | 2255 | | |
2205 | | - | |
| 2256 | + | |
| 2257 | + | |
2206 | 2258 | | |
2207 | 2259 | | |
2208 | 2260 | | |
| |||
0 commit comments