Add a Settings About page, with attribution in a new CreditKit module#140
Conversation
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
| 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 |
There was a problem hiding this comment.
Is placing this in WhereCore correct? Attributions aren't unique to WhereCore, they can come in anywhere, right?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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;./attributionruns 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.AppAttributionmakes that call for Where, splitting the two cases the wayBuildInfosplits 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
AppAttributionTestsin the Where app's own test bundle — the one hosted byWhere.app, soBundle.mainis 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.
There was a problem hiding this comment.
Thinking out loud, we should also integrate with the sync-agents tool to credit the external skills we've added.
There was a problem hiding this comment.
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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.
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
.extendingDefaultvalues 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.jsonand 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/CreditKitnow owns the format and the tooling and nothing else. An app declares its own inputs,./attributionruns 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 byPackage.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:
SnapshotKitTestinglinks 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'sshippedFromnames 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 alibraryand any other linked package is adevelopmentTool— same bucket as the vendored agent skills, credited because the repo depends on them rather than because they reach a device.shippedFromis the only value set by hand.WhereCore.AppAttributionreads the report fromBundle.mainbesideBuildInfo, 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.maincaches the decode for the process — About takes it as a default argument, and Swift re-evaluates those on everyinit, 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 (
SoftwareCreditisIdentifiableby name, and a library's name is its repo basename, so two orgs can collide) or an unrecognizedkind, 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
Wheretarget writesWhereGitSHA/WhereGitStatusinto 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, andbasedOnDependencyAnalysis: falseso an unchanged source tree can't ship the previous commit's SHA. A checkout without git metadata stampsunknownrather 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.mdcalls 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 --lintswift run bumper config .,swift run bumper test .,swift run bumper lint .— no violationstuist test CreditKitTests,WhereCoreTests,WhereTests,RegionKitTests,WhereUITests— all green, including the newAttributionManifestTests,AppAttributionTests,BuildInfoTests,RegionDataSourceTests,AboutSettingsViewTests,LicenseViewTests./attributionis idempotent — a second run rewrites the same bytesWhere.app/Info.plist(WhereGitSHA 778d7e8523d1,WhereGitStatus dirty) and re-runs on every buildStuff-iOS-Testsscheme — 251 suites green after mergingmain(the earlier run's fourStuffTestHostbundle 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 recordingSnapshots
main's snapshot bundle (#101) landed after this branch wrote the About screen, so merging the two invalidated 9 of the 11SettingsViewSnapshotTestsreferences — the Settings list grew an About row. Those are re-recorded, andAboutSettingsView/LicenseViewnow have theSnapshotProvidingconformance 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
RegionDataSourceTestsfails if a catalog region is covered by zero or two sources, so a regenerated catalog can't ship uncredited geometry. Coverage derives from theus-prefix the generator mints plus an explicit id list — deliberately not an "everything else" bucket that would quietly mis-credit a new region../attribution --checkre-derives the expected report fromPackage.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.AppAttributionTestshad 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 themainmerge had added. A test bundle can't readPackage.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.separatesLinkedLibrariesFromDevelopmentToolsfails if either section would render empty or lose its heading.Follow-up filed
Root
TODOs.md: aLogSessionnames only a version and build number, both pinned in the manifest, so every developer build logs asv1.0 (1). PeriscopeCore sits below WhereCore and can't readBuildInfo, so carrying the commit into log sessions needs a seam rather than a direct adoption.Made with Cursor