@@ -27,9 +27,7 @@ import {
2727 clearOwnedOutputs ,
2828} from './lib/json-schema-out-dir' ;
2929import {
30- AUTHORABLE_SURFACE_DESCRIPTION ,
3130 AUTHORABLE_SURFACE_DIR_NAME ,
32- SCHEMA_MANIFEST_DESCRIPTION ,
3331 SCHEMA_MANIFEST_DIR_NAME ,
3432 aggregateCategoryShards ,
3533 authorableSurfaceShardTexts ,
@@ -38,6 +36,7 @@ import {
3836 serializeShard ,
3937 writeShards ,
4038 type GitRun ,
39+ type ShardArrayField ,
4140} from './lib/sharded-artifacts' ;
4241// The #4666 default-value ratchet: what an author gets when they OMIT a key.
4342// Its own module because the fingerprint's normalisation rules — and the
@@ -483,15 +482,11 @@ if (defKeyCollisions.length > 0) {
483482// run means a code change unpublished a schema — fail loudly instead of
484483// letting gen:docs quietly delete its reference docs (#2978). Deliberate
485484// removals must delete the key from the manifest in the same PR.
486- /**
487- * The manifest's description — the procedure a reader who opens a shard to
488- * delete a line follows. Until #4725 it ended "remove a key ONLY for a
489- * deliberate retirement", which was the entire requirement and was checked by
490- * nothing; it now names the gate and the table that answer for a removal. It
491- * lives in scripts/lib/sharded-artifacts.ts with the writer that stamps it into
492- * every shard (#5837).
493- */
494- const MANIFEST_DESCRIPTION = SCHEMA_MANIFEST_DESCRIPTION ;
485+ // The manifest's and the authorable surface's shard descriptions used to be
486+ // re-exported through here. #5837 moved both to scripts/lib/sharded-artifacts.ts,
487+ // beside the writer that stamps them into every shard, and nothing in this file
488+ // has read them since — the import and the `MANIFEST_DESCRIPTION` alias were
489+ // residue no checker could see (#5475).
495490
496491/**
497492 * Every def key recorded across `json-schema.manifest/`, or null when the whole
@@ -1094,7 +1089,13 @@ function readSurfaceKeysAtRev(
10941089 git : GitRun ,
10951090 rev : string ,
10961091 dirName : string ,
1097- field : 'keys' | 'schemas' ,
1092+ // `ShardArrayField`, not a re-spelled copy of it. This parameter used to read
1093+ // `'keys' | 'schemas'` — a hand-written narrowing of the exported union that
1094+ // `readShardedKeysAtRev` below actually takes. When #4666 added `'defaults'`
1095+ // to `ShardArrayField` and a call site passing it, the copy here was left
1096+ // behind and no type checker existed to say so (#5475). Harmless at runtime,
1097+ // since the value is only forwarded, but it is the drift this program is for.
1098+ field : ShardArrayField ,
10981099 context : string ,
10991100) : { entries : string [ ] } | null {
11001101 const read = readShardedKeysAtRev ( git , rev , dirName , field ) ;
@@ -1453,12 +1454,33 @@ function assertAnchorMovesForward(git: GitRun, committedRev: string, resolvedRev
14531454}
14541455
14551456/**
1456- * Set when THIS run resolved the baseline from git. It is the ONLY input
1457- * `--update-base` may write the in-tree anchor from: an offline build must never
1458- * be able to advance the anchor to its own state (#5235). The second half of that
1459- * discipline is #5358 — no build writes it at all, only the explicit mode.
1457+ * What `resolveSurfaceBase()` resolved: the baseline itself, plus — only when
1458+ * the GIT path produced it — the anchor that path is allowed to write.
1459+ *
1460+ * `gitAnchor` is a returned field rather than the module-level assignment it
1461+ * used to be, and that is a type-checking fix, not a style one (#5475). The old
1462+ * shape declared `let gitResolvedAnchor: {...} | null = null` here and assigned
1463+ * it from INSIDE this function. TypeScript's control-flow analysis does not
1464+ * follow an assignment made in a function body, so at every top-level read below
1465+ * the variable was still narrowed to `null` — which made `if (gitResolvedAnchor)`
1466+ * a block whose body is typed `never`, i.e. the entire in-tree anchor writer
1467+ * (#5235/#5358/#5370/#5847, ~100 lines) was invisible to tsc while reading as
1468+ * ordinary checked code. Returning the value puts the assignment in the caller's
1469+ * own flow, where CFA can see it. Runtime behaviour is unchanged: the git path
1470+ * sets it, the in-tree path leaves it null, exactly as before.
14601471 */
1461- let gitResolvedAnchor : { rev : string ; keys : string [ ] } | null = null ;
1472+ type SurfaceBaseResolution = {
1473+ rev : string ;
1474+ doc : AuthorableSurface ;
1475+ /**
1476+ * Set when THIS run resolved the baseline from git. It is the ONLY input
1477+ * `--update-base` may write the in-tree anchor from: an offline build must
1478+ * never be able to advance the anchor to its own state (#5235). The second
1479+ * half of that discipline is #5358 — no build writes it at all, only the
1480+ * explicit mode.
1481+ */
1482+ gitAnchor : { rev : string ; keys : string [ ] } | null ;
1483+ } ;
14621484
14631485/**
14641486 * The committed authorable surface this PR started from: its content at
@@ -1483,7 +1505,7 @@ let gitResolvedAnchor: { rev: string; keys: string[] } | null = null;
14831505 * What is NOT offered is an env-var skip: that is precisely the bypass #4650
14841506 * closes. With no anchor of either kind this still exits 1.
14851507 */
1486- function resolveSurfaceBase ( ) : { rev : string ; doc : AuthorableSurface } | null {
1508+ function resolveSurfaceBase ( ) : SurfaceBaseResolution | null {
14871509 const git = gitInPackage ;
14881510 const committed = readCommittedSurfaceBase ( ) ;
14891511
@@ -1520,10 +1542,10 @@ function resolveSurfaceBase(): { rev: string; doc: AuthorableSurface } | null {
15201542 return null ;
15211543 }
15221544 const doc : AuthorableSurface = { keys : baseline . entries } ;
1523- gitResolvedAnchor = { rev, keys : doc . keys } ;
1545+ const gitAnchor = { rev, keys : doc . keys } ;
15241546 // The environment that CAN police the in-tree anchor is the one that must.
1525- if ( committed ) verifyCommittedSurfaceBase ( git , tip , gitResolvedAnchor , committed . doc ) ;
1526- return { rev, doc } ;
1547+ if ( committed ) verifyCommittedSurfaceBase ( git , tip , gitAnchor , committed . doc ) ;
1548+ return { rev, doc, gitAnchor } ;
15271549 }
15281550
15291551 if ( committed ) {
@@ -1535,6 +1557,9 @@ function resolveSurfaceBase(): { rev: string; doc: AuthorableSurface } | null {
15351557 return {
15361558 rev : committed . doc . baseRev ,
15371559 doc : { keys : committed . doc . keys } ,
1560+ // Offline: this run did not resolve an anchor from git, so it has nothing
1561+ // it is entitled to write one from (#5235).
1562+ gitAnchor : null ,
15381563 } ;
15391564 }
15401565
@@ -1722,11 +1747,20 @@ function checkManifestRemovals(git: GitRun, baseRev: string | null): void {
17221747 * is one resolution, shared — a second `resolveSurfaceBase()` call would ask git
17231748 * the same question twice and could answer it differently.
17241749 */
1725- let resolvedSurfaceBase : { rev : string ; doc : AuthorableSurface } | null = null ;
1750+ let resolvedSurfaceBase : SurfaceBaseResolution | null = null ;
1751+
1752+ /**
1753+ * The git-resolved anchor of this run, hoisted out of the block below because
1754+ * the in-tree anchor writer further down is a separate top-level block.
1755+ * Assigned HERE, in the module's own control flow, which is what keeps it typed
1756+ * as the union it is declared as — see `SurfaceBaseResolution.gitAnchor`.
1757+ */
1758+ let gitResolvedAnchor : { rev : string ; keys : string [ ] } | null = null ;
17261759
17271760{
17281761 const base = resolveSurfaceBase ( ) ;
17291762 resolvedSurfaceBase = base ;
1763+ gitResolvedAnchor = base ?. gitAnchor ?? null ;
17301764 // Whole defs first: check (c) below waives every baseline line under a def this
17311765 // build stopped emitting, on the grounds that this gate adjudicates it. Running
17321766 // it first is what makes that deferral true rather than circular.
@@ -1748,9 +1782,10 @@ let resolvedSurfaceBase: { rev: string; doc: AuthorableSurface } | null = null;
17481782 const violations : string [ ] = [ ] ;
17491783 const goneDefs = new Map < string , number > ( ) ; // def no longer emitted -> deleted key count
17501784 for ( const key of deletedKeys ) {
1751- const sep = key . indexOf ( ':' ) ;
1752- const defKey = key . slice ( 0 , sep ) ;
1753- const prop = key . slice ( sep + 1 ) ;
1785+ // Only the def half is read now. The leaf half fed the leaf-NAME match
1786+ // #5898 removed from route 3 (see the RETIRED_KEYS_BY_MAJOR message
1787+ // below); slicing it out survived the rewrite as a dead local (#5475).
1788+ const defKey = key . slice ( 0 , key . indexOf ( ':' ) ) ;
17541789 if ( ! generatedSchemas . has ( defKey ) ) {
17551790 goneDefs . set ( defKey , ( goneDefs . get ( defKey ) ?? 0 ) + 1 ) ;
17561791 continue ;
0 commit comments