Repository navigation
feat: add network history and streamline profile audits - #27
Conversation
GitHub exposes no follow timestamp and documents no ordering for the follower and following endpoints, so the Network lists have had no chronology to sort by. This records one GitProfileLens can defend: when each relationship was first observed in this browser. network-history.js stores, per audited profile and per relationship list, firstObservedAt / lastObservedAt / currentlyPresent. Nothing is named for a follow event, and a test scans the shipped source to keep it that way. The design decision that matters is cohorts. An observation establishes a set, not a sequence: seeing 131 followers at once proves all 131 existed by that moment and proves nothing about their order. Everything first seen in one observation therefore shares one firstObservedAt, so loop order can never masquerade as chronology. Completeness is respected per list. An incomplete retrieval is not merged at all, so it can never mark the accounts it did not reach as gone. Storage is localStorage: one versioned JSON value, one read and at most one write per observation, bounded profile count, unknown schema versions discarded rather than guessed at, blocked or full storage degrading to no history rather than to no Network.
The optimizer ranks every Strong candidate ahead of every Worth polishing repository before presentation score is consulted at all, and candidacy settles 81.9% of pairwise comparisons. Breadth carries a score band for this exact shape of problem, so the partition was measured against the same question. evaluation/candidacy-diagnose.js reports the gates, which gate held each repository back, every Strong/Worth score inversion, how much displacement the partition causes, threshold sensitivity, a policy comparison, and ten constructed cases the corpus does not contain. It first asserts its own gate table reproduces the production classifier, so it cannot describe a classifier the product does not have. The finding is that the partition is unchanged, for reasons the corpus supports rather than for want of looking: - Moving candidacy below score, and bounding it at 2, 5 and 10 points, produces identical recommended sets for all 13 corpus profiles, and for a live 33-repository profile with 12 Strong candidates. - Seven of the nine gates read evidence the score already reads, so a repository that fails one has already paid for it in score. The Strong floor is 87 and the Worth ceiling is 90: the widest inversion presentation evidence can produce is 3 points. Breadth was unbounded because language is orthogonal to score; candidacy is correlated with it, and that correlation is the ceiling. - The two gates the score does not read, fork and archive status, already have dedicated ranking stages immediately after candidacy, so for those the partition's ordering contributes nothing. Every gate is still a cliff, sharpest at maintenance where one day either side of 730 flips the label for three points. That is recorded rather than patched, because the coupling above caps what the flip can do. tests/scoring/candidacy-policy.test.js asserts these measurements through the diagnostic's own functions, so if the partition ever starts moving a recommendation the question reopens with the failing case in hand.
evaluation/performance-diagnose.js drives the real page in a real browser
against mocked GitHub responses and reports counts rather than durations:
requests issued and repeated, elements created per interaction, derived
computations repeated, DOM nodes per panel, storage writes, and what a
second audit leaves behind. Counts are deterministic; the wall-clock
figures it prints are labelled informational and are not assertions.
Two things it found were real and are fixed:
- GET /users/{login} was requested twice, once for the audit and again
inside fetchNetwork. The audit now hands its profile over. Opening
Network on a live-shaped profile drops from 4 GitHub requests to 3 and
repeated URLs from 2 to 0, which matters against an unauthenticated
allowance of sixty an hour. An explicit retry still re-reads the
profile, because retry means look again.
- GitHubAudit.scoreProfile ran twice per render, from the header and from
the overview, over every repository. Computed once and shared: 2 to 1.
Three things it found are deliberately left alone, with the measurement
recorded in docs/scoring.md so the question is not reopened from scratch:
- Deferring the audit panel, 5,797 of 7,415 nodes at 150 repositories,
was measured rather than assumed. Removing the entire audit render
produced no measurable change in time to a usable audit; run-to-run
noise is larger than the effect. Element creation is not the bottleneck.
- The optimizer and Markdown together account for 121 of 7,415 nodes and
already run once each. There is nothing worth the invalidation logic.
- The README GraphQL query's three filename aliases cost nothing: on a
real 33-repository profile the other two resolved for zero
repositories, and the browser receives only the 6.5 KB analysis.
Relationship pagination still issues one request past the last full page.
A short page is the only evidence a list has ended, and the reported count
can change mid-retrieval, so completeness is not traded for a request.
Every control in the reset group disappears when it is used: the reset button when the confirmation opens, the confirmation when it is answered, and the whole group when history is deleted. Each of those left focus on a hidden element, which drops a keyboard reader back to the document body. Focus now follows the control that replaced the one that vanished, and after a deletion lands on the sentence that says what happened, which is a status region so it is announced rather than only shown.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
The previous interval concluded that the Strong / Worth partition needed
no band, and rested that on a claimed bound: a Strong floor of 87, a Worth
ceiling of 90, and a maximum inversion of 3 points. That was not a bound.
It was one hand-built pair from one generator, reported as though it
described the whole classifier, and the live profile it was meant to
explain contradicted it outright by carrying Worth polishing repositories
at 93, 93 and 94.
measureReachableBounds now answers the question by search rather than by
example, enumerating 120,960 repository shapes per population. The real
figures:
lowest score a Strong can reach 79
highest a Worth can reach while failing
only score-visible gates 95 (fails readme)
maximum inversion so caused 16
Strong shapes below that ceiling 5,764 of 6,912
So the unbanded rule let a repository presenting at 79 precede one
presenting at 95 because the latter's README carried one core section
fewer than the gate wants, and that covered most of the Strong space
rather than a corner. Sixteen points on a 0-100 scale is not a cap worth
resting an argument on, and the evidence behind it had already been
counted once in the score. This is the same defect breadth was given a
band for in PR #26.
Rule 1 now abstains outside five points, matching the band rule 4 already
uses so the optimizer has one notion of comparable presentation. Fork and
archive status are the two gates the score does not read, and they are
untouched: rules 2 and 3 apply them directly, and the pairwise tally shows
archive picking up the 7 comparisons it should always have settled.
No recommendation moved. The banded rule, the unbanded rule, removing
candidacy from the ordering entirely, and bands of 2, 5 and 10 all produce
identical sets on all 13 corpus profiles and on the live 33-repository
profile, whose recommended set still contains none of the three Worth
polishing repositories that prompted the question. Eligibility is
untouched; this is an ordering bound, not a way to move a set.
What did change is which rule can be pointed at: candidacy settled 81.9%
of pairwise comparisons and now settles 2.3%, while presentation score
rises from 12.3% to 90.4%.
Two problems, one measured and one found while measuring it. The documentation claimed the bounded profile count kept the snapshot "well inside a typical 5 MB origin quota". That was asserted, not measured, and it is wrong. Serializing the worst case gives: one live-sized profile (147 + 396) 543 relationships ~84 KB one profile at the pagination caps 20,000 relationships ~3.1 MB twenty profiles at the caps 400,000 relationships ~59 MB About 154 bytes per relationship, and browsers commonly charge localStorage per UTF-16 code unit, so roughly double against a quota. A single profile at the caps exceeds a 5 MB budget on its own. There is no portable quota to design against, so the documentation now states the measured figures and stops claiming the worst case fits. Ordinary use is far inside any plausible quota: twenty live-sized profiles come to about 1.7 MB. Verifying the failure path then turned up a real bug. write() adopted the new snapshot into the in-memory cache *before* attempting setItem, so a refused write left the session ordering by a cohort that existed nowhere on disk, and silently reverting on the next reload. A feature whose whole purpose is to avoid claiming more than the evidence supports cannot also display an order its own storage rejected. The snapshot is now adopted only once the write succeeds, and a refused write reports the history that is actually stored. A refused write is also retried with only the profile being observed, since a single large network can exceed the quota alone and the reader is looking at one profile; if even that is refused nothing is written and prior history stands. The Network tab says which of these happened rather than letting the observation count quietly stop advancing. A browser test drives a real QuotaExceededError through the real Storage interface and asserts the retrieval, the derived list and the export are all unaffected, nothing is stored, and the lists fall back to GitHub's order with the interface saying so.
Two claims in the summary were wrong. Both are corrected, and one hid a bug.1. "Strong floor 87, Worth ceiling 90, max inversion 3" was not a boundIt was one hand-built pair from one generator, reported as though it described the classifier. The live profile contradicted it outright — it carries
So the unbanded rule let a repository presenting at 79 precede one presenting at 95 because the latter's README carried one core section fewer — across most of the The no-change conclusion did depend on the false bound, so I revisited it. 16 points on a 0–100 scale is not a cap worth resting an argument on, and the evidence behind it had already been counted once in the score. This is the same defect breadth was given a band for in PR #26. Rule 1 now abstains outside five points, matching the band rule 4 already uses. Fork and archive status are the two gates the score does not read, and they are untouched — rules 2 and 3 apply them directly. The pairwise tally confirms it: archive now settles the 7 comparisons it should always have settled. No recommendation moved. Banded, unbanded, candidacy removed from ordering entirely, and bands of 2/5/10 all produce identical sets on all 13 corpus profiles and on the live 33-repository profile — whose recommended set still contains none of the three What did change is which rule can be pointed at: candidacy settled 81.9% of pairwise comparisons, now 2.3%; presentation score rises from 12.3% to 90.4%. 2. The storage-size claim was asserted, not measured — and wrongSerializing the worst case:
~154 bytes per relationship, and browsers commonly charge Verifying the failure path found a real bug. A feature whose purpose is to avoid claiming more than the evidence supports cannot display an order its own storage rejected. Now: the snapshot is adopted only once the write succeeds; a refused write reports what is actually stored; the retry keeps the profile in view when the full snapshot will not fit; prior history survives intact (a failed A browser test drives a real Verification
|
Three workstreams, each measured before it was changed.
Network chronology
GitHub exposes no follow timestamp and documents no ordering for the follower and following endpoints. The previous audit (PR #26) established that and left the lists in the API's order. This adds the only chronology GitProfileLens can honestly offer: when it first observed each relationship, in this browser.
network-history.jsstoresfirstObservedAt,lastObservedAtandcurrentlyPresentper relationship. No field is named for a follow event, and a test scans the shipped source to keep it that way.The design decision that carries the feature is cohorts. An observation establishes a set, not a sequence: seeing 147 followers at once proves all 147 existed by that moment and proves nothing about their order. Everything first seen in one observation therefore shares one
firstObservedAt, so loop iteration order can never masquerade as chronology. The first observation is one baseline cohort with no internal order; each later observation adds at most one new cohort.firstObservedAtis never rewritten.localStorage, versioned, one read and at most one write per observation, bounded profile count, unknown schema versions discarded rather than guessed at, blocked or full storage degrading to no history rather than to no Network.29 node tests and 5 browser tests cover the semantics.
docs/network-ordering.mdspecifies it, including what it still cannot do.Portfolio candidacy
The optimizer ranks every
Strong candidateahead of everyWorth polishingrepository before presentation score is consulted. Breadth carries a score band for exactly this shape of problem, so the partition was measured against the same question.The partition is unchanged, for reasons the evidence supports rather than for want of looking:
Strongcandidates.Strongfloor is 87 and theWorthceiling is 90: the widest inversion presentation evidence can produce is 3 points. Breadth was unbounded because language is orthogonal to score; candidacy is correlated with it, and that correlation is the ceiling.Every gate is still a cliff, sharpest at maintenance where one day either side of 730 flips the label for three points of score. That is recorded rather than patched, because the coupling above caps what the flip can do.
npm run eval:candidacyreproduces all of it;tests/scoring/candidacy-policy.test.jsasserts the findings through the diagnostic's own functions, so if the partition ever starts moving a recommendation the question reopens with the failing case in hand.Performance
npm run eval:performancedrives the real page in a real browser and reports counts, not durations.Fixed:
GET /users/{login}fetched twice, once for the audit and again infetchNetworkscoreProfilecomputed twice per renderMeasured and deliberately not changed, with the evidence recorded in
docs/scoring.md:Verification
npm run check,npm test(336 pass),npm run eval,npm run eval:pins(no corpus outcome or recommendation changed),npm run eval:candidacy,npm run eval:performance,npm run test:browser,git diff --check, and a realnpm startagainst the live profile: 147 followers, 396 following, 303 not following back, baseline cohort formed, profile fetched once, export and interface in agreement, no page errors.