Skip to content

Add a Settings About page, with attribution in a new CreditKit module#140

Merged
kyleve merged 16 commits into
mainfrom
settings-about-page
Jul 27, 2026
Merged

Add a Settings About page, with attribution in a new CreditKit module#140
kyleve merged 16 commits into
mainfrom
settings-about-page

Conversation

@kyleve

@kyleve kyleve commented Jul 26, 2026

Copy link
Copy Markdown
Owner

Summary

Adds an About block at the bottom of Settings covering the things the app couldn't previously answer: which build is running, what third-party work it's built with, and where its bundled region boundaries came from.

The app had no build metadata at all — version and build number were Tuist's .extendingDefault values and nothing recorded the commit — so this also adds the plumbing to stamp it.

Credits are vended by the module that owns the thing being credited, and the screen only renders them:

  • WhereCore.BuildInfo — the version keys plus the commit stamped into the built Info.plist.
  • CreditKit.SoftwareCredit — third-party software and its license notice, in a new Shared module (see below).
  • RegionKit.RegionDataSource — the origin, license, and fidelity of the bundled GeoJSON.

Attribution: CreditKit reports, the app ships the report

Attribution started out in WhereCore, which was wrong: WhereCore is just the only module that happens to link an external package today, and the edge pointed the wrong way — Broadway, Periscope and friends are documented as portable and app-agnostic, so none of them could have vended a credit without depending on app code.

The first pass moved it to a Shared module, but that module then was the Where app's credits wearing a library's clothes: a credits.json and five vendored notices in its own resource bundle, a generator hardcoding this repo's paths, and tests asserting ZIPFoundation was linked. A second app could not have used any of it.

Shared/CreditKit now owns the format and the tooling and nothing else. An app declares its own inputs, ./attribution runs the report, and the result lands in that app's resources — for Where, Where/Where/Resources/attribution.json:

{
  "output": "Where/Where/Resources/attribution.json",
  "sources": [
    { "type": "swiftPackageManager", "kind": "library",
      "manifest": "Package.swift", "resolved": "Package.resolved" },
    { "type": "agentSkills", "kind": "developmentTool",
      "manifest": ".agents/external-skills.json" }
  ]
}

Dropping the resource bundle dropped the last dependency too: CreditKit is Foundation-only and throws rather than logs, leaving the caller to decide what a missing report means. Notices are inlined into the report, so one decode yields everything needed and the missing-file path stops existing.

The report is still derived, which is what stops it going stale: credits come from packages a target actually links via .product(name:package:), pinned by Package.resolved, with each notice read at the pinned revision. A new dependency is credited wherever it lands, and a package resolved only for tooling (BumperBowling, swift-syntax) correctly isn't. The generated notices came out byte-identical to the ones previously vendored by hand, and re-running produces no diff.

Kind is derived too, not declared. Linking is not shipping: SnapshotKitTesting links the snapshot-testing engine and the accessibility parser, and crediting those as libraries would tell a reader their binary contains a test harness. The config's shippedFrom names the package targets the app and its extensions link (WhereUI, WhereIntents); the generator walks the manifest's target graph from there, so a package inside that closure is a library and any other linked package is a developmentTool — same bucket as the vendored agent skills, credited because the repo depends on them rather than because they reach a device. shippedFrom is the only value set by hand.

WhereCore.AppAttribution reads the report from Bundle.main beside BuildInfo, and splits the two failures they share: a bundle with no report is legitimate (only the app target ships one), while a report that won't decode is a corrupt resource and faults. AppAttribution.main caches the decode for the process — About takes it as a default argument, and Swift re-evaluates those on every init, so reading eagerly re-read and re-decoded the file on every SwiftUI body pass; current(bundle:) remains for tests and other bundles.

