Skip to content

feat(engine): @Resource — find resource types at launch (ENG-3734) - #29

Merged
Ryan Zhu (underthestars-zhy) merged 14 commits into
ryan/eng-3705-astrolabe-entry-style-macro-for-factsfrom
ryan/eng-3734-astrolabe-resource-find-resource-types-at-launch-instead-of
Nov 1, 2026
Merged

Ryan Zhu (underthestars-zhy) merged 14 commits into
ryan/eng-3705-astrolabe-entry-style-macro-for-factsfrom
ryan/eng-3734-astrolabe-resource-find-resource-types-at-launch-instead-of

Conversation

@underthestars-zhy

@underthestars-zhy Ryan Zhu (underthestars-zhy) commented Oct 13, 2026 •

Copy link
Copy Markdown
Member

Implements ENG-3734. Stacked on #28 (ENG-3705), which is stacked on #26 (ENG-3699) and #25 (ENG-3688, next).

What

Apps had to list every resource type so the engine could decode records.json after a restart:

static var resourceTypes: [any Resource.Type] { [StagingFile.self] }

A missing entry failed silently, and only after a restart. Example: state.json drops a.txt and the daemon restarts. The StagingFile record can't be decoded, so a.txt is never removed.

@Resource replaces the list, the way Swift Testing finds @Test:

@Resource
struct StagingFile {
    static let kind = "staging.file"
    …
}

The macro adds the conformance and a record that the linker gathers into one section of the binary:

@section("__DATA_CONST,__astrolabe_res") @used
public nonisolated static let __resourceRecord: __ResourceRecord = (
    0x6173_7472,
    { out in out.initializeMemory(as: (any Resource.Type).self, to: StagingFile.self) }
)

At launch the daemon reads that section in every loaded image (_dyld_image_count + getsectiondata) and gives every linked type to the registry. So a record decodes whether or not body declares its type.

Mistakes the compiler catches

  • Forgetting the macro: Resource requires __resourceRecord, and an unavailable default turns a hand-written conformance into an error:

    unavailable static property '__resourceRecord' was used to satisfy a requirement of protocol 'Resource': conform with @Resource, so the engine can find the type at launch

  • Generic types, or types nested in a generic type or function, get a macro error. A record names one concrete type, and @section / @used don't work in generic contexts anyway.
  • One kind claimed by two types anywhere in the binary now stops the daemon at launch, even if body declares neither.

Changes

  • ResourceMacro (member + extension) in AstrolabeMacros. It skips the conformance when the type already declares it, for example through a refining protocol.
  • ResourceRecords.linkedTypes() scans the section. Records carry a magic number, so stray bytes are never called.
  • Engine takes resourceTypes:. RunEngine passes the linked types, and tests pass their own: the test binary links ClashA / ClashB, which share a kind on purpose.
  • Astrolabe.resourceTypes is removed. It never shipped on main, so there's no deprecation.
  • swift-tools-version: 6.3: @section / @used are SE-0492, implemented in Swift 6.3. README, docs and AGENTS.md updated.
  • Example: StagingFile is @Resource, and StagingApp drops its list.

Tests

swift test on this Mac (Swift 6.4): 21/21 macro tests pass, and 165/166 engine tests pass. The one failure is storageClientConcurrentWritesPreserveAllKeys (.helperNotFound, Xcode 27 beta), which also fails on main.

  • ResourceMacroTests: expansions for a struct and a nested type, and the diagnostics for a protocol, a generic type, and a type inside a generic type or function.
  • ResourceRecordsTests: every @Resource type in the test binary is found exactly once, including a nested private type nothing names. A registry over the linked types decodes them, and the two clash types fail it.
  • Fakes: the generic FakeResource<P> becomes four concrete @Resource types over a FakeResource protocol, with the same kinds and JSON. The harness takes resourceTypes: and defaults to all four.
  • Compiler checks, with a throwaway file: a hand-written conformance fails with the message above, @Resource plus an explicit : Resource compiles, and a generic type gets the macro's error.
  • Release binary: swift build -c release --product StagingEngine contains exactly one 16-byte record in __DATA_CONST,__astrolabe_res, starting with the magic. A separate spike found every type in debug, release, release with -dead_strip, and full LTO.

Not yet verified

  • On a staging Mac: roll out a.txt, remove it from state.json, and restart the daemon. a.txt is removed.
  • The Cursor Linux VM still has Swift 6.2.4, which can't read a 6.3 manifest. It needs a 6.3+ toolchain via swiftly.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Summary by CodeRabbit

  • New Features
    • Resource types can now be declared with @Resource and discovered automatically when the engine launches, including types not explicitly listed by an app.
  • Updates
    • Engine setup now takes resource types directly, rather than reading them from the app declaration.
    • Resource types that claim the same kind cause a launch conflict; later duplicates encountered in a declaration are rejected.
  • Compatibility
    • Swift 6.3 or later is now required.

Apps listed every resource type in Astrolabe.resourceTypes so records
could be decoded after a restart. A type left off the list failed
silently: once body stopped declaring it, its record stayed
undecodable and the resource was never removed.

@resource replaces the list, the way Swift Testing finds @test:

    @resource
    struct StagingFile { … }

The macro adds the Resource conformance and a record, a @section /
@used constant in __DATA_CONST,__astrolabe_res. At launch the daemon
reads that section in every loaded image and seeds the registry with
every linked type.

- Resource requires the record. An unavailable default turns a
  conformance written without the macro into an error that says to
  add @resource.
- ResourceMacro (member + extension) rejects generic types and types
  nested in a generic type or function: a record names one concrete
  type.
- Engine takes resourceTypes: RunEngine passes the linked types, tests
  pass their own. The test binary links ClashA/ClashB, which share a
  kind on purpose.
- Astrolabe.resourceTypes is removed. It never shipped on main.
- swift-tools-version 6.3: @section/@used are SE-0492, Swift 6.3.
- Tests: the generic FakeResource<P> becomes four concrete @resource
  fakes over a FakeResource protocol, with the same kinds and JSON.
  ResourceMacroTests covers expansions and diagnostics, and
  ResourceRecordsTests covers discovery. The macro test helper moves
  to AssertExpansion.swift.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 13, 2026 19:10

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Organization UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: 8f64270b-fbf0-47f7-9c9d-c1dde3489f48

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review

No actionable comments were generated in the recent review.

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Team
  • Run ID: 4b4e3dad-91e8-445a-9c60-1d6829913466
📥 Commits

Reviewing files that changed from the base of the PR and between 6206da9 and 0491683.

📒 Files selected for processing: 26
  • AGENTS.md
  • Examples/StagingEngine/StagingApp.swift
  • Examples/StagingEngine/StagingFile.swift
  • Package.swift
  • README.md
  • Sources/Astrolabe/Astrolabe.swift
  • Sources/Astrolabe/CLI/RunEngine.swift
  • Sources/Astrolabe/Engine/Engine.swift
  • Sources/Astrolabe/Resource/Resource.swift
  • Sources/Astrolabe/Resource/ResourceRecords.swift
  • Sources/Astrolabe/Resource/ResourceRegistry.swift
  • Sources/AstrolabeMacros/Plugin.swift
  • Sources/AstrolabeMacros/ResourceMacro.swift
  • Tests/AstrolabeMacrosTests/AssertExpansion.swift
  • Tests/AstrolabeMacrosTests/FactEntryMacroTests.swift
  • Tests/AstrolabeMacrosTests/ResourceMacroTests.swift
  • Tests/AstrolabeTests/AstrolabeProtocolTelemetryTests.swift
  • Tests/AstrolabeTests/DeclarationTests.swift
  • Tests/AstrolabeTests/EngineFactEntryTests.swift
  • Tests/AstrolabeTests/EngineFactTests.swift
  • Tests/AstrolabeTests/EngineIntegrationTests.swift
  • Tests/AstrolabeTests/ResourceRecordsTests.swift
  • Tests/AstrolabeTests/Support/EngineHarness.swift
  • Tests/AstrolabeTests/Support/FakeResource.swift
  • docs/content/docs/getting-started.mdx
  • docs/content/docs/index.mdx
