Library vs Target Packs¶
Pack v1 (#2026-04-27 devlog) introduced pack.yaml as the authored boundary for both runnable targets and the cross-target reusable tooling that already shipped via the framework trailblaze pack — but never named the distinction. Empirically two pack shapes were already present: ones with a target: block (clock, wikipedia, contacts) and ones without (trailblaze). Any contract that should only apply to one of them lived as a comment, an implicit convention, or nothing at all.
This PR draws the line. A pack’s target: field — the existing target: PackTargetConfig? slot — is the discriminator:
- Target pack (
targetnon-null) — models a runnable app under test. May declaretarget:(display shape, platforms, app ids),tools:,toolsets:,waypoints:, and trailhead tools. The framework-bundledclock,wikipedia,contactspacks are target packs. - Library pack (
targetnull) — ships cross-target reusable tooling:tools:andtoolsets:only. Library packs MUST NOT declarewaypoints:or trailhead tools (a trailhead bootstraps to a known waypoint, which only makes sense within a target). The framework-bundledtrailblazepack — which contributes per-platformdefaults:— is the canonical library pack and was retroactively the first one.
We deliberately did not add a new type: library | target field. A separate type tag risks disagreeing with the actual content (type: library next to a populated target: block); the existing target: slot is the single source of truth. Tools and toolsets are orthogonal to pack type — both shapes contribute them through the same registries; the runtime tool registry doesn’t care which pack a tool came from.
The library/target distinction also makes the reverse lookup deterministic: given a waypoint id, walk the resolved pack list to find the (single) pack that declared it, then read that pack’s target for the owning target. No id-prefix convention needed, no first-slash split. This is enabled but not implemented in this PR.
Load-time enforcement¶
The contract is enforced at load time, atomic-per-pack — a violation drops the offending pack but does not poison sibling packs:
TrailblazePackManifestLoader.parseManifestrejects a pack withtarget == nulland a non-emptywaypoints:list, naming the offending entries in the error.TrailblazeProjectConfigLoader.resolvePackSiblingsrejects library packs whose tool YAMLs declare atrailhead:block. This rule has to live one layer deeper than the manifest because thetrailhead:block is inside the tool YAML, not the manifest — so the check has to happen after sibling tools are read.
The discovery hook this PR also lands¶
Tool discovery for the global registry runs through ToolYamlLoader.discoverAndLoadAll, which scans trailblaze-config/tools/ non-recursively. Pack-resolved tools (the tools: list in pack.yaml) only landed in projectConfig.tools for the host-side path; they never made it into TrailblazeSerializationInitializer.buildAllTools() or ToolNameResolver.fromBuiltInAndCustomTools(). That meant a tool YAML moved into a pack subdirectory would be silently invisible to toolset name resolution.
The fix is a recursive scan of trailblaze-config/packs/<id>/tools/*.{tool,shortcut,trailhead}.yaml added to ToolYamlLoader.discoverAllConfigs — pack-bundled tool YAMLs surface in the same map the flat scan populates, and downstream consumers (toolset name resolver, serializer, trail decoder) see them transparently. Plain-.yaml PackScriptedToolFile descriptors (the per-target shape used by clock’s target.tools:) are deliberately excluded — they flow through per-target resolution, not the global registry. The convention is intentionally narrow: only <pack-id>/tools/<name>.<kind>.yaml is accepted, so a stray YAML elsewhere in the pack tree doesn’t falsely register as a tool.
JVM-only path. The Android AssetManager-backed ConfigResourceSource doesn’t recurse, so on-device instrumentation tests don’t pick up pack-bundled tools today. Acceptable for now — the host-side path is sufficient for the use cases that drove this hook. When pack-bundled tools that need on-device discovery land, AssetManagerConfigResourceSource will need a recursive variant.
Library pack happy path¶
# trailblaze-config/packs/<library-pack-id>/pack.yaml
id: my-library
tools:
- tools/foo.tool.yaml
- tools/bar.tool.yaml
No target:. No waypoints:. Toolset YAMLs and trails reference these tools by bare id. Consumer target packs enable a toolset that lists them — same authoring surface as flat-tools/-dir tools.
Scope choices¶
What this PR is not:
- No
type:field onTrailblazePackManifest. Thetarget:slot is the discriminator. - No id-prefix-equals-pack-id rule for waypoints. The pack-manifest binding is enough; the prefix convention is a separate, debatable change.
- No changes to pack-tool dispatch or the runtime tool registry’s resolution semantics. Library-pack tools and target-pack tools register identically; the registry stays unaware of which pack a tool came from.
- No changes to the
trailblaze waypointCLI. Once this PR lands, a follow-up can simplifyWaypointDiscovery.resolveWaypointRootto walk pack-manifest target bindings instead of the convention<workspace>/packs/<id>/waypoints/, but the existing--rootflag’s behavior is preserved. - No
AssetManagerConfigResourceSourcerecursion for pack-bundled tools on Android. Tracked as a follow-up if a pack-bundled on-device tool ships.