About says something in every empty case, keyed on the section rather than on the report: "no report at all" and "nothing of this kind" describe different builds, and a header and footer over no rows describes neither. The generator also refuses to write a report with duplicate credit names (SoftwareCredit is Identifiable by name, and a library's name is its repo basename, so two orgs can collide) or an unrecognized kind, which would otherwise generate and commit cleanly and then fail to decode inside the app.

The build stamp

A post-build script phase on the Where target writes WhereGitSHA / WhereGitStatus into the built product's plist. Post (not pre) so it edits the plist after "Process Info.plist" writes it and before signing seals the bundle, and basedOnDependencyAnalysis: false so an unchanged source tree can't ship the previous commit's SHA. A checkout without git metadata stamps unknown rather than failing the build, and unstamped bundles (RegionViewer, StuffTestHost, the extensions) report no commit rather than a placeholder.

Content call worth a look

Canada and the EU have no external source and no license — RegionKit/README.md calls them coarse placeholders unfit for a real residency audit. The page says that plainly ("Approximate outline drawn for this app — coarse, and not authoritative") rather than omitting them. Say if you'd rather word it differently or leave them off.

Test plan

  • ./swiftformat --lint, ./xcstrings --lint
  • swift run bumper config ., swift run bumper test ., swift run bumper lint . — no violations
  • tuist test CreditKitTests, WhereCoreTests, WhereTests, RegionKitTests, WhereUITests — all green, including the new AttributionManifestTests, AppAttributionTests, BuildInfoTests, RegionDataSourceTests, AboutSettingsViewTests, LicenseViewTests
  • ./attribution is idempotent — a second run rewrites the same bytes
  • Built and installed the app: the stamp lands in Where.app/Info.plist (WhereGitSHA 778d7e8523d1, WhereGitStatus dirty) and re-runs on every build
  • Rendered both screens in the simulator
  • Full Stuff-iOS-Tests scheme — 251 suites green after merging main (the earlier run's four StuffTestHost bundle kills were simulator contention on this machine; Give every checkout a simulator of its own #141's per-checkout simulator cleared it)
  • tuist test WhereUISnapshotTests — 31 suites green on a clean re-run after recording

Snapshots

main's snapshot bundle (#101) landed after this branch wrote the About screen, so merging the two invalidated 9 of the 11 SettingsViewSnapshotTests references — the Settings list grew an About row. Those are re-recorded, and AboutSettingsView / LicenseView now have the SnapshotProviding conformance and suite the module convention asks for. The About matrix is built around absence, since the shipping build is the only one both stamped and attributed: a full report, a dirty tree, no report at all, and a report crediting only one kind. That last case pins the empty-section fix as an image, which suits a failure that was purely visual.

Recording caught a stale string: LicenseView's empty-notice case still said "This license text couldn't be loaded from the app bundle", describing the old architecture where notices were separate bundle resources. Nothing is loaded now — the notice is inlined — so it reads "This credit's report entry carries no license text".

Drift guards

  • RegionDataSourceTests fails if a catalog region is covered by zero or two sources, so a regenerated catalog can't ship uncredited geometry. Coverage derives from the us- prefix the generator mints plus an explicit id list — deliberately not an "everything else" bucket that would quietly mis-credit a new region.
  • ./attribution --check re-derives the expected report from Package.swift, Package.resolved, and the skills manifest and diffs it against the committed one, gated in CI. It makes no network calls — notices are read at the pinned revision, so a matching revision means matching text — and runs in under a second. This replaced a guard that didn't work. AppAttributionTests had compared the report to a hardcoded list of names, which a stale report satisfies exactly as well as a fresh one; it passed on this branch while the committed report was missing two packages the main merge had added. A test bundle can't read Package.swift, so it can't do this job at all. It now asserts only what the shipping bundle can answer: present, decodable, notices inline, unique names, both kinds populated.
  • AboutSettingsViewTests.separatesLinkedLibrariesFromDevelopmentTools fails if either section would render empty or lose its heading.

Follow-up filed

Root TODOs.md: a LogSession names only a version and build number, both pinned in the manifest, so every developer build logs as v1.0 (1). PeriscopeCore sits below WhereCore and can't read BuildInfo, so carrying the commit into log sessions needs a seam rather than a direct adoption.

Made with Cursor

kyleve and others added 6 commits July 26, 2026 13:47
Settings > About needs to answer "which build am I running?", and the app
carried no build metadata at all: version and build number were Tuist's
`.extendingDefault` values and nothing recorded the commit.

State the version keys explicitly in the manifest so a user-visible number is
one we chose, and add a post-build script phase that writes `WhereGitSHA` /
`WhereGitStatus` into the built product's plist. Post (not pre) so it edits the
plist after "Process Info.plist" writes it and before signing seals the bundle,
and always-out-of-date so an unchanged source tree can't ship the previous
commit's SHA. A checkout without git metadata stamps `unknown` rather than
failing the build.

Groundwork: nothing reads these keys yet.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds WhereCore's BuildInfo, which reads the Xcode version keys and the commit
keys the stamp script writes. Unstamped bundles — RegionViewer, StuffTestHost,
the extensions — and a stamp git couldn't fill in both report no commit rather
than a placeholder, so a caller can't render an invented build identity.

Groundwork: no UI reads it yet.
Co-authored-by: Cursor <cursoragent@cursor.com>
Provenance for the 54 bundled GeoJSON files lived only in prose, so the app had
no way to credit its data and nothing failed when a regenerated catalog added a
region nobody had traced. RegionDataSource makes it a value RegionKit owns:
origin, links, license, and fidelity, so a consumer can distinguish boundaries
simplified from the Census set from the coarse hand-drawn Canada/EU outlines
without knowing which is which.

Coverage derives from the catalog — the US sources by the `us-` prefix the
generator mints, the rest by an explicit id list rather than an "everything
else" bucket, so a new region from a real source can't be quietly credited as
hand-drawn. The test fails when a region is covered zero times or twice.

Groundwork: no UI reads it yet.
Co-authored-by: Cursor <cursoragent@cursor.com>
The app links ZIPFoundation for backup archives but reproduced its MIT notice
nowhere, which the license requires. SoftwareCredit lists the third-party
libraries WhereCore pulls in and vends the notice from a verbatim copy bundled
under Resources/Licenses.

A credit whose license file is missing fault-logs and asserts rather than
returning empty text: shipping a dependency without its notice is a licensing
defect, so the caller has to be able to tell "unavailable" from "no terms".

Groundwork: no UI reads it yet.
Co-authored-by: Cursor <cursoragent@cursor.com>
Adds an About block below Storage covering the three things the app couldn't
answer before: which build is running, which open-source libraries it links,
and where its bundled region boundaries came from.

The screen only renders — every fact is vended by the module that owns it, so
adding a dependency or a dataset means updating that module's credit rather
than editing this view. A library's full notice pushes on as its own screen,
since a license belongs in full text rather than a truncated row.

Each data source's details stack under its name instead of trailing it: a
boundary set's name wraps, and a count that sometimes drops to its own line
reads as a different layout per row. The source links name their host so a tap
target says where it goes.

Co-authored-by: Cursor <cursoragent@cursor.com>
Records the rule the previous commits establish: a credit belongs to the module
that owns the thing being credited, and the About screen only renders. Without
it the natural next move — hard-coding a new library into the view — looks fine.

Files the follow-up the build stamp makes possible: a LogSession names only a
version and build number, both pinned in the manifest, so every developer build
logs as v1.0 (1). Periscope sits below WhereCore and can't read BuildInfo, so
carrying the commit needs a seam rather than a direct adoption.

Co-authored-by: Cursor <cursoragent@cursor.com>
Comment thread Where/WhereCore/README.md Outdated
a selectable look-back `RecentActivityWindow`.
- **`WherePreferences`** — persisted user intent (onboarding, tracking intent,
reminder / summary schedules) behind a `KeyValueStore`.
- **`BuildInfo`** + **`SoftwareCredit`** — what Settings > About shows about the

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is placing this in WhereCore correct? Attributions aren't unique to WhereCore, they can come in anywhere, right?

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on $USER's behalf.

You're right, and the doc comment was overstating its case — it said "WhereCore owns this because WhereCore is what pulls the dependencies in," which reads like a principle but was really a coincidence. WhereCore just happens to be the only module linking an external runtime package today (ZIPFoundation; BumperBowling and swift-syntax are dev/CI-only and never reach the binary).

The concrete problem: the dependency edge pointed the wrong way. Broadway, Periscope, LifecycleKit and friends are documented as portable and app-agnostic, so if one of them picked up a package it could not vend a credit without depending on app code.

Fixed in f6fe23b by giving attribution its own Shared leaf module, CreditKit, that anything may depend on.

The other half of your question — "they can come in anywhere" — turned out to be the more interesting one, because placement alone doesn't solve it. Wherever the type lives, a hand-kept list silently goes stale when a different module adds a package. So the manifest is now generated instead: Shared/CreditKit/Tools/generate-credits.rb derives the credits from packages a target actually links via .product(name:package:), pinned by Package.resolved, and fetches each notice at the pinned revision. A new dependency gets credited wherever it lands, and a package resolved only for tooling correctly doesn't.

Worth noting as validation: the generated ZIPFoundation notice came out byte-identical to the one I'd vendored by hand from the pinned checkout.

BuildInfo stays in WhereCore — that's build identity, not attribution.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on $USER's behalf.

Follow-up in 149c17c, after your point about the shape. You were right that the first pass didn't finish the job: I'd moved attribution into a Shared module, but that module was the Where app's credits wearing a library's clothes — a credits.json and five vendored notices in its own Bundle.module, a generator hardcoding this repo's paths, and tests asserting ZIPFoundation was linked. A library making claims about its consumer's dependency graph. A second app couldn't have used any of it.

CreditKit now owns the format and the tooling and nothing else:

  • An app declares its inputs in an attribution-sources.json; ./attribution runs the report; the result lands in that app's resources (Where/Where/Resources/attribution.json). A report describes one app's dependency graph, so it's the app's data.
  • Notices are inlined in the report rather than sidecar files, so one decode yields everything and the missing-file failure path stops existing.
  • Dropping the resource bundle dropped the last dependency: CreditKit is Foundation-only now, and throws instead of logging, because only the caller knows whether a missing report is a problem.
  • WhereCore.AppAttribution makes that call for Where, splitting the two cases the way BuildInfo splits stamped from unstamped: no report is legitimate (only the app target ships one), a report that won't decode is a corrupt resource and faults.
  • The app-specific assertions moved to AppAttributionTests in the Where app's own test bundle — the one hosted by Where.app, so Bundle.main is the shipping bundle.

Two checks worth reporting: the generated notices are byte-identical to the ones previously vendored by hand, and re-running the report produces no diff.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking out loud, we should also integrate with the sync-agents tool to credit the external skills we've added.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on $USER's behalf.

Done in f6fe23b — and there was a real gap here, not just a nice-to-have. All four external skills are MIT (all from twostraws), and sync-agents clones the repo then copies only the skill subdirectory, so the upstream root LICENSE gets dropped on the floor. We were making copies on every dev machine and cloud agent with none of the notices attached.

Rather than a second mechanism, the same generate-credits.rb reads .agents/external-skills.json and vendors each skill's notice at its pinned ref, alongside the library credits.

One thing I did push back on slightly. I kept them visually distinct rather than folding them into the existing "Open Source" list: SoftwareCredit.Kind is .library (compiled into the binary) or .developmentTool, and About renders a separate "Development Tools" section whose footer says outright that they aren't part of the app on your device. A skill is credited because the repo copies it, not because a user is running it, and one merged list would have credited honestly while describing the app falsely. The Kind distinction is enforced as an invariant in CreditKit/AGENTS.md so a future UI can't quietly collapse it.

Follow-on for whenever you next touch it: ./sync-agents --update changes the pinned refs, so the credits script needs a re-run after it. That's documented in the root AGENTS.md and in the sync-agents section, but it's a manual step — if it proves easy to forget, having sync-agents --update invoke the generator itself would be the obvious next move.

Attribution lived in WhereCore only because WhereCore happened to be the
one module linking an external package. That made the dependency edge
point the wrong way: a package pulled in by Broadway or Periscope could
not be credited without those modules depending on app code.

Attribution now has its own Shared leaf module, CreditKit, that anything
may depend on. Its manifest and license notices are generated rather
than hand-kept -- generate-credits.rb derives libraries from the
packages a target actually links via .product(name:package:) and reads
each notice at the pinned revision, so a new dependency is credited
wherever it lands, and a package resolved only for tooling
(BumperBowling, swift-syntax) correctly is not.

The same script credits the four agent skills sync-agents vendors: we
make copies of MIT-licensed work and were shipping none of the notices,
because sync-agents copies only the skill subdirectory and drops the
upstream LICENSE. SoftwareCredit.Kind keeps them apart from linked
libraries, and About renders them as a separate section -- they are
credited because the repo copies them, not because they are in the
binary, and one merged list would describe the app falsely.

Co-authored-by: Cursor <cursoragent@cursor.com>
@kyleve kyleve changed the title Add a Settings About page Add a Settings About page, with attribution in a new CreditKit module Jul 26, 2026
kyleve and others added 9 commits July 26, 2026 16:51
CreditKit was the Where app's attribution data wearing a library's
clothes: it carried this repo's credits.json and five vendored notices
in its own resource bundle, its generator hardcoded Package.swift and
.agents/external-skills.json, and its tests asserted that ZIPFoundation
was linked -- a library making claims about its consumer's dependency
graph. A second app could not have used any of it.

It now owns the format and the tooling and nothing else. An app declares
its own inputs in an attribution-sources.json, ./attribution runs the
report over them, and the result lands in that app's resources, because
a report describes one app's dependency graph and is therefore that
app's data. Dropping the resource bundle also drops the last dependency:
CreditKit is Foundation-only and throws rather than logs, leaving the
caller to decide what a missing report means.

Notices are inlined into the report rather than shipped as sidecar
files, so one decode yields everything needed to discharge the
attribution and the missing-file path stops existing. The generated
notices are byte-identical to the ones previously vendored by hand.

WhereCore.AppAttribution reads the report from Bundle.main beside
BuildInfo, and splits the two failures those two share: a bundle with no
report is legitimate -- only the app target ships one -- while a report
that will not decode is a corrupt resource and faults. About renders an
explicit "no report" state instead of empty sections. The assertions
about what Where actually credits moved to its own test bundle, the one
hosted by Where.app, where Bundle.main is the shipping bundle.

Co-authored-by: Cursor <cursoragent@cursor.com>
Four defects found reviewing the attribution refactor.

`AboutSettingsView` took the report as a default argument, and Swift
evaluates those on every `init` — which SwiftUI does on every body pass
of the Settings stack. Each one re-read a 6.8KB file, re-ran the
decoder over five inlined notices, and emitted a log line, so typing in
the search field did all of it per keystroke. The catalog this replaced
was a `static let` that decoded once. `AppAttribution.main` restores
that; `current(bundle:)` stays for tests and other bundles.

The "no attribution" message was keyed on the report being absent
rather than on the section being empty, so a report carrying only
libraries rendered the tools header and footer over no rows — with the
footer still promising a list to tap through. Both cases now say
something, in different words: an absent report means the build is
unattributed, an empty section means only that this kind is.

The generator could also emit two credits with one name, which
`SoftwareCredit.id` hands SwiftUI as a duplicate `ForEach` id. A
library's name is its repo basename, so two orgs publishing the same
name is all it takes. It now rejects collisions case-insensitively over
the assembled report, the only place holding every credit at once.

Finally, `kind` went from config into the report unvalidated, so a typo
generated and committed cleanly and then failed to decode in the app as
a fault plus a debug trap. It is now checked against the two cases
`SoftwareCredit.Kind` decodes, before any network work.

Report output is byte-identical; the new guards only add failure paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
Neither is addressed in this branch, so they go to the backlog rather
than being dropped.

The drift guard's blind spot is the more serious one: AppAttributionTests
compares credit names, so a bumped pin leaves every name unchanged and
the report keeps stale versions and stale notice text silently. That
defeats the pinned-revision read, so it lands as a P1. It sits at root
because the fix reaches the ./attribution wrapper and CI as well as the
generator, and it is needs-design because the obvious regenerate-and-diff
check would make CI depend on GitHub being up.

The loose github_slug regex is a clarity fix, not a security one, and is
CreditKit's alone — which gives that module its first TODOs.md.

Co-authored-by: Cursor <cursoragent@cursor.com>
Brings in the typed LifecycleKit migration (#116), matrixed WhereUI
snapshot testing (#101), and per-checkout simulators (#141).

Two conflicts, both documentation prose where each side added a
different thing to the same paragraph:

- Root AGENTS.md, the dev-script list: main added `simulator` owning a
  device per checkout and renamed its section heading; this branch added
  `attribution`. Kept main's wording and anchor, and folded `attribution`
  back into both the list and the "owns state easy to corrupt" set,
  which is now three scripts rather than two.

- WhereUI README, the RootView bullet: main rewrote it for the typed
  launch plan and LifecycleKitUI's container; this branch had appended
  the About screen's description. Took main's rewrite as the base and
  re-added the About sentence after the data-screens clause.

Everything else auto-merged. Verified on the merged tree rather than
trusting that: regenerated the project, then swiftformat --lint,
xcstrings --lint, bumper lint, and the full Stuff-iOS-Tests scheme —
251 suites, all green, with the attribution suites among them.
The snapshot bundle from #101 landed on main after this branch wrote the
About screen, so the two had never met: adding an About row to the
Settings list invalidated 9 of its 11 references, and the two new screens
had no coverage at all. That bundle runs in its own scheme outside
Stuff-iOS-Tests, which is why the main suite stayed green through it.

Re-recorded the 9 Settings references, and gave AboutSettingsView and
LicenseView the conformance + suite the module convention asks for. The
About matrix is built around absence, since the shipping build is the
only one that is both stamped and attributed: a full report, a dirty
tree, no report at all, and a report that credits only one kind. That
last one pins the empty-section bug from the review as an image, which
is the right guard for it — the failure was purely visual, a header and
footer over no rows.

Recording surfaced a stale string. LicenseView's empty-notice case still
read "This license text couldn't be loaded from the app bundle", which
described the old architecture where notices were separate resources.
There is no file to load now — the notice is inlined into the report —
so it reads "This credit's report entry carries no license text".

Also moved the hand-built SoftwareCredit fixture into PreviewSupport as
sampleCredit(noticeText:); it had been spelled out in three places.

WhereUISnapshotTests: 31 suites green on a clean re-run. Stuff-iOS-Tests,
swiftformat --lint, xcstrings --lint, and bumper lint all green too.
The .product(name:package:) heuristic meant "linked by something in the
package graph", and it was only accidentally the same as "ships in the
app" — ZIPFoundation in WhereCore was the sole .product site when it was
written. Merging main broke that: SnapshotKitTesting links the
snapshot-testing engine and the accessibility parser, so the generator
would have credited both as libraries, telling a reader their binary
contains a test harness. That is exactly the misdescription Kind exists
to prevent.

Kind is now derived rather than declared. The config's shippedFrom names
the package targets the app and its extensions link (WhereUI and
WhereIntents cover both, and SnapshotKitTesting sits outside that
closure); the generator parses the manifest's target graph and walks it
from those roots, and a package inside the closure is a library while any
other linked package is a developmentTool. shippedFrom is the only value
set by hand — everything downstream follows from the graph, so a new
dependency can't be silently misfiled.

That also brings back the two packages the report had been missing
entirely: the committed report predated the merge, so it credited neither.
They are development tools for the same reason the vendored agent skills
are — the repo depends on them, they just never reach a device.

Verified the derivation rather than the output: adding SnapshotKitTesting
to shippedFrom flips both packages to library, so reachability is really
driving it. Report is idempotent (same sha256 across runs).

Since a source can now emit more than one kind, credits sort by kind then
name, so adding a test-only package doesn't reshuffle shipping libraries.
Source validation also moved up front, ahead of any network work, and now
checks required keys per source type.
Closes the P1 drift-guard item, and corrects what that item claimed.

I filed it saying the guard caught an added dependency but not a bumped
one. It caught neither. AppAttributionTests compared the report against a
hardcoded list of names, so a dependency added without regenerating left
the report and the list still agreeing — which is exactly what happened
on this branch: the suite passed while the committed report was missing
the two snapshot packages the main merge had added. A literal can only
ever restate what someone wrote down, and a test bundle can't read
Package.swift to do better.

--check re-derives the expected report from Package.swift,
Package.resolved, and the skills manifest, then diffs it against the
committed one. It makes no network calls, which is what lets it gate CI:
notices are fetched at the pinned revision, so a matching revision means
matching text by construction, and the notice being present is checked
directly. Runs in well under a second, so it rides along in the format
job beside the catalog lint rather than taking a new required check name.

Verified against the three ways a report goes stale — a credit missing
after a dependency lands, a version left behind by a bump (the case the
old guard was supposed to catch), and a notice blanked by hand. Each
fails with the offending credit named.

AppAttributionTests keeps only what the shipping bundle alone can answer:
the report is present, decodable, carries its notices inline, has unique
names, and populates both kinds. The name lists are gone rather than
updated, since keeping them means editing a test on every dependency
change for no added safety.
Brings in #142, which ledgers the broken-snapshot follow-ups from #101.
Docs-only — four TODOs.md files, no code — so the branch's existing
verification still stands.

One conflict, in the root TODOs.md P2 bucket, and purely additive: main
added the broken-snapshots item and its two nested sub-items while this
branch had added the PeriscopeCore LogSession one. Kept both.
Brings in #143, which consolidates every image suite into one
StuffSnapshotTests bundle (renamed from WhereUISnapshotTests) and moves
the PeriscopeViewer and SwiftDataInspector suites to the modules that own
those views.

The rename doesn't reach this branch's two new suites: the consolidated
bundle still lists Where/WhereUI/SnapshotTests/** among its sources, so
AboutSettingsViewSnapshotTests and LicenseViewSnapshotTests are picked up
unchanged. Main also touched the snapshot rendering helpers
(MotionIsStatic, SnapshotCaptureFlagProbe), which could have shifted the
28 references recorded here, so the whole bundle was re-run rather than
assumed: 30 suites green with no re-recording needed.

One conflict, in the root TODOs.md, structural rather than substantive:
main moved its broken-snapshots item into "Completed issues" and bucketed
that section by original priority, while this branch had appended the
closed CreditKit item there flat. Kept both, with the CreditKit item
under a P1s heading to match main's new bucketing.

Verified on the merged tree: swiftformat --lint, xcstrings --lint,
./attribution --check, bumper lint, Stuff-iOS-Tests, and the renamed
StuffSnapshotTests scheme all green.
@kyleve
kyleve merged commit 575f83c into main Jul 27, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant