diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/.openspec.yaml b/openspec/changes/archive/2026-08-18-release-source-linking/.openspec.yaml new file mode 100644 index 0000000..9567240 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-08-18 diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/design.md b/openspec/changes/archive/2026-08-18-release-source-linking/design.md new file mode 100644 index 0000000..39d46c7 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/design.md @@ -0,0 +1,68 @@ +# Design: Release Source Linking + +## Context + +Git is a Merkle DAG: an annotated tag object hash transitively pins the commit, tree, and every blob beneath it. A release record carrying that hash does for *source* exactly what the artifact digest does for *built bytes* — a content-addressed, publisher-signed pin. What stays unverifiable is the arrow between them ("these artifacts were built from this source"), which requires reproducible builds and is out of scope. + +Tangled is the atproto-native forge: repos are `sh.tangled.repo` records in the owner's PDS (rkey `any`, `knot` = git host), build artifacts are `sh.tangled.repo.artifact` records (TID key, `tag` = 20 raw bytes of the annotated-tag-object SHA-1, `artifact` = PDS blob ≤ 52MB), and knots emit `sh.tangled.git.refUpdate` push events onto the firehose. `town.dist.project.source` already accepts `at://` URIs, so projects can point at Tangled repo records today — without spec'd semantics. + +## Goals / Non-Goals + +**Goals:** +- Immutable releases can pin their source (commit required, tag-object hash optional) before ecosystem adoption makes the gap permanent. +- Same-DID source associations are self-authenticated with zero infrastructure. +- Tangled's existing tag-attached artifacts work as dist.town storage locations and flow into releases at authoring time. +- All schema changes are additive; nothing existing breaks. + +**Non-Goals:** +- Verifying that artifacts were built from the claimed source (SLSA/reproducible-builds territory; possible future interop). +- Per-artifact source (per-release only; revisit if split-source releases materialize). +- Forge-side verification beyond the same-DID rule (other atproto verification methods are a flagged investigation, not part of this change). +- Any change on Tangled's side; dist.town consumes their records read-only. + +## Decisions + +### D1: `source` is an open union on the release, mirroring `storage` +The `storage` union established the pattern: dist.town defines the integrity layer, variants define location, third parties extend under their own authority. `source` follows it — `gitSource` now; hg/pijul/fossil variants can come from anyone. Consumers hitting an unknown variant degrade to "release with an unrecognized source declaration," indexed and displayed, not rejected. + +### D2: Commit required, tag optional; hash is truth, name is label +`commit` is universal — every forge and workflow has one (GitHub releases frequently use lightweight tags, which have no tag object). `tag` (the annotated-tag-object hash) is the upgrade pin: it adds tagger identity, date, message, and any GPG/SSH signature to what's pinned. `ref` is the mutable human label — tags can be force-moved, so display trusts the name while verification trusts only hashes. Both hashes are algorithm-prefixed strings (`sha1:`, `sha256:`) per the existing digest convention — never raw bytes, so git's SHA-256 migration needs no schema change. (Tangled's `tag` field is 20 raw bytes, SHA-1-only, annotated-only; the matching rule converts trivially, and their constraint is theirs to outgrow.) + +### D3: `repo` inherits from the project +When `gitSource.repo` is absent, the project's `source` field is the claimed repo. The common case (all releases from one repo) states the repo once on the project; a release carries `repo` only when it diverges. Avoids per-release duplication drift; keeps monorepos trivial (tag naming is opaque to us). + +### D4: Same-DID self-authentication, claims everywhere else +When a source at-uri's authority equals the record's DID — the publisher's release pointing at the publisher's own `sh.tangled.repo` record — the association is self-authenticating: both records are signed by the same key in the same repo. Surfaces may badge it. Cross-DID at-uris and `https://` sources are plain claims, rendered as such. This is the changelog authority rule applied to source, costs one string comparison, and honestly degrades: it simply does not extend beyond atproto-native forges. Alternatives (forge API checks, domain proofs, labeler attestations) are deferred as an explicit investigation — claim-only is v1's posture. + +### D5: `recordBlobStorage` is generic, with per-collection resolution rules +The variant is `{ record: at-uri }` — "bytes are the blob carried by another atproto record" — rather than a Tangled-specific shape. Resolution is defined per known collection: for `sh.tangled.repo.artifact`, fetch the record, extract the `artifact` blob ref, serve via the owner's PDS `getBlob`. Any future record type carrying blobs joins by documenting its rule. The digest stays on dist.town's artifact descriptor, so verification is unchanged: the variant provides location, never integrity. Rejected alternative: `tangledStorage` (needlessly narrow) and reusing `blobStorage` (its blob ref must live in the release record's own repo — wrong shape for referencing another record's blob). + +### D6: Discovery splits by whether the publisher must sign +- **Level 1 (new artifacts) is authoring-time only.** Artifact descriptors require digests and releases are immutable, so artifacts can only join at creation, signed by the publisher. The CLI/broker queries the publisher's own PDS for `sh.tangled.repo.artifact` records with matching tag hash, then **downloads and hashes each blob locally** before writing the digest — integrity is re-derived by the publisher's tooling, never copied from Tangled's blob refs. Suggest-and-confirm by default; a flag/config enables CI auto-include. The pipeline norm (upload artifacts, then cut the release) is documentation, not mechanism — late artifacts simply cannot join, by the same write-once discipline as the rest of the record. +- **Level 2 (new locations) is index-time and continuous.** For declared artifacts, a same-DID tag-matched Tangled blob whose bytes hash to a declared digest is just another location for the same bytes — added through the existing `artifactView.locations` / per-location verification machinery. Per-location verification *is* the consent model: a location only ever means "the publisher-signed digest, retrievable here too." Works retroactively for releases cut before the Tangled artifacts existed; failed hashes are simply not locations; blob disappearance is location health, already modeled. + +### D7: Associated-but-not-in-record artifacts are a display tier, not a trust tier +Same-DID tag-matched Tangled artifacts that match **no** declared digest (typically uploaded after the release was cut) are surfaced as *associated*, explicitly caveated as not present in the release record, and visually separated from declared artifacts. They get no digest verification against the release (there is nothing to verify against) and no front-door download treatment — the presentation must make "the publisher's record does not vouch for this file" unmistakable. The data is already in the indexer from level-2 discovery; this tier is what remains after location matching consumes the digest matches. + +## Risks / Trade-offs + +- [Source links read as build provenance ("built from") when they only pin association] → Claim-only posture stated in specs and rendered language; badge means "self-authenticated association," never "verified build." Reproducible-build attestation is named as explicitly absent. +- [Associated-artifact tier gets mistaken for release content] → Normative separation and caveat wording in consumer-surfaces requirements; no download parity with declared artifacts. +- [Tangled lexicon evolution (wire-format changes have shipped before) breaks discovery/resolution] → Read-only consumption isolated behind the per-collection resolution rule; a schema drift degrades discovery, never release integrity (digests are ours). +- [SHA-1 pinning weakens over time] → Prefixed encoding admits `sha256:` the day git repos migrate; SHA-1 remains what git itself provides today (hardened SHA-1DC in practice). +- [Level-2 discovery adds continuous fetch/hash load] → Bounded by Tangled blob cap (52MB) and same-DID + tag-match preconditions; reuses the existing verification worker pattern from location health. +- [Cross-DID Tangled repos (org-style) get no badge] → Correct behavior, not a gap: cross-DID is a claim by design; future verification methods may upgrade it. + +## Migration Plan + +1. Lexicon additions (`source` union, `gitSource`, `recordBlobStorage`, view extensions) — additive, deploy first. +2. Indexer: same-DID evaluation + view hydration for source; then level-2 discovery worker; then associated-artifact derivation. +3. CLI/broker level-1 attach flow. +4. Consumer surfaces last (per the design sign-off norm for user-visible changes). +5. Rollback: each layer disables independently; records written with `source`/`recordBlobStorage` remain valid lexicon data regardless. + +## Open Questions + +- View shape: does `releaseView` gain `source` + `associatedArtifacts` directly, or does the associated tier ride a separate query? (Read plane serves lexicon-defined views only — settle when extending view lexicons.) +- Should level-1 discovery also run in the broker's web publish flow, or CLI-only at first? +- `ref` matching as a fallback when the publisher used a lightweight tag (no tag object): allow name-based discovery with explicit lower confidence, or require the tag-object hash for any discovery? Current lean: require the hash — discovery follows the pin, not the label. diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/proposal.md b/openspec/changes/archive/2026-08-18-release-source-linking/proposal.md new file mode 100644 index 0000000..a4e3390 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/proposal.md @@ -0,0 +1,37 @@ +# Release Source Linking + +## Why + +A release that cannot say what source it came from is a claim with no anchor: every packaging ecosystem consumers trust (npm, crates, Go modules) links artifacts to a repository, and dist.town cannot be adopted at large without the same. The urgency is structural: release records are immutable, so every release published before a source field exists can **never** carry a source link — the gap is permanent per release, which makes landing the schema *before* ecosystem adoption the only moment it can land. Starting with Tangled, the atproto-native forge, additionally makes dist.town legible to the rest of the atproto ecosystem: source links become at-uris, associations become same-DID-verifiable, and Tangled's existing tag-attached artifacts become first-class storage locations instead of a competing silo. + +## What Changes + +- New optional `source` field on `town.dist.release`: an **open union** (third parties may add non-git VCS variants under their own authority), with one variant defined now — `gitSource`: `commit` (required, algorithm-prefixed `sha1:`/`sha256:`, following the existing digest convention), `ref` (optional tag name — mutable human label; the hash is the truth), `tag` (optional annotated-tag-object hash, algorithm-prefixed — a git tag object transitively pins commit, tree, and blobs), `repo` (optional uri, `at://` or `https://`; when absent, the project's existing `source` field is the claimed repo). +- Spec'd **same-DID source semantics** for projects and releases (no project schema change — `source` already accepts `at://`): a source at-uri whose authority equals the record's DID is a self-authenticated association and may be badged; cross-DID at-uris and `https://` sources render as plain claims. Same trust rule as changelog authority. +- **Claim-only posture**: "built from this source" is not verified (that requires reproducible builds — out of scope); the link provides identity of source, navigability, and discovery. Further atproto verification methods are a noted future investigation. +- New storage union variant `recordBlobStorage` `{ record: at-uri }`: artifact bytes are the blob carried by another atproto record, resolved per documented per-collection rules — `sh.tangled.repo.artifact` first (fetch record, extract blob, serve from the owner's PDS). The digest stays on dist.town's artifact descriptor; no variant redefines what verified means. +- **Two-level auto-discovery** of Tangled artifacts: + - *Level 1 — new artifacts, authoring time only*: at release cut, the CLI/broker queries the publisher's own PDS for `sh.tangled.repo.artifact` records whose tag hash matches `gitSource.tag`, offers to attach them (confirm by default; flag/config for CI auto-include), downloads each blob and **hashes locally** — the digest is freshly publisher-attested, never trusted from Tangled — with `recordBlobStorage` as the location. Documented pipeline norm: CI uploads artifacts, then cuts the release; late artifacts can never join the immutable record. + - *Level 2 — new locations, index time, continuous*: for artifacts already declared in a release, the AppView discovers same-DID tag-matched Tangled blobs, hashes bytes against the declared digest, and on match adds a verified location through the existing per-location verification machinery. No record is touched; works retroactively; provides link-rot fallback. +- **Frontend**: same-DID tag-matched Tangled artifacts that appeared after release creation and match no declared digest are listed as *associated*, with an explicit caveat that they are not present in the release record — clearly separated from declared (digest-verified) artifacts. + +## Capabilities + +### New Capabilities + +- `source-linking`: Cross-cutting source-association semantics — the gitSource claim model, hash-is-truth/name-is-label discipline, project-source inheritance, the same-DID self-authentication rule, the tag-hash matching rule (prefixed-string ↔ Tangled's raw bytes), and both discovery levels. + +### Modified Capabilities + +- `release-records`: `Release record shape` gains the optional `source` open union; new requirements for the `gitSource` variant shape and the `recordBlobStorage` storage variant. +- `artifact-hosting`: resolution rules for `recordBlobStorage` (per-collection, `sh.tangled.repo.artifact` first) and index-time discovered locations feeding the existing verification/health model. +- `consumer-surfaces`: release and project pages display the source link with authority-aware presentation (badge only when self-authenticated); release page lists associated-but-not-in-record Tangled artifacts with the caveat. +- `publishing`: CLI/broker release-cut attach flow (suggest/confirm, local hashing, CI auto-include option) and the upload-then-release pipeline ordering norm. + +## Impact + +- **Lexicons**: `town.dist.release` gains optional `source` (schema-additive, non-breaking); `town.dist.defs` gains `gitSource` and `recordBlobStorage`; view lexicons extended so the read plane can carry source and associated-artifact data (read plane serves lexicon-defined views only). +- **AppView/indexer**: same-DID authority evaluation for sources, level-2 location discovery worker, associated-artifact derivation, Tangled record/blob fetching. +- **CLI/broker**: level-1 discovery and attach flow. +- **External surface**: read-only consumption of `sh.tangled.repo.artifact` records and PDS blobs; Tangled's side needs no changes. Their `tag` field is 20 raw bytes (SHA-1, annotated tags only) — our prefixed-string encoding converts trivially and is not constrained by their limits. +- **No breaking changes**: existing releases, projects, and consumers are unaffected; releases published before this change simply cannot carry source links (the motivating urgency). diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/specs/artifact-hosting/spec.md b/openspec/changes/archive/2026-08-18-release-source-linking/specs/artifact-hosting/spec.md new file mode 100644 index 0000000..0478af7 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/specs/artifact-hosting/spec.md @@ -0,0 +1,17 @@ +# artifact-hosting Specification (delta) + +## ADDED Requirements + +### Requirement: recordBlobStorage resolution +The read plane SHALL resolve `recordBlobStorage` locations by fetching the referenced record and serving its blob from the owning repo's PDS, per collection-specific rules. The first documented collection is `sh.tangled.repo.artifact` (blob in the `artifact` field). Resolution failures (record deleted, blob missing, PDS unreachable) are location health states, never release validity states. + +#### Scenario: Deleted Tangled record degrades to location health +- **WHEN** a `sh.tangled.repo.artifact` record referenced by a release's storage is deleted +- **THEN** that location reports unresolvable health (pending with the resolution failure as detail, retried on the normal cadence — never `failed`, which is reserved for bytes that hashed wrong) while the release and its other locations are unaffected + +### Requirement: Discovered locations enter the standard verification lifecycle +Locations discovered at index time (rather than declared in the release record) SHALL enter the same per-location verification lifecycle as declared locations: pending until bytes are hashed against the record digest, verified on match, never surfaced as verified before hashing. Discovered locations MUST be distinguishable from declared locations in view data. + +#### Scenario: Discovered location verifies before serving browsers +- **WHEN** the indexer discovers a same-DID Tangled blob for a declared artifact +- **THEN** browsers are redirected to it only after its bytes have been hashed against the release digest and the location is verified diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/specs/consumer-surfaces/spec.md b/openspec/changes/archive/2026-08-18-release-source-linking/specs/consumer-surfaces/spec.md new file mode 100644 index 0000000..fbc733e --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/specs/consumer-surfaces/spec.md @@ -0,0 +1,21 @@ +# consumer-surfaces Specification (delta) + +## ADDED Requirements + +### Requirement: Source rendering respects authority +Release and project pages SHALL display the source link when present: repo (resolved through project inheritance when the release omits it), ref name, and pinned hashes. A self-authenticated association (same-DID at-uri source) MAY carry a badge stating that the association is self-authenticated; unverified claims (cross-DID or `https://`) MUST render without a badge and without verification language. No surface may present a source link as proof that artifacts were built from that source. + +#### Scenario: Tangled source badged, GitHub source plain +- **WHEN** one release claims the publisher's own `sh.tangled.repo` record and another claims a GitHub URL +- **THEN** the first renders with the self-authenticated association badge and the second renders the same link treatment with no badge + +### Requirement: Associated artifacts are caveated, not conflated +Same-DID tag-matched forge artifacts that match no declared digest (typically uploaded after the release was cut) SHALL be listed on the release page as associated artifacts, visually separated from declared artifacts, with an explicit caveat that they are not present in the release record and are not covered by the publisher's signed digests. Associated artifacts MUST NOT receive the verified-download treatment or any digest badge. + +#### Scenario: Late Tangled upload appears with caveat +- **WHEN** a publisher uploads a `sh.tangled.repo.artifact` for the release's tag after the release was cut, and its bytes match no declared digest +- **THEN** the release page lists it under associated artifacts with the not-in-record caveat, clearly separated from the declared artifact list + +#### Scenario: Digest-matching upload becomes a location instead +- **WHEN** a late Tangled upload's bytes hash to a declared digest +- **THEN** it appears as an additional verified location on the declared artifact, not as an associated artifact diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/specs/publishing/spec.md b/openspec/changes/archive/2026-08-18-release-source-linking/specs/publishing/spec.md new file mode 100644 index 0000000..3139e17 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/specs/publishing/spec.md @@ -0,0 +1,21 @@ +# publishing Specification (delta) + +## ADDED Requirements + +### Requirement: Release-cut artifact discovery +When cutting a release whose `gitSource.tag` is set, the CLI SHALL query the publisher's own PDS for `sh.tangled.repo.artifact` records with a matching tag hash and offer to attach them. Attachment downloads each blob, computes its digest locally, and writes the artifact descriptor with `recordBlobStorage` referencing the Tangled record. The default is suggest-and-confirm; a flag or configuration enables non-interactive auto-include for CI. The CLI MUST NOT derive digests from anything other than the downloaded bytes. + +#### Scenario: Interactive attach at release cut +- **WHEN** a publisher cuts a release for an annotated tag that has three Tangled artifacts and confirms the suggestion +- **THEN** the release is written with three artifact descriptors whose digests were computed from locally downloaded bytes and whose storage references the Tangled records + +#### Scenario: CI auto-include +- **WHEN** a CI pipeline runs the CLI with the auto-include option enabled +- **THEN** matching Tangled artifacts attach without prompting, with identical hashing behavior + +### Requirement: Upload-then-release pipeline norm +Publisher documentation SHALL establish the pipeline ordering norm: upload forge artifacts first, cut the release second. Tooling SHALL state, at cut time, that artifacts appearing on the forge after the release is created can never join the immutable record (they surface only as caveated associated artifacts or, when digest-matching, as additional locations). + +#### Scenario: Late upload consequence is stated up front +- **WHEN** a publisher cuts a release while their CI artifact uploads are still running +- **THEN** the CLI's output makes clear which artifacts were attached and that later uploads cannot join this release diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/specs/release-records/spec.md b/openspec/changes/archive/2026-08-18-release-source-linking/specs/release-records/spec.md new file mode 100644 index 0000000..9fab658 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/specs/release-records/spec.md @@ -0,0 +1,34 @@ +# release-records Specification (delta) + +## MODIFIED Requirements + +### Requirement: Release record shape +The system SHALL define a `town.dist.release` record with TID record key, containing: `project` (at-uri to a same-DID `town.dist.project`, required), `version` (string, required), `artifacts` (array of artifact descriptors, required), and optional `channel`, `license` (SPDX identifier string), and `source` (open union declaring the source snapshot this release corresponds to; unknown variants degrade to "release with an unrecognized source declaration" — indexed and displayed, not rejected). + +#### Scenario: Release with no changelog is valid +- **WHEN** a release record is created with no `site.standard.document` referencing it +- **THEN** the release is complete and valid; release notes are optional and attach later by backlink + +#### Scenario: Release without source is valid +- **WHEN** a release record is created with no `source` +- **THEN** the release is complete, valid, and indexed identically to before this field existed + +## ADDED Requirements + +### Requirement: Git source variant +The system SHALL define a `gitSource` member of the source union containing: `commit` (required, algorithm-prefixed lowercase-hex hash of the commit object, e.g. `sha1:<40 hex>` or `sha256:<64 hex>`, following the artifact digest convention), `ref` (optional tag name, display label only), `tag` (optional algorithm-prefixed hash of the annotated tag object, which transitively pins commit, tree, and blobs), and `repo` (optional uri accepting `at://` or `https://`; when absent, the project's `source` is the claimed repository). Hashes MUST be prefixed strings, never raw bytes; new algorithms slot into the prefix without schema change. + +#### Scenario: Commit-only source is valid +- **WHEN** a release declares `gitSource` with only `commit: "sha1:ab34…"` +- **THEN** the source claim is valid; tag, ref, and repo are optional refinements + +#### Scenario: SHA-256 git repository needs no schema change +- **WHEN** a publisher's repository uses git's SHA-256 object format +- **THEN** `commit: "sha256:…"` validates under the same schema + +### Requirement: Record-blob storage variant +The system SHALL define a `recordBlobStorage` member of the artifact storage union containing `record` (required at-uri): the artifact bytes are the blob carried by the referenced atproto record, resolved per documented per-collection rules. The digest remains on the artifact descriptor per the storage-union contract; the variant provides location only. Consumers encountering a record collection with no documented resolution rule degrade as for any unknown storage variant. + +#### Scenario: Tangled artifact serves release bytes +- **WHEN** an artifact's storage is `recordBlobStorage` referencing a `sh.tangled.repo.artifact` record +- **THEN** resolution fetches that record, extracts its blob reference, and serves the bytes from the owning repo's PDS, verified against the artifact descriptor's digest as with any location diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/specs/source-linking/spec.md b/openspec/changes/archive/2026-08-18-release-source-linking/specs/source-linking/spec.md new file mode 100644 index 0000000..79889f5 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/specs/source-linking/spec.md @@ -0,0 +1,60 @@ +# source-linking Specification (delta) + +## ADDED Requirements + +### Requirement: Source is a claim of association, not build provenance +A source link SHALL be treated as the publisher's claim of which source snapshot a release corresponds to. The system MUST NOT present a source link as verification that artifacts were built from that source, and MUST NOT block or warn on releases without source links. Verification of build provenance (reproducible builds, attestation) is explicitly out of scope. + +#### Scenario: Sourceless release remains first-class +- **WHEN** a release is published with no `source` +- **THEN** it indexes, resolves, and renders identically to today, with no warning or degraded treatment + +### Requirement: Hash is truth, name is label +Where a source declaration carries both a ref name and object hashes, consumers and surfaces MUST derive identity from the hashes (`commit`, `tag`) and treat the ref name as display text only. Discovery and matching MUST key on object hashes, never on ref names. + +#### Scenario: Force-moved tag does not move the release's source +- **WHEN** a publisher force-moves tag `v1.2.3` to a different commit after cutting a release pinned to the original commit +- **THEN** the release's source claim still identifies the original commit; only the display label has drifted + +### Requirement: Project source inheritance +When a release's `gitSource` omits `repo`, the project record's `source` field SHALL be the claimed repository for that release. A release-level `repo` overrides the project's for that release only. + +#### Scenario: Single-repo project states its repo once +- **WHEN** a project's `source` is set and its releases carry `gitSource` entries with only `commit` and `ref` +- **THEN** every such release's claimed repository resolves to the project's `source` + +### Requirement: Same-DID source associations are self-authenticated +When a claimed repository is an at-uri whose authority (DID) equals the DID of the record making the claim, the association SHALL be treated as self-authenticated: both records are signed by the same key in the same repo. All other sources — cross-DID at-uris and `https://` URIs — SHALL be treated as unverified claims. Surfaces MAY badge self-authenticated associations and MUST NOT badge claims. + +#### Scenario: Publisher's own Tangled repo is self-authenticated +- **WHEN** a release under `did:plc:alice…` claims a repo at `at://did:plc:alice…/sh.tangled.repo/xyz` +- **THEN** the association is self-authenticated and may be badged + +#### Scenario: GitHub URL is a plain claim +- **WHEN** a release claims `https://github.com/alice/project` +- **THEN** the association renders as an unverified claim with no badge + +### Requirement: Tag-hash matching rule +Discovery of forge artifacts SHALL match on the annotated-tag-object hash: a `sh.tangled.repo.artifact` record matches a release when the record's `tag` bytes equal the hash encoded in the release's `gitSource.tag` (algorithm-prefixed hex ↔ raw bytes conversion). Matching MUST require the tag-object hash; ref names MUST NOT be used for discovery. Releases whose `gitSource` has no `tag` participate in no discovery. + +#### Scenario: Byte-for-byte tag match discovers artifacts +- **WHEN** a release's `gitSource.tag` is `sha1:ab34…` and a same-DID `sh.tangled.repo.artifact` carries those 20 bytes in `tag` +- **THEN** the artifact is discovered for that release + +#### Scenario: Lightweight-tag release opts out of discovery +- **WHEN** a release's `gitSource` carries only `commit` (the publisher used a lightweight tag) +- **THEN** no Tangled artifact discovery occurs for that release + +### Requirement: Authoring-time artifact discovery requires publisher attestation +New artifacts SHALL join a release only at creation time, through publisher tooling that downloads each discovered blob and computes its digest locally before writing it into the release record. Tooling MUST NOT copy integrity information from the forge's records. Discovered artifacts attach with `recordBlobStorage` referencing the forge record. + +#### Scenario: Digest is derived, not copied +- **WHEN** the CLI attaches a discovered Tangled artifact to a release being cut +- **THEN** the digest written to the release comes from hashing the downloaded blob bytes locally, and verification of that artifact thereafter needs nothing from Tangled + +### Requirement: Index-time location discovery for declared artifacts +For artifacts already declared in a release, the indexer SHALL treat a same-DID tag-matched forge blob whose bytes hash to a declared digest as an additional location for that artifact, entering the existing per-location verification lifecycle. Blobs whose bytes match no declared digest MUST NOT become locations. Release records are never modified by discovery. + +#### Scenario: Retroactive location on an older release +- **WHEN** a Tangled artifact matching a declared digest is uploaded after the release was cut +- **THEN** the artifact gains a verified location serving those bytes, with the release record untouched diff --git a/openspec/changes/archive/2026-08-18-release-source-linking/tasks.md b/openspec/changes/archive/2026-08-18-release-source-linking/tasks.md new file mode 100644 index 0000000..daf1a66 --- /dev/null +++ b/openspec/changes/archive/2026-08-18-release-source-linking/tasks.md @@ -0,0 +1,31 @@ +# Tasks: Release Source Linking + +## 1. Lexicons + +- [x] 1.1 Add the `source` open union to `town.dist.release` and the `gitSource` object to `town.dist.defs` (commit required/prefixed, optional ref/tag/repo) +- [x] 1.2 Add `recordBlobStorage` to the artifact storage union in `town.dist.defs` +- [x] 1.3 Extend view lexicons: `releaseView` (and `projectView` as needed) carry source data with authority state; settle the view shape for associated artifacts (field on `releaseView` vs. separate query) and add it +- [x] 1.4 Validate all touched lexicons + +## 2. Indexing and read plane + +- [x] 2.1 Index release `source` declarations; evaluate same-DID authority (release DID vs. source at-uri authority, with project inheritance) and hydrate views +- [x] 2.2 Implement `recordBlobStorage` resolution for `sh.tangled.repo.artifact` (fetch record → blob ref → PDS getBlob) wired into the location/health model; resolution failures degrade to location health +- [x] 2.3 Implement level-2 discovery: watch same-DID `sh.tangled.repo.artifact` records, match tag hashes against indexed releases, hash blobs against declared digests, add verified locations (marked as discovered, never surfaced before verification) +- [x] 2.4 Derive associated artifacts (tag-matched, no digest match) for the release view + +## 3. CLI + +- [x] 3.1 Populate `gitSource` at release cut from the local git checkout (commit hash; annotated tag object hash and ref name when tagging) +- [x] 3.2 Implement level-1 discovery and attach: query own PDS for tag-matched Tangled artifacts, suggest-and-confirm, download + hash locally, write descriptors with `recordBlobStorage`; add the CI auto-include flag/config +- [x] 3.3 Cut-time messaging: attached artifact summary and the late-uploads-cannot-join notice + +## 4. Web + +- [x] 4.1 Render source on release and project pages with authority-aware presentation (badge only when self-authenticated); review on the dev server before deploy per the design sign-off norm +- [x] 4.2 Render the associated-artifacts tier with the not-in-record caveat, visually separated, no verified-download treatment; same sign-off review + +## 5. Docs + +- [x] 5.1 Publisher docs: source linking, annotated vs. lightweight tags (discovery requires the tag object), upload-then-release pipeline norm +- [x] 5.2 Interop note: Tangled artifact support (what is read, what is never trusted), and the claim-only verification posture with the deferred verification-methods investigation recorded diff --git a/openspec/specs/artifact-hosting/spec.md b/openspec/specs/artifact-hosting/spec.md index 81d0bc7..d5c243e 100644 --- a/openspec/specs/artifact-hosting/spec.md +++ b/openspec/specs/artifact-hosting/spec.md @@ -66,3 +66,17 @@ The AppView SHALL continuously check artifact availability (HEAD/size), periodic - **WHEN** an artifact's only location stops responding - **THEN** the release page and read plane report the location unhealthy with a last-verified timestamp + +### Requirement: recordBlobStorage resolution +The read plane SHALL resolve `recordBlobStorage` locations by fetching the referenced record and serving its blob from the owning repo's PDS, per collection-specific rules. The first documented collection is `sh.tangled.repo.artifact` (blob in the `artifact` field). Resolution failures (record deleted, blob missing, PDS unreachable) are location health states, never release validity states. + +#### Scenario: Deleted Tangled record degrades to location health +- **WHEN** a `sh.tangled.repo.artifact` record referenced by a release's storage is deleted +- **THEN** that location reports unresolvable health (pending with the resolution failure as detail, retried on the normal cadence — never `failed`, which is reserved for bytes that hashed wrong) while the release and its other locations are unaffected + +### Requirement: Discovered locations enter the standard verification lifecycle +Locations discovered at index time (rather than declared in the release record) SHALL enter the same per-location verification lifecycle as declared locations: pending until bytes are hashed against the record digest, verified on match, never surfaced as verified before hashing. Discovered locations MUST be distinguishable from declared locations in view data. + +#### Scenario: Discovered location verifies before serving browsers +- **WHEN** the indexer discovers a same-DID Tangled blob for a declared artifact +- **THEN** browsers are redirected to it only after its bytes have been hashed against the release digest and the location is verified diff --git a/openspec/specs/consumer-surfaces/spec.md b/openspec/specs/consumer-surfaces/spec.md index 0aa4d1c..b06c4c1 100644 --- a/openspec/specs/consumer-surfaces/spec.md +++ b/openspec/specs/consumer-surfaces/spec.md @@ -154,3 +154,21 @@ When a changelog document offers neither rendered HTML nor plaintext, the change - **WHEN** a release's changelog is a document written in an unsupported format - **THEN** the surface shows its title and tags with a quiet line linking out to the canonical page, on both the stream and the release page + +### Requirement: Source rendering respects authority +Release and project pages SHALL display the source link when present: repo (resolved through project inheritance when the release omits it), ref name, and pinned hashes. A self-authenticated association (same-DID at-uri source) MAY carry a badge stating that the association is self-authenticated; unverified claims (cross-DID or `https://`) MUST render without a badge and without verification language. No surface may present a source link as proof that artifacts were built from that source. + +#### Scenario: Tangled source badged, GitHub source plain +- **WHEN** one release claims the publisher's own `sh.tangled.repo` record and another claims a GitHub URL +- **THEN** the first renders with the self-authenticated association badge and the second renders the same link treatment with no badge + +### Requirement: Associated artifacts are caveated, not conflated +Same-DID tag-matched forge artifacts that match no declared digest (typically uploaded after the release was cut) SHALL be listed on the release page as associated artifacts, visually separated from declared artifacts, with an explicit caveat that they are not present in the release record and are not covered by the publisher's signed digests. Associated artifacts MUST NOT receive the verified-download treatment or any digest badge. + +#### Scenario: Late Tangled upload appears with caveat +- **WHEN** a publisher uploads a `sh.tangled.repo.artifact` for the release's tag after the release was cut, and its bytes match no declared digest +- **THEN** the release page lists it under associated artifacts with the not-in-record caveat, clearly separated from the declared artifact list + +#### Scenario: Digest-matching upload becomes a location instead +- **WHEN** a late Tangled upload's bytes hash to a declared digest +- **THEN** it appears as an additional verified location on the declared artifact, not as an associated artifact diff --git a/openspec/specs/publishing/spec.md b/openspec/specs/publishing/spec.md index 095de22..7180414 100644 --- a/openspec/specs/publishing/spec.md +++ b/openspec/specs/publishing/spec.md @@ -49,3 +49,21 @@ The broker's `publishRelease` SHALL be idempotent on (project, version): retryin - **WHEN** a publisher yanks `1.2.3` and directs `latest` to `1.2.2` - **THEN** the status record and pointer move land in a single commit, leaving no window where `latest` targets a yanked release + +### Requirement: Release-cut artifact discovery +When cutting a release whose `gitSource.tag` is set, the CLI SHALL query the publisher's own PDS for `sh.tangled.repo.artifact` records with a matching tag hash and offer to attach them. Attachment downloads each blob, computes its digest locally, and writes the artifact descriptor with `recordBlobStorage` referencing the Tangled record. The default is suggest-and-confirm; a flag or configuration enables non-interactive auto-include for CI. The CLI MUST NOT derive digests from anything other than the downloaded bytes. + +#### Scenario: Interactive attach at release cut +- **WHEN** a publisher cuts a release for an annotated tag that has three Tangled artifacts and confirms the suggestion +- **THEN** the release is written with three artifact descriptors whose digests were computed from locally downloaded bytes and whose storage references the Tangled records + +#### Scenario: CI auto-include +- **WHEN** a CI pipeline runs the CLI with the auto-include option enabled +- **THEN** matching Tangled artifacts attach without prompting, with identical hashing behavior + +### Requirement: Upload-then-release pipeline norm +Publisher documentation SHALL establish the pipeline ordering norm: upload forge artifacts first, cut the release second. Tooling SHALL state, at cut time, that artifacts appearing on the forge after the release is created can never join the immutable record (they surface only as caveated associated artifacts or, when digest-matching, as additional locations). + +#### Scenario: Late upload consequence is stated up front +- **WHEN** a publisher cuts a release while their CI artifact uploads are still running +- **THEN** the CLI's output makes clear which artifacts were attached and that later uploads cannot join this release diff --git a/openspec/specs/release-records/spec.md b/openspec/specs/release-records/spec.md index f0789a5..56d74c5 100644 --- a/openspec/specs/release-records/spec.md +++ b/openspec/specs/release-records/spec.md @@ -45,12 +45,34 @@ Renaming a project SHALL be performed by creating a new project record under the - **THEN** consumers and the AppView resolve through the alias to the new project record, and the release remains valid ### Requirement: Release record shape -The system SHALL define a `town.dist.release` record with TID record key, containing: `project` (at-uri to a same-DID `town.dist.project`, required), `version` (string, required), `artifacts` (array of artifact descriptors, required), and optional `channel` and `license` (SPDX identifier string). +The system SHALL define a `town.dist.release` record with TID record key, containing: `project` (at-uri to a same-DID `town.dist.project`, required), `version` (string, required), `artifacts` (array of artifact descriptors, required), and optional `channel`, `license` (SPDX identifier string), and `source` (open union declaring the source snapshot this release corresponds to; unknown variants degrade to "release with an unrecognized source declaration" — indexed and displayed, not rejected). #### Scenario: Release with no changelog is valid - **WHEN** a release record is created with no `site.standard.document` referencing it - **THEN** the release is complete and valid; release notes are optional and attach later by backlink +#### Scenario: Release without source is valid +- **WHEN** a release record is created with no `source` +- **THEN** the release is complete, valid, and indexed identically to before this field existed + +### Requirement: Git source variant +The system SHALL define a `gitSource` member of the source union containing: `commit` (required, algorithm-prefixed lowercase-hex hash of the commit object, e.g. `sha1:<40 hex>` or `sha256:<64 hex>`, following the artifact digest convention), `ref` (optional tag name, display label only), `tag` (optional algorithm-prefixed hash of the annotated tag object, which transitively pins commit, tree, and blobs), and `repo` (optional uri accepting `at://` or `https://`; when absent, the project's `source` is the claimed repository). Hashes MUST be prefixed strings, never raw bytes; new algorithms slot into the prefix without schema change. + +#### Scenario: Commit-only source is valid +- **WHEN** a release declares `gitSource` with only `commit: "sha1:ab34…"` +- **THEN** the source claim is valid; tag, ref, and repo are optional refinements + +#### Scenario: SHA-256 git repository needs no schema change +- **WHEN** a publisher's repository uses git's SHA-256 object format +- **THEN** `commit: "sha256:…"` validates under the same schema + +### Requirement: Record-blob storage variant +The system SHALL define a `recordBlobStorage` member of the artifact storage union containing `record` (required at-uri): the artifact bytes are the blob carried by the referenced atproto record, resolved per documented per-collection rules. The digest remains on the artifact descriptor per the storage-union contract; the variant provides location only. Consumers encountering a record collection with no documented resolution rule degrade as for any unknown storage variant. + +#### Scenario: Tangled artifact serves release bytes +- **WHEN** an artifact's storage is `recordBlobStorage` referencing a `sh.tangled.repo.artifact` record +- **THEN** resolution fetches that record, extracts its blob reference, and serves the bytes from the owning repo's PDS, verified against the artifact descriptor's digest as with any location + ### Requirement: Version strings are opaque Version strings SHALL be treated as opaque identifiers. The system MUST NOT require or assume any versioning scheme (semver, ChronVer, and arbitrary strings are all valid), MUST NOT derive ordering from version strings, and MUST use record creation order (TID) wherever ordering is needed. diff --git a/openspec/specs/source-linking/spec.md b/openspec/specs/source-linking/spec.md new file mode 100644 index 0000000..0e447e7 --- /dev/null +++ b/openspec/specs/source-linking/spec.md @@ -0,0 +1,63 @@ +# source-linking Specification + +## Purpose +Link releases and projects to the source snapshots they correspond to — git commits and annotated tags, across forges, Tangled first — as publisher claims with structurally checkable same-DID authority, plus the two-level discovery contract for forge artifacts. Created by archiving change release-source-linking. + +## Requirements + +### Requirement: Source is a claim of association, not build provenance +A source link SHALL be treated as the publisher's claim of which source snapshot a release corresponds to. The system MUST NOT present a source link as verification that artifacts were built from that source, and MUST NOT block or warn on releases without source links. Verification of build provenance (reproducible builds, attestation) is explicitly out of scope. + +#### Scenario: Sourceless release remains first-class +- **WHEN** a release is published with no `source` +- **THEN** it indexes, resolves, and renders identically to today, with no warning or degraded treatment + +### Requirement: Hash is truth, name is label +Where a source declaration carries both a ref name and object hashes, consumers and surfaces MUST derive identity from the hashes (`commit`, `tag`) and treat the ref name as display text only. Discovery and matching MUST key on object hashes, never on ref names. + +#### Scenario: Force-moved tag does not move the release's source +- **WHEN** a publisher force-moves tag `v1.2.3` to a different commit after cutting a release pinned to the original commit +- **THEN** the release's source claim still identifies the original commit; only the display label has drifted + +### Requirement: Project source inheritance +When a release's `gitSource` omits `repo`, the project record's `source` field SHALL be the claimed repository for that release. A release-level `repo` overrides the project's for that release only. + +#### Scenario: Single-repo project states its repo once +- **WHEN** a project's `source` is set and its releases carry `gitSource` entries with only `commit` and `ref` +- **THEN** every such release's claimed repository resolves to the project's `source` + +### Requirement: Same-DID source associations are self-authenticated +When a claimed repository is an at-uri whose authority (DID) equals the DID of the record making the claim, the association SHALL be treated as self-authenticated: both records are signed by the same key in the same repo. All other sources — cross-DID at-uris and `https://` URIs — SHALL be treated as unverified claims. Surfaces MAY badge self-authenticated associations and MUST NOT badge claims. + +#### Scenario: Publisher's own Tangled repo is self-authenticated +- **WHEN** a release under `did:plc:alice…` claims a repo at `at://did:plc:alice…/sh.tangled.repo/xyz` +- **THEN** the association is self-authenticated and may be badged + +#### Scenario: GitHub URL is a plain claim +- **WHEN** a release claims `https://github.com/alice/project` +- **THEN** the association renders as an unverified claim with no badge + +### Requirement: Tag-hash matching rule +Discovery of forge artifacts SHALL match on the annotated-tag-object hash: a `sh.tangled.repo.artifact` record matches a release when the record's `tag` bytes equal the hash encoded in the release's `gitSource.tag` (algorithm-prefixed hex ↔ raw bytes conversion). Matching MUST require the tag-object hash; ref names MUST NOT be used for discovery. Releases whose `gitSource` has no `tag` participate in no discovery. + +#### Scenario: Byte-for-byte tag match discovers artifacts +- **WHEN** a release's `gitSource.tag` is `sha1:ab34…` and a same-DID `sh.tangled.repo.artifact` carries those 20 bytes in `tag` +- **THEN** the artifact is discovered for that release + +#### Scenario: Lightweight-tag release opts out of discovery +- **WHEN** a release's `gitSource` carries only `commit` (the publisher used a lightweight tag) +- **THEN** no Tangled artifact discovery occurs for that release + +### Requirement: Authoring-time artifact discovery requires publisher attestation +New artifacts SHALL join a release only at creation time, through publisher tooling that downloads each discovered blob and computes its digest locally before writing it into the release record. Tooling MUST NOT copy integrity information from the forge's records. Discovered artifacts attach with `recordBlobStorage` referencing the forge record. + +#### Scenario: Digest is derived, not copied +- **WHEN** the CLI attaches a discovered Tangled artifact to a release being cut +- **THEN** the digest written to the release comes from hashing the downloaded blob bytes locally, and verification of that artifact thereafter needs nothing from Tangled + +### Requirement: Index-time location discovery for declared artifacts +For artifacts already declared in a release, the indexer SHALL treat a same-DID tag-matched forge blob whose bytes hash to a declared digest as an additional location for that artifact, entering the existing per-location verification lifecycle. Blobs whose bytes match no declared digest MUST NOT become locations. Release records are never modified by discovery. + +#### Scenario: Retroactive location on an older release +- **WHEN** a Tangled artifact matching a declared digest is uploaded after the release was cut +- **THEN** the artifact gains a verified location serving those bytes, with the release record untouched