5959 * 3. upstream moved -- same as (1) from this side, and the fix is a re-sync
6060 * rather than a revert, which is why the message names the procedure.
6161 *
62+ * ## The ref lives on the ENTRY, beside the digest it describes (objectui#8288)
63+ *
64+ * The pin used to carry ONE global `upstream.ref` while every digest was
65+ * per-file, and `--resync` set that global field on every run. So re-syncing a
66+ * single file re-labelled the others with a ref their digests had never been
67+ * taken from: after objectui#7749 re-synced the hook self-test to `70e77ec3b`,
68+ * this gate printed that ref beside `check-half-states.mjs` and `invoked-as.mjs`
69+ * too, whose digests were taken at `bf10debd5`. Both stayed GREEN — the digest
70+ * is the assertion and their bytes were untouched — while the provenance line
71+ * beside them was false. A check that reports a wrong fact confidently is this
72+ * repository's worst failure direction, and it was the gate itself doing it.
73+ *
74+ * So `ref` is a required field on each `files[]` entry and the global one is
75+ * RETIRED, not kept as a default. A default would have to be read as
76+ * `entry.ref ?? pin.upstream.ref`, which is the same false label re-spelled as
77+ * a feature: an absent `ref` cannot be distinguished from one nobody updated.
78+ * Provenance is a property of a digest, so it is stored where the digest is,
79+ * and `validatePin` REFUSES a pin that still carries `upstream.ref` rather than
80+ * silently ignoring it. `upstream.repo` stays global because it is measurably
81+ * uniform — every entry is a port from the same repository — and splitting it
82+ * would be a speculative field with no reader.
83+ *
6284 * ⛔ What it deliberately does NOT do is fetch anything. A gate that reached
6385 * api.github.com would be red on a network hiccup and green on a cached 200,
6486 * and this repository's whole reason for owning a patrol is that a check which
82104 * node scripts/check-upstream-port-parity.mjs --resync /tmp/up.mjs --ref <the commit sha>
83105 *
84106 * `--resync` applies the declared divergences FORWARD onto the new upstream
85- * text, writes the ported file, and rewrites the pin's ref and digest. It is
107+ * text, writes the ported file, and rewrites THAT ENTRY's ref and digest --
108+ * only that entry's, so a re-sync can never re-label a file it did not read. It is
86109 * the only supported way to move the pin, because the alternative -- editing a
87110 * digest by hand until the gate goes green -- is indistinguishable from
88111 * baselining the drift it exists to catch. Afterwards, run the ported file's
@@ -178,10 +201,15 @@ export function validatePin(pin) {
178201 if ( typeof up . repo !== 'string' || ! / ^ [ \w . - ] + \/ [ \w . - ] + $ / . test ( up . repo ) ) {
179202 bad ( '`upstream.repo` is not an `owner/name` repository' ) ;
180203 }
181- // A ref that is not a full commit sha cannot identify one tree. A branch
182- // name would make the pin read as precise while naming a moving target.
183- if ( typeof up . ref !== 'string' || ! HEX40 . test ( up . ref ) ) {
184- bad ( '`upstream.ref` is not a 40-character commit sha' ) ;
204+ // ⛔ RETIRED (objectui#8288). One global ref beside per-file digests is
205+ // the false-provenance bug itself; refusing it here is what makes the
206+ // retirement real, rather than a field the gate quietly stops reading
207+ // while the pin goes on carrying a stale value that looks authoritative.
208+ if ( 'ref' in up ) {
209+ bad (
210+ '`upstream.ref` is RETIRED — the ref is provenance for a digest, so it belongs on each ' +
211+ '`files[]` entry beside the digest it was taken with. Move it there and delete this field.' ,
212+ ) ;
185213 }
186214 }
187215 if ( ! Array . isArray ( pin . files ) || pin . files . length === 0 ) {
@@ -202,6 +230,14 @@ export function validatePin(pin) {
202230 if ( seen . has ( f . ported ) ) bad ( `${ at } .ported is pinned twice (${ f . ported } )` ) ;
203231 seen . add ( f . ported ) ;
204232 }
233+ // A ref that is not a full commit sha cannot identify one tree. A branch
234+ // name would make the pin read as precise while naming a moving target.
235+ // Required, never defaulted: an entry with no ref is an entry whose digest
236+ // has no provenance, and the whole point of objectui#8288 is that a ref
237+ // supplied from somewhere else is worse than one that is missing.
238+ if ( typeof f . ref !== 'string' || ! HEX40 . test ( f . ref ) ) {
239+ bad ( `${ at } .ref is not a 40-character commit sha — every entry states the ref its digest was taken from` ) ;
240+ }
205241 if ( typeof f . upstreamSha256 !== 'string' || ! HEX64 . test ( f . upstreamSha256 ) ) {
206242 bad ( `${ at } .upstreamSha256 is not a 64-character SHA-256 digest` ) ;
207243 }
@@ -318,7 +354,8 @@ function resyncCommand(pin, entry) {
318354 ` git -C <objectstack checkout> fetch origin main\n` +
319355 ` git -C <objectstack checkout> show origin/main:${ entry . upstreamPath } > /tmp/upstream.mjs\n` +
320356 ` node scripts/check-upstream-port-parity.mjs --resync /tmp/upstream.mjs --ref <commit sha>\n` +
321- ` Upstream is ${ pin . upstream . repo } ; the pin currently names ${ pin . upstream . ref } .`
357+ ` Upstream is ${ pin . upstream . repo } ; the pin names ${ entry . ref } for THIS file. ` +
358+ `Other entries carry their own refs and are not affected by re-syncing this one.`
322359 ) ;
323360}
324361
@@ -370,6 +407,29 @@ export function resyncWriteVerdict(portedPath, { allowGoverned = false } = {}) {
370407 } ;
371408}
372409
410+ /**
411+ * The pin after re-syncing ONE entry: that entry gets the new ref and digest,
412+ * and EVERY OTHER ENTRY IS RETURNED UNTOUCHED.
413+ *
414+ * Pure, so the self-test drives the real transform rather than a restatement of
415+ * it -- and the property that matters is one a fixture can actually assert.
416+ * objectui#8288: the old spelling was `pin.upstream.ref = ref`, a write to a
417+ * ledger-wide field from inside a per-file operation, so re-syncing one file
418+ * silently re-labelled the rest with a ref their digests were never taken from.
419+ * The digests stayed correct and the gate stayed green, which is why nothing
420+ * caught it for months. Keeping the untouched entries structurally untouched --
421+ * rather than rewriting them with values that happen to match -- is what makes
422+ * that class of bug unavailable here.
423+ *
424+ * @returns {object } a new pin; the argument is not mutated.
425+ */
426+ export function resyncedPin ( pin , portedPath , ref , upstreamSha256 ) {
427+ return {
428+ ...pin ,
429+ files : pin . files . map ( ( f ) => ( f . ported === portedPath ? { ...f , ref, upstreamSha256 } : f ) ) ,
430+ } ;
431+ }
432+
373433function main ( root = ROOT ) {
374434 let pin ;
375435 try {
@@ -401,7 +461,7 @@ function main(root = ROOT) {
401461 const verdict = verifyFile ( entry , portedText ) ;
402462 if ( verdict . ok ) {
403463 console . log (
404- `✓ ${ entry . ported } : byte-identical to ${ pin . upstream . repo } @${ pin . upstream . ref . slice ( 0 , 9 ) } :` +
464+ `✓ ${ entry . ported } : byte-identical to ${ pin . upstream . repo } @${ entry . ref . slice ( 0 , 9 ) } :` +
405465 `${ entry . upstreamPath } modulo ${ entry . divergences . length } declared divergence(s).` ,
406466 ) ;
407467 continue ;
@@ -420,20 +480,28 @@ function main(root = ROOT) {
420480 ) ;
421481 return 1 ;
422482 }
483+ // ⛔ Never collapse the refs into one (objectui#8288). Entries are pinned at
484+ // whichever ref each was last re-synced from, so naming a single one here
485+ // would restate the false-provenance bug at the summary line -- the exact
486+ // sentence that read as authoritative while two of three files were pinned
487+ // elsewhere. Distinct refs are listed; one ref prints as one ref.
488+ const refs = [ ...new Set ( pin . files . map ( ( f ) => f . ref . slice ( 0 , 9 ) ) ) ] ;
423489 console . log (
424- `✓ check-upstream-port-parity: ${ pin . files . length } ported file(s) match ` +
425- `${ pin . upstream . repo } @${ pin . upstream . ref . slice ( 0 , 9 ) } modulo their declared divergences. ` +
490+ `✓ check-upstream-port-parity: ${ pin . files . length } ported file(s) match ${ pin . upstream . repo } ` +
491+ `modulo their declared divergences, each at its own pinned ref ` +
492+ `(${ refs . length === 1 ? refs [ 0 ] : `${ refs . length } distinct: ${ refs . join ( ', ' ) } ` } ). ` +
426493 '(The digest is verified; the ref beside it is provenance and is NOT fetched.)' ,
427494 ) ;
428495 return 0 ;
429496}
430497
431498function list ( root = ROOT ) {
432499 const pin = readPin ( root ) ;
433- console . log ( `upstream: ${ pin . upstream . repo } @ ${ pin . upstream . ref } ` ) ;
500+ console . log ( `upstream: ${ pin . upstream . repo } ` ) ;
434501 for ( const entry of pin . files ) {
435502 const portedText = readPorted ( root , entry . ported ) ;
436503 console . log ( `\n${ entry . ported } <- ${ entry . upstreamPath } ` ) ;
504+ console . log ( ` pinned upstream ref : ${ entry . ref } ` ) ;
437505 console . log ( ` pinned upstream digest: ${ entry . upstreamSha256 } ` ) ;
438506 console . log ( ` declared divergences : ${ entry . divergences . length } ` ) ;
439507 for ( const d of entry . divergences ) {
@@ -501,12 +569,14 @@ function resync(argv, root = ROOT) {
501569 for ( const r of write . reasons ) ( write . write ? console . log : console . error ) ( r ) ;
502570 if ( ! write . write ) return 2 ;
503571 writeFileSync ( path . join ( root , entry . ported ) , text , 'utf8' ) ;
504- entry . upstreamSha256 = digest ( upstreamText ) ;
505- pin . upstream . ref = ref ;
506- writeFileSync ( path . join ( root , PIN_PATH ) , `${ JSON . stringify ( pin , null , 2 ) } \n` , 'utf8' ) ;
572+ const next = resyncedPin ( pin , entry . ported , ref , digest ( upstreamText ) ) ;
573+ writeFileSync ( path . join ( root , PIN_PATH ) , `${ JSON . stringify ( next , null , 2 ) } \n` , 'utf8' ) ;
507574 console . log (
508575 `✓ re-synced ${ entry . ported } from ${ pin . upstream . repo } @${ ref . slice ( 0 , 9 ) } :${ entry . upstreamPath } ` +
509- `(${ entry . divergences . length } divergence(s) re-applied) and bumped the pin.` ,
576+ `(${ entry . divergences . length } divergence(s) re-applied) and bumped THIS entry's ref and digest.` ,
577+ ) ;
578+ console . log (
579+ ` Every other pinned file keeps its own ref: a re-sync speaks only for the file it read.` ,
510580 ) ;
511581 console . log ( ' ⚠️ Now run the ported file\'s own suites: a divergence that still APPLIES but no longer' ) ;
512582 console . log ( ' makes sense is invisible here and visible only there.' ) ;
@@ -551,6 +621,7 @@ function selfTest() {
551621 const ENTRY = {
552622 ported : 'scripts/x.mjs' ,
553623 upstreamPath : 'scripts/x.mjs' ,
624+ ref : 'a' . repeat ( 40 ) ,
554625 upstreamSha256 : digest ( UP ) ,
555626 divergences : DIVS ,
556627 } ;
@@ -608,6 +679,47 @@ function selfTest() {
608679 t ( 'a divergence whose upstream anchor vanished fails the re-sync loudly' , lost . problems . length === 1 ) ;
609680 t ( '…naming the divergence that no longer applies' , lost . problems . join ( ' ' ) . includes ( '`extra-guard`' ) ) ;
610681
682+ // ── row 4b: a re-sync speaks ONLY for the file it read (objectui#8288) ─────
683+ // The defect this row exists for: the pin carried ONE global `upstream.ref`
684+ // beside per-file digests, and `--resync` set that global field on every run.
685+ // Re-syncing one file therefore re-labelled the others with a ref their
686+ // digests had never been taken from -- and both stayed GREEN, because the
687+ // digest is the assertion and their bytes were untouched. Only the provenance
688+ // line was false, which is the failure direction this tree treats as worst.
689+ // Driven through `resyncedPin`, the transform `resync()` itself applies, so a
690+ // green row means the write path has the property rather than that a
691+ // restatement of it does.
692+ const OTHER = {
693+ ported : 'scripts/y.mjs' ,
694+ upstreamPath : 'scripts/y.mjs' ,
695+ ref : 'b' . repeat ( 40 ) ,
696+ upstreamSha256 : digest ( 'const other = 1;\n' ) ,
697+ divergences : [ DIVS [ 0 ] ] ,
698+ } ;
699+ const LEDGER = { upstream : { repo : 'o/r' } , files : [ ENTRY , OTHER ] } ;
700+ const NEW_REF = 'c' . repeat ( 40 ) ;
701+ const after = resyncedPin ( LEDGER , ENTRY . ported , NEW_REF , digest ( UP2 ) ) ;
702+ t ( "--resync writes the re-synced entry's own ref" , after . files [ 0 ] . ref === NEW_REF ) ;
703+ t ( "…and that entry's digest" , after . files [ 0 ] . upstreamSha256 === digest ( UP2 ) ) ;
704+ // The card's case, stated as the byte-identity it has to be: not "the other
705+ // entry looks right" but "the other entry was not written".
706+ t (
707+ '…and leaves every OTHER entry byte-identical — ref AND digest (objectui#8288)' ,
708+ JSON . stringify ( after . files [ 1 ] ) === JSON . stringify ( OTHER ) ,
709+ ) ;
710+ t (
711+ '…specifically: the untouched entry keeps ITS ref, not the re-synced one' ,
712+ after . files [ 1 ] . ref === 'b' . repeat ( 40 ) && after . files [ 1 ] . ref !== NEW_REF ,
713+ ) ;
714+ t (
715+ '…and there is no global ref left for a re-sync to write through' ,
716+ ! ( 'ref' in after . upstream ) ,
717+ ) ;
718+ t ( '…and the input pin is not mutated' , LEDGER . files [ 0 ] . ref === 'a' . repeat ( 40 ) ) ;
719+ // The control leg. Without it every row above is satisfied by a transform
720+ // that does nothing at all — the shape this whole file exists to refuse.
721+ t ( '…while the re-synced entry itself DID change' , JSON . stringify ( after . files [ 0 ] ) !== JSON . stringify ( ENTRY ) ) ;
722+
611723 // ── row 5: --resync REFUSES to rewrite governed surface ────────────────────
612724 // The write path only. Every row drives `resyncWriteVerdict`, which is the
613725 // decision `resync()` makes, so a green row means the refusal is reachable
@@ -644,13 +756,19 @@ function selfTest() {
644756 t ( '…and drift in one still REDS, with no flag involved' , ! verifyFile ( govEntry , driftedOutside ) . ok ) ;
645757
646758 // ── row 6: a malformed pin is REFUSED, never read as clean ─────────────────
647- const good = { upstream : { repo : 'o/r' , ref : 'a' . repeat ( 40 ) } , files : [ ENTRY ] } ;
759+ const good = { upstream : { repo : 'o/r' } , files : [ ENTRY ] } ;
648760 t ( 'the fixture pin is well-formed' , validatePin ( good ) . length === 0 ) ;
649761 const broken = [
650762 [ 'a non-object pin' , 'nope' ] ,
651763 [ 'no files at all' , { ...good , files : [ ] } ] ,
652- [ 'a branch name where a commit sha belongs' , { ...good , upstream : { repo : 'o/r' , ref : 'main' } , } ] ,
653- [ 'a short ref' , { ...good , upstream : { repo : 'o/r' , ref : 'abc1234' } } ] ,
764+ [ 'a branch name where a commit sha belongs' , { ...good , files : [ { ...ENTRY , ref : 'main' } ] } ] ,
765+ [ 'a short ref' , { ...good , files : [ { ...ENTRY , ref : 'abc1234' } ] } ] ,
766+ // objectui#8288: both directions of the retirement. An entry with no ref is
767+ // a digest with no provenance; a pin still carrying the global field is the
768+ // old spelling, and it is REFUSED rather than ignored — a value that is no
769+ // longer read but still reads as authoritative is how this bug survived.
770+ [ 'an entry with no ref of its own' , { ...good , files : [ { ...ENTRY , ref : undefined } ] } ] ,
771+ [ 'a RETIRED global upstream.ref' , { ...good , upstream : { repo : 'o/r' , ref : 'a' . repeat ( 40 ) } } ] ,
654772 [ 'a repo that is not owner/name' , { ...good , upstream : { repo : 'objectstack' , ref : 'a' . repeat ( 40 ) } } ] ,
655773 [ 'a digest that is not SHA-256' , { ...good , files : [ { ...ENTRY , upstreamSha256 : 'deadbeef' } ] } ] ,
656774 [ 'an absolute ported path' , { ...good , files : [ { ...ENTRY , ported : '/etc/passwd' } ] } ] ,
@@ -695,7 +813,8 @@ function selfTest() {
695813 `✓ check-upstream-port-parity self-test: ${ cases . length } cases pass — parity holds on an undrifted copy, ` +
696814 'drift outside the declared regions reds as a digest mismatch, drift inside one names its divergence, ' +
697815 'an ambiguous anchor is refused rather than applied, the pin-bump procedure round-trips (and a vanished ' +
698- 'anchor fails it loudly), `--resync` refuses to rewrite governed surface unless the named flag is passed ' +
816+ 'anchor fails it loudly), a re-sync writes ONLY the re-synced entry\'s ref and digest and leaves every ' +
817+ 'other entry byte-identical, `--resync` refuses to rewrite governed surface unless the named flag is passed ' +
699818 'while the CHECK path stays ungated, and every malformed-pin shape is refused instead of read as clean.' ,
700819 ) ;
701820 return 0 ;
0 commit comments