💤 Files with no reviewable changes: 5
  • Examples/StagingEngine/StagingApp.swift
  • Tests/AstrolabeTests/EngineFactEntryTests.swift
  • Tests/AstrolabeMacrosTests/FactEntryMacroTests.swift
  • Tests/AstrolabeTests/EngineFactTests.swift
  • Tests/AstrolabeTests/AstrolabeProtocolTelemetryTests.swift

Included review availability: This review used your included allowance. 2 included reviews remain after this review. Your included PR review attempts over the past 7 days set your current allowance at 4 reviews per hour.

📜 Recent review details
🧰 Additional context used
🧠 Learnings: 1
📓 Common learnings
Learnt from: CR
Repo: photon-hq/Astrolabe

Timestamp: 2026-10-13T23:09:12.250Z
Learning: Source excerpt:
# AGENTS.md

## Cursor Cloud specific instructions

### Toolchain on the Linux VM

- The package needs Swift **6.3+** (`swift-tools-version: 6.3`; `Resource` emits
  `section`/`used`, SE-0492). The VM snapshot was set up with Swift **6.2.4** via
  `swiftly`, which can't read the manifest: install a 6.3+ toolchain with `swiftly`
  first. It is on `PATH` for login shells. In a non-login/non-interactive context, call it
  by absolute path `"$HOME/.local/share/swiftly/bin/swift"` or source
  `"$HOME/.local/share/swiftly/env.sh"` first.
🔇 Additional comments: 21
AGENTS.md: 1

6-6: LGTM!

Also applies to: 22-23, 33-36

Package.swift: 1

1-1: LGTM!

README.md: 1

320-320: LGTM!

docs/content/docs/getting-started.mdx: 1

33-33: LGTM!

docs/content/docs/index.mdx: 1

73-73: LGTM!

Sources/Astrolabe/Resource/Resource.swift: 1

61-63: LGTM!

Also applies to: 94-97

Sources/AstrolabeMacros/ResourceMacro.swift: 1

32-83: LGTM!

Sources/AstrolabeMacros/Plugin.swift: 1

6-6: LGTM!

Tests/AstrolabeMacrosTests/AssertExpansion.swift: 1

1-28: LGTM!

Tests/AstrolabeMacrosTests/ResourceMacroTests.swift: 1

1-125: LGTM!

Sources/Astrolabe/Astrolabe.swift: 1

23-25: LGTM!

Sources/Astrolabe/CLI/RunEngine.swift: 1

36-36: LGTM!

Sources/Astrolabe/Engine/Engine.swift: 1

47-51: LGTM!

Also applies to: 67-67

Sources/Astrolabe/Resource/ResourceRegistry.swift: 1

4-6: LGTM!

Tests/AstrolabeTests/ResourceRecordsTests.swift: 1

1-40: LGTM!

Examples/StagingEngine/StagingFile.swift: 1

5-6: LGTM!

Tests/AstrolabeTests/DeclarationTests.swift: 1

122-123: LGTM!

Also applies to: 131-132, 223-223

Tests/AstrolabeTests/EngineIntegrationTests.swift: 1

6-7: LGTM!

Also applies to: 80-83

Tests/AstrolabeTests/Support/EngineHarness.swift: 1

45-56: LGTM!

Also applies to: 108-109, 183-189

Tests/AstrolabeTests/Support/FakeResource.swift: 1

5-77: LGTM!

Sources/Astrolabe/Resource/ResourceRecords.swift: 1

33-35: 🩺 Stability & Availability

The alignment concern is undecidable from the supplied evidence. The macro places typed __ResourceRecord storage in the custom section, but the Swift section proposal does not specify the resulting section or symbol alignment. No built image or linker output establishes alignment across the supported configurations. The source therefore does not prove a misaligned load, but it also does not establish that load(fromByteOffset:as:) is safe in every configuration.

Walkthrough

The package now requires Swift 6.3. Resource types use the @Resource macro to emit linked records. At launch, the CLI discovers those records and supplies their types to the engine registry. Engine callers and tests now pass resource types explicitly.

Changes: Resource Discovery

