Skip to content

Commit fadf6cd

Browse files
claude[bot]claude
andauthored
fix(devx): give the upstream port pin a per-file ref, retiring the global one (#8386)
The pin carried ONE global `upstream.ref` beside PER-FILE digests, and `--resync` set that global field on every run while updating only the re-synced entry's digest. Re-syncing one file therefore re-labelled the others with a ref their digests had never been taken from. Measured on the tree: after #7749 re-synced the hook self-test to objectstack `70e77ec3b`, the gate printed that ref beside all three files. The other two carry digests taken at `bf10debd5`, confirmed by hashing the upstream blobs at both refs: scripts/pm/check-half-states.mjs bf10debd5 449a0aec… 70e77ec3b 274bac47… scripts/invoked-as.mjs bf10debd5 90f72bf4… 70e77ec3b 6d99f65c… The pinned digests are the `bf10debd5` ones. All three stayed GREEN — the digest is the assertion and the bytes were untouched — while the provenance line beside two of them was false. `ref` is now a required field on each `files[]` entry and the global one is RETIRED rather than kept as a default: a default would be read as `entry.ref ?? pin.upstream.ref`, which is the same false label re-spelled as a feature. `validatePin` refuses a pin that still carries `upstream.ref`. `upstream.repo` stays global — it is measurably uniform. The two entries' refs are corrected to `bf10debd5`. That is a correction of a false label, NOT a re-sync: no digest, no divergence and no ported file's bytes change (the diff on the pin is one removed global ref and three added per-file refs, nothing else). Claude-Session: https://claude.ai/code/session_01FhBNJcLRZLe8M87VcUgpKr Co-authored-by: Claude <noreply@anthropic.com>
1 parent 5b0df18 commit fadf6cd

3 files changed

Lines changed: 194 additions & 20 deletions

File tree

‎scripts/__tests__/upstream-port-parity-wiring.test.ts‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,59 @@ describe('check-upstream-port-parity is wired, not merely present', () => {
162162
}
163163
});
164164

165+
it('the ref is per ENTRY, and the retired global one is gone (objectui#8288)', () => {
166+
// The schema half of the card. The pin used to carry ONE `upstream.ref`
167+
// beside per-file digests, and `--resync` set it on every run — so
168+
// re-syncing one file re-labelled the others with a ref their digests had
169+
// never been taken from (objectui#7749 did exactly that to
170+
// `check-half-states.mjs` and `invoked-as.mjs`). Both stayed GREEN, because
171+
// the digest is the assertion; only the provenance line was false.
172+
//
173+
// Asserted structurally, never by value: the refs themselves move on every
174+
// re-sync, and a copy of them here would be the second thing to keep honest
175+
// that this file's header already refuses.
176+
const pin = JSON.parse(fs.readFileSync(path.join(ROOT, PIN), 'utf8'));
177+
expect(pin.upstream.ref, 'the global ref is retired, not merely unread').toBeUndefined();
178+
expect(pin.files.length).toBeGreaterThan(0);
179+
for (const f of pin.files) {
180+
expect({ ported: f.ported, ref: f.ref }).toEqual({
181+
ported: f.ported,
182+
ref: expect.stringMatching(/^[0-9a-f]{40}$/),
183+
});
184+
}
185+
});
186+
187+
it('the gate prints each file at ITS OWN ref, not one ref for all', () => {
188+
// The reporting half. This is the assertion that would have caught the card:
189+
// every printed provenance line must name the ref stored beside that file's
190+
// own digest. Relational — it reads the expected ref out of the pin — so it
191+
// survives any re-sync and fails the moment one file's line borrows
192+
// another's ref.
193+
const pin = JSON.parse(fs.readFileSync(path.join(ROOT, PIN), 'utf8')) as {
194+
files: Array<{ ported: string; ref: string }>;
195+
};
196+
const out = stripAnsi(execFileSync('node', [GATE], { cwd: ROOT, encoding: 'utf8' }));
197+
for (const f of pin.files) {
198+
const line = out.split('\n').find((l) => l.includes(f.ported));
199+
expect(line, `no verdict line for ${f.ported}`).toBeTruthy();
200+
expect({ ported: f.ported, namesOwnRef: line!.includes(f.ref.slice(0, 9)) }).toEqual({
201+
ported: f.ported,
202+
namesOwnRef: true,
203+
});
204+
}
205+
// The control leg: with more than one distinct ref pinned, a line must NOT
206+
// name a ref belonging to a different entry. Without this the loop above is
207+
// satisfied by a gate that prints every ref on every line.
208+
const distinct = [...new Set(pin.files.map((f) => f.ref))];
209+
if (distinct.length > 1) {
210+
for (const f of pin.files) {
211+
const line = out.split('\n').find((l) => l.includes(f.ported))!;
212+
const foreign = distinct.filter((r) => r !== f.ref && line.includes(r.slice(0, 9)));
213+
expect({ ported: f.ported, foreignRefs: foreign }).toEqual({ ported: f.ported, foreignRefs: [] });
214+
}
215+
}
216+
});
217+
165218
it('its self-test passes — the half that makes a green comparison mean something', () => {
166219
const out = execFileSync('node', [GATE, '--self-test'], { cwd: ROOT, encoding: 'utf8' });
167220
// objectui#7897 — the COUNT, not the shape. `\d+ cases pass` is satisfied

‎scripts/check-upstream-port-parity.mjs‎

Lines changed: 137 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,28 @@
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
@@ -82,7 +104,8 @@
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+
373433
function 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

431498
function 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;

‎scripts/upstream-port-pin.json‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"upstream": {
3-
"repo": "objectstack-ai/objectstack",
4-
"ref": "70e77ec3b566b92d7c6551637d6bc746fb840bb6"
3+
"repo": "objectstack-ai/objectstack"
54
},
65
"files": [
76
{
87
"ported": "scripts/pm/check-half-states.mjs",
98
"upstreamPath": "scripts/pm/check-half-states.mjs",
9+
"ref": "bf10debd587f6ba891be9eadc2b76c91e15bd82b",
1010
"upstreamSha256": "449a0aec0cfa36738e0fc5651ec978faf846316522daeb9ca6efa1714daf6ac9",
1111
"divergences": [
1212
{
@@ -80,6 +80,7 @@
8080
{
8181
"ported": "scripts/invoked-as.mjs",
8282
"upstreamPath": "scripts/invoked-as.mjs",
83+
"ref": "bf10debd587f6ba891be9eadc2b76c91e15bd82b",
8384
"upstreamSha256": "90f72bf45a2fd158b19d5774269eb96a6b8b61540251ce68c29ddce7c93bfaa7",
8485
"divergences": [
8586
{
@@ -141,6 +142,7 @@
141142
{
142143
"ported": ".claude/hooks/guard-main-checkout.selftest.sh",
143144
"upstreamPath": ".claude/hooks/guard-main-checkout.selftest.sh",
145+
"ref": "70e77ec3b566b92d7c6551637d6bc746fb840bb6",
144146
"upstreamSha256": "1d8d4458c90beb9f673556524e34262a8a0593e1a4d2de20a352f6b5a77c6721",
145147
"divergences": [
146148
{

0 commit comments

Comments
 (0)