Layer / File(s) Summary
Swift 6.3 requirement
Package.swift, AGENTS.md, README.md, docs/content/docs/*
The package manifest and version guidance now require Swift 6.3 or later.
@Resource macro
Sources/Astrolabe/Resource/Resource.swift, Sources/AstrolabeMacros/*, Tests/AstrolabeMacrosTests/*
The macro emits resource records and Resource conformance for supported declarations. Expansion tests cover generated output and diagnostics.
Linked-type scanning and engine registration
Sources/Astrolabe/Resource/*, Sources/Astrolabe/CLI/RunEngine.swift, Sources/Astrolabe/Engine/Engine.swift, Tests/AstrolabeTests/ResourceRecordsTests.swift
The runtime scans loaded Mach-O sections for resource records. The CLI passes discovered types to the engine, which uses them to seed the registry. Tests cover linked types and duplicate kind conflicts.
Engine callers and resource fixtures
Sources/Astrolabe/Astrolabe.swift, Examples/StagingEngine/*, Tests/AstrolabeTests/*
Astrolabe.resourceTypes is removed. Callers and test harnesses pass resource types explicitly, and resource fixtures use @Resource.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant RunEngine
  participant ResourceRecords
  participant Engine
  participant ResourceRegistry
  RunEngine->>ResourceRecords: Call linkedTypes()
  ResourceRecords->>ResourceRecords: Scan loaded Mach-O sections
  ResourceRecords-->>RunEngine: Return resource metatypes
  RunEngine->>Engine: Initialize with resourceTypes
  Engine->>ResourceRegistry: Seed registry with resource types
Loading
Merge Risk: Minimal · previous review 04916

No demonstrated issue blocks merging. Section-record alignment remains unverified and is worth checking in a supported build.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 17 files. (4 skipped:… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: launch-time resource type discovery through @Resource.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 32.14% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 17 files. (4 skipped: 4 unsupported.)

Finishing Touches
Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

I’m a rabbit, ears held high,
I watch new resource records fly.
Swift six point three lights the way,
Linked types join the engine’s day.
I nibble clover, soft and slow,
While registries learn what’s in the show.

The v2 engine couldn't install the Xcode Command Line Tools, so it
couldn't bootstrap Homebrew. CommandLineTools is the first built-in
resource, installed headlessly through softwareupdate:

    CommandLineTools()                    // install if missing
    CommandLineTools(version: .latest)    // and keep it current

.installed reads no fact. .latest reads \.commandLineTools
.latestAvailable, the max of the installed version and the newest
non-beta CLT the catalog lists: softwareupdate stops listing CLT once
it's current, so the max is the same before and after an install and
is safe to hash. Its first read after launch never contacts Apple, and
it checks the catalog at most every 6 hours. \.commandLineTools
.installed is public for resources that must redo work after an
update.

- Both facts read under the lock the rollout installs under, so
  neither sees a half-finished install. ensure checks the Mac first
  without the lock, unless an install is in progress.
- Versions are read from the listing's Version: field, not the label
  (macOS 26 changed the format), and normalize to major.minor.patch.
- The rollout forgets a stale receipt, places the trigger file
  (removed on every path), installs the newest CLT at or above the
  minimum, verifies it, and selects it when xcode-select points
  nowhere. remove never uninstalls.
- All I/O goes through CommandLineToolsSystem, bound per task through
  CommandLineToolsContext, so tests never run softwareupdate.
- Re-adds the Semaphore dependency for the cancellable lock.
- Tests: listing fixtures for macOS 15 and 26, versions, the catalog
  schedule, both reads, the rollout, the lock, and engine runs over a
  FakeCLTSystem that writes its receipt before the install finishes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The v2 engine only rolled an id out again when its declared hash
changed, a retry came due, or its operation was cancelled. A deleted
CLT stayed deleted, and Homebrew (ENG-3749) has the same gap. @monitor
works like @fact, but for resources:

    @resource
    struct CommandLineToolsInstall {
        let minimum: CLTVersion?
        @monitor(\.commandLineTools.isInstalled) var isInstalled
        …
    }

It reads the fact once, when body builds the resource, and stores the
value in the snapshot. The engine hashes every monitor into a monitor
hash, kept apart from compute's: a monitor-only change reruns rollout
toward the same hash, and never cancels a running one. CLT monitors the
new derived fact \.commandLineTools.isInstalled, so a deletion is
repaired, and an update made outside Astrolabe (16.4 → 16.5) starts
nothing.

- Monitors are found by reflection, the way SwiftUI finds DynamicProperty
  fields: no macro changes and no public requirement, and a monitor
  built outside body is detected and logged
  (astrolabe.declaration.unobserved_monitor).
- Target stays declared-only. The monitor hash rides beside it in
  Declaration.Item and a computed Mark.monitor, so failures, attempts,
  backoff and .wait are unchanged, and a flapping fact can't defeat
  backoff. records.json gains no field: the value lives in the snapshot.
- diff cancels a running operation only for a new target; a monitor
  change holds .waitingForOperation and reruns once it ends.
- wrappedValue is Value?: nil outside a pass, or in a record written
  before the monitor existed, which still decodes (the key is left out
  when unread, and a missing key decodes as unread).
- The dry run marks a monitor-only rollout with "(monitor)".
- CommandLineTools() now reads installed through the monitor; its first
  pass waits for that read, which never contacts Apple. Docs for
  installed, Resource.compute and the README follow.
- Tests: @monitor reads and coding; engine runs for an idle change, a
  change mid-rollout, a declared change superseding, a flip-back,
  records across restarts and from before the monitor, one follow-up
  after a repair, backoff and .rollback, the dry run, and an unobserved
  monitor; CLT deleted while running, under .latest, and while the
  daemon was down, an outside update, and an old record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The engine never re-reads a fact when an operation ends, so whatever
waits on \.commandLineTools.installed (Homebrew, ENG-3749) sat gated for
up to 15 minutes after CLT landed. At 60 s a read nearly always comes
due during the install and queues on the CLT lock, so the value lands
the moment the install ends. A deleted CLT is also repaired within a
minute.

The cost is a stat and one pkgutil --pkg-info a minute. latestAvailable
stays at 15 minutes, and its catalog check at 6 hours.

- Tests: the drift tests' "next read" advances are 60 s, so they prove
  the new interval.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every Brew (ENG-3738) needs brew, and the v2 engine could install CLT
but not Homebrew. Homebrew(owner:) installs it headlessly, as the root
daemon, even at the login window, and keeps it owned by one account:

    CommandLineTools()          // required: Homebrew never installs CLT
    Homebrew(owner: "ops")

owner is an account's short name, never root, one per Mac. Every brew
runs as it; people run brew by hand with `brew as-brew-user <cmd>`.

- Installs with Homebrew's own install.sh, run as the owner with
  NONINTERACTIVE=1. It refuses root, and as the owner needs sudo only to
  create the prefix in root's /opt. So the daemon creates /opt/homebrew
  and chowns it (and anything a broken install left) to the owner first:
  no sudo, no password. The daemon writes /etc/paths.d/homebrew, which
  install.sh skips without sudo.
- Homebrew is a Component that declares HomebrewInstall only once
  \.commandLineTools.isInstalled reads true. Safe to gate: remove does
  nothing. Without CommandLineTools() it waits silently.
- @monitor(\.homebrew.owners) repairs drift. owners lstats the prefix,
  Cellar, Caskroom, opt and bin, and each entry directly inside them,
  every 60 s, under the lock the rollout installs under. Deleted ([]) →
  install again. Wrong or mixed owners (another admin's brew install) →
  chown -R -P <owner> /opt/homebrew, nothing downloaded. brew update
  changes nothing.
- ensure mirrors CLTInstaller: a lock-free healthy fast path (owners ==
  [owner] and brew --version runs as owner), then a locked CLT read that
  waits out a CLT install, then the Homebrew lock. Locks never nest.
  Unknown owner, root, Intel and a missing CLT throw before touching the
  Mac; a re-own or install that didn't take throws, so retries apply.
- UserContext.setting(_:) sets extra environment variables for a child
  run as a user.
- CLT docs no longer say Homebrew calls CLTInstaller.ensure.
- Examples/HomebrewEngine reads the owner from
  /etc/astrolabe-homebrew-owner: a LaunchDaemon never sees the sudo
  environment that installed it.
- Tests: the owners read (sorted, waits out an install, throws on
  failure), ensure on a fresh Mac, leftovers, failures, cancellation, bad
  inputs, a CLT install in progress, mixed owners, an owner change, a
  re-own that didn't take, a brew that won't run, the lock, and the
  engine: CLT then Homebrew, no CommandLineTools(), restart, deletion,
  another admin's formula, an owner change and drop, CLT deleted, and the
  dry run. withCLT moves to Support beside withHomebrew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…3738)

v2 could install CLT and Homebrew itself, but not a single formula or
cask, and most farm setup is Brew(...). Brew ports v1's Brew onto
@resource, @monitor, and the Homebrew facts and lock:

    CommandLineTools()
    Homebrew(owner: "ops")
    Brew("wget")
    Brew("firefox", type: .cask)
    Brew("tailscale", version: .atLeast("1.102.0"))

- Brew is a Component that declares BrewPackage (astrolabe.brew) once
  \.homebrew.isInstalled reads true. Gated on isInstalled, never on one
  owner: remove really uninstalls, and mixed owners (another admin's
  brew install) would otherwise hide, and uninstall, every package.
- The id is brew:<type>:<short name>, so tap-qualifying a name keeps the
  id and never uninstalls. The hash covers name, type and minimum: a new
  .atLeast floor upgrades in place.
- rollout: check the Mac without the lock (satisfied → no lock, no
  brew); else under HomebrewContext.lock, check again, resolve the one
  owner from system.owners(), brew install|upgrade --formula|--cask
  <name> as the owner, and verify. An uncomparable version throws and
  never upgrades. [] owners → homebrewMissing; mixed, root or #uid →
  homebrewNotReady, retried once Homebrew re-owns.
- remove: brew uninstall --formula|--cask <short name>, only if it's
  there. Uses only what the id is built from.
- \.homebrew.packages scans Cellar, Caskroom and opt every 60 s with
  readdir and readlink, no brew and no lock. Throws only on real I/O
  errors. Each BrewPackage monitors \.homebrew[satisfies:], its own Bool,
  so a hand-run brew uninstall or a downgrade is undone within a minute.
  This replaces v1's 15 s loop.
- .latest(checkEvery:) is dropped: time passing never changes a hash. A
  follow-up adds an available fact that body pins into .atLeast.
- HomebrewSystem gains packages() and brew(_:as:). BrewVersion is v1's,
  verbatim.
- Examples/BrewEngine reads packages from /etc/astrolabe-brew-packages.
- Tests: ids, hashes and the snapshot; the scan on a temp prefix; v1's
  version cases; check and the subscript; ensure and uninstall over the
  fake (call order, no lock when satisfied, upgrade, floors, owners,
  failures, casks, taps, cancellation, one brew at a time); and the
  engine: CLT then Homebrew then Brew with no failures, no Homebrew(),
  drift, dropped from body and across a restart, a new floor, tap
  qualification, mixed owners, a deleted Homebrew, a superseded rollout
  waiting on the lock, and the dry run. FakeHomebrewSystem keeps one
  package map and runs brew.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…y in dry runs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rift' into ryan/eng-3749-astrolabe-homebrew-itself-as-a-v2-resource
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e' into ryan/eng-3738-astrolabe-brew-as-a-v2-resource
…-a-v2-resource

feat(brew): Brew — Homebrew formulae and casks as a v2 resource (ENG-3738)
…w-itself-as-a-v2-resource

feat(homebrew): Homebrew — Homebrew itself as a v2 resource (ENG-3749)
…-repair-resources-that-drift

feat(engine): @monitor — repair resources that drift (ENG-3751)
…-line-tools-as-a-v2-resource

feat(clt): CommandLineTools — Xcode CLT as a v2 resource (ENG-3739)
@underthestars-zhy
Ryan Zhu (underthestars-zhy) merged commit a1857e8 into ryan/eng-3705-astrolabe-entry-style-macro-for-facts Nov 1, 2026
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.

2 participants