diff --git a/.vscode/extensions.json b/.vscode/extensions.json deleted file mode 100644 index af3c0b2..0000000 --- a/.vscode/extensions.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "recommendations": [ - "Webnative.webnative" - ] -} diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..462d8bf --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,327 @@ +--- +title: Twisted product and implementation specification +status: ready +--- + +# Twisted roadmap + +## Objective + +Twisted is a read-only Tangled client with its own responsive interface. It +runs as a website, installable PWA, and Capacitor app for Android and iOS. Users +can browse the full public Tangled dataset exposed by Bobbin without signing in +or sending writes to a PDS or knot. + +The first release must make Tangled useful as a reading and discovery tool. It +includes repository hosting data, social records, collaboration records, +pipelines, labels, strings, and infrastructure views. Mobile packaging follows +the complete web/PWA experience; it does not reduce the product scope. + +## Success criteria + +- A user can resolve a handle or DID, open a profile, and navigate all public + records associated with that actor. +- Search supports text, collection, author, repo, and date filters with cursor + pagination. +- Repository pages expose metadata, source, refs, commits, diffs, downloads, + issues, pulls, discussions, activity, labels, collaborators, CI, and + artifacts. +- Dedicated views cover follows, vouches, stars, actor-authored activity, + strings, knots, spindles, members, public keys, and other public + infrastructure data exposed by Bobbin. +- Every API response uses atcute's generated types and schemas. Local types are + limited to Bobbin-specific contracts that atcute does not publish. +- Loading, empty, incomplete-index, rate-limit, not-found, offline, and upstream + failure states keep navigation usable and offer the right recovery action. +- The UI works with keyboard navigation and screen readers, honors reduced + motion and safe areas, and meets WCAG 2.2 AA for application-owned colors and + controls. +- Users can choose and persist a Base16 theme. The same theme works in light and + dark system chrome, PWA mode, Android, and iOS. +- The PWA is installable and its application shell opens offline. Public + Tangled data is presented as online data unless a later cache has a clear + freshness policy. +- Unit, component, Cypress, and Hurl checks pass. Hurl verifies the live Bobbin + contract against the approved `desertthunder.dev` fixture. +- Android and iOS builds launch, navigate, and render the same read-only flows + as the PWA. + +## Current state + +The repository is an Ionic Vue starter using Vue 3, TypeScript, Vite 5, +Vue Router, Ionic 8, Capacitor 8, Vitest, Cypress, and ESLint. It contains one +placeholder page, no API client, no product navigation, and no meaningful test +coverage. Capacitor has a starter app identifier and no checked-in Android or +iOS project. + +The API contract is documented in [docs/api.md](docs/api.md). Bobbin is a new, +read-only XRPC AppView whose hosted instance is `https://api.tangled.org`. + +## Users and core flows + +Twisted is for people who want to inspect Tangled on desktop or mobile without +logging in. A user can: + +1. Search public Tangled records or enter a handle, DID, repo DID, or AT-URI. +2. Open a profile and browse its repositories, social graph, activity, public + keys, knot memberships, and spindle memberships. +3. Open a repository and move among its overview, source, history, issues, + pulls, CI, labels, collaborators, and activity. +4. Follow links among authors, subjects, state records, comments, reactions, + pipelines, artifacts, knots, spindles, and strings without losing context. +5. Copy identifiers, open the canonical Tangled page, or download a repository + archive. Actions that change data remain on Tangled. + +## Product scope + +### Discovery and identity + +- Resolve handles and DIDs through Bobbin's typed Microcosm identity query. +- Accept complete Tangled and AT-URI links and route them to the matching view. +- Provide full-text search with every Bobbin filter and cursor pagination. +- Group mixed search hits by recognizable record type while preserving Bobbin's + relevance order. +- Show Bobbin coverage when indexed results may be incomplete. + +### Profiles and actor activity + +- Render profile metadata, links, avatar, location, pronouns, pinned repos, and + validated handle/DID information. +- List owned repositories, stars, follows, followers, vouches, and inbound + vouches. +- Show actor-authored comments, reactions, issues, pulls, state changes, ref + updates, collaborator records, label operations, pipelines, statuses, + artifacts, and knot or spindle memberships through the `*By` endpoints. +- Use separate paginated sections so one failed endpoint does not blank the + whole profile. + +### Repositories and Git data + +- Render repository metadata, topics, links, knot, spindle, labels, star count, + collaborators, languages, and latest ref activity. +- Render sanitized Markdown README content when the tree response provides it. +- Browse branches, tags, trees, files, and commit history. Preserve the selected + ref while moving through directories. +- Render text blobs with line numbers, anchors, wrapping controls, and a safe + size limit. Offer download or canonical Tangled links for binary and oversized + blobs. +- Show commits, path-filtered logs, single-ref diffs, and two-revision compares. +- Download streamed repository archives without loading the full archive into + application memory. + +### Collaboration and social records + +- List and filter issues by current state and author; render issue details, + state history, comments, reactions, labels, references, and mentions. +- List and filter pulls by current status and author; render pull details, + status history, comments, reactions, patch information, and comparison data. +- Render comments and reactions on issues, pulls, and strings using canonical + `sh.tangled.feed.*` records while accepting documented legacy aliases. +- Display stars, follows, vouches, collaborators, and ref updates with links to + both ends of each relationship. +- Display rkeys as canonical issue and pull identifiers. Do not invent + sequential numbers Bobbin does not supply. + +### Pipelines, labels, strings, and infrastructure + +- List pipelines for repos and spindles, their status histories, and artifacts. +- Display artifact metadata and stream downloads where the upstream response + permits it. +- Display label definitions and label operation history for their scopes. +- Render Tangled strings and their comments or reactions. +- Browse public knots and spindles, their owners, versions, keys, and membership + records. Clearly label data whose freshness Bobbin cannot report. +- Display public keys and service diagnostics without exposing management + controls. + +### Theme and platform behavior + +- Represent each Base16 scheme as the sixteen canonical `base00` through + `base0F` colors plus a stable id and display name. +- Map Base16 colors to semantic application and Ionic CSS custom properties. + Components consume semantic tokens rather than palette slots. +- Ship a small set of reviewed light and dark schemes, support user-imported + Base16 JSON, validate imports, and persist the selected scheme locally. +- Fall back to a reviewed default when persisted data is absent or invalid. +- Use responsive Ionic layouts, native safe-area insets, platform back behavior, + and touch targets of at least 44 by 44 CSS pixels. +- Install as a PWA with manifest metadata and an offline application shell. +- Package the completed PWA for Android first and iOS second with a final app id, + name, icons, splash assets, status-bar treatment, and external-link policy. + +## Information architecture + +The router must use stable, shareable URLs and encode opaque identifiers safely. +Routes should cover: + +- discovery and search; +- profiles by handle or DID; +- repository overview by repo record AT-URI; +- source tree, blob, commits, branches, tags, diff, and compare; +- issue and pull lists and details; +- pipelines and artifacts; +- actor activity and relationship views; +- strings, knots, spindles, labels, and public-key diagnostics; +- settings for theme, API service, and application information. + +Keep query filters and cursors in route query parameters when doing so produces +a useful shareable URL. Do not expose opaque cursors as human identifiers. + +## Technical plan + +### API and types + +- Add `@atcute/client` `5.1.1`, `@atcute/tangled` `2.0.13`, + `@atcute/microcosm` `2.0.2`, and an explicit compatible + `@atcute/lexicons` dependency. +- Register Tangled and Microcosm ambient XRPC declarations once. +- Put the atcute client, endpoint wrappers, runtime validation, error mapping, + abort handling, and configuration in one API boundary. Views must not issue + raw `fetch` calls. +- Validate every embedded `value: unknown` with its generated atcute schema + before mapping it for display. +- Keep a local type for `sh.tangled.bobbin.getCoverage` until atcute publishes + the lexicon. Use small typed parameter overlays for Bobbin's documented knot + proxy differences instead of copying generated response models. +- Default to `https://api.tangled.org`; allow an advanced user to set another + HTTPS Bobbin instance and restore the default. + +### Application structure and state + +- Organize code by domain: API boundary, shared record views, features, router, + theme, and platform integration. +- Use Vue composables and component-local state. Add a global state library only + if implementation proves that shared state cannot stay simple. +- Cache successful GET responses in memory by NSID plus normalized parameters. + Deduplicate concurrent requests and use short, explicit stale times. Never + persist API payloads until the product defines cache invalidation and privacy + behavior. +- Each request accepts an `AbortSignal`. Route changes cancel obsolete work. +- Cursor lists append pages without changing earlier item order and prevent + duplicate requests for the same cursor. + +### Content safety + +- Render Markdown through `marked` `18.0.7`, sanitize the result with DOMPurify + `3.4.12`, and restrict URL schemes before inserting HTML. +- Treat repository blobs as untrusted text or bytes. Never execute HTML, + JavaScript, SVG scripts, or repository-provided styles. +- Open external links with an explicit external-browser policy and safe opener + settings. Encode identifiers when constructing routes and query strings. + +### PWA and native apps + +- Add `vite-plugin-pwa` `1.3.0` for manifest generation, installation, update + handling, and application-shell caching. +- Do not use a service worker to hide Bobbin freshness or failure states. +- Add `@capacitor/android` and `@capacitor/ios` versions compatible with the + existing Capacitor 8 packages. Keep web behavior as the source of truth. +- Native builds need no authentication, secrets, background jobs, analytics, + or push notifications. + +## Testing plan + +### Stable boundaries + +- Unit tests cover identifier parsing, endpoint mapping, embedded-record + validation, pagination, caching, error classification, Markdown safety, and + Base16 validation or token mapping. +- Component tests mount each route-level state against a fake API boundary. + They cover success, empty, partial, loading, malformed record, and retryable + failure states without calling the network. +- Cypress covers user-visible journeys against deterministic intercepted XRPC + fixtures: search to profile, profile to repo, source browsing, issue and pull + reading, activity or infrastructure navigation, theme persistence, and PWA + routing. +- [Hurl](https://github.com/Orange-OpenSource/hurl) `8.0.1` is the live contract + boundary. It checks Bobbin coverage, identity + resolution, profile lookup, repo listing and lookup, search, one aggregation, + and one knot-proxied Git query. + +### Hurl fixture policy + +Use this approved public fixture: + +```text +handle=desertthunder.dev +actor_did=did:plc:xg2vq45muivyy3xwatcehspu +profile_uri=at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.actor.profile/self +repo_uri=at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.repo/3mho6hukiei22 +repo_did=did:plc:4iw5fospv2asv3344au236ka +``` + +Hurl asserts status, content type, stable identifiers, `$type`, and response +shape. It must not assert mutable CIDs, counts, descriptions, dates, branch +names, or list contents. Run files sequentially with a delay and bounded retry +to respect Bobbin's hosted rate limit. A live-data failure blocks release only +after a maintainer distinguishes an upstream outage or fixture change from a +Twisted contract regression. + +### Commands + +```sh +npm install +npm run lint +npm run test:unit -- --run +npm run build +npm run dev +npm run test:e2e +hurl --test --jobs 1 --delay 250ms --retry 2 --retry-interval 2s tests/smoke +npx cap sync +``` + +The Cypress command requires the dev server at `http://localhost:5173`. Native +release tickets must add the exact Android and iOS build commands once those +platform projects exist. + +## Delivery milestones + +1. Establish the typed Bobbin boundary, live contract checks, theme engine, and + application shell. +2. Complete discovery, profiles, repository reading, collaboration, social, + activity, pipelines, labels, strings, and infrastructure views. +3. Add PWA installation, offline shell behavior, responsive and accessibility + review, and deterministic end-to-end coverage. +4. Package and verify Android, then iOS, without forking product behavior. + +Every capability listed in this specification has a ticket in [TODO.md](TODO.md). + +## Boundaries + +### Always + +- Keep the client read-only and unauthenticated. +- Use atcute types and generated validators for published lexicons. +- Preserve partial results and identify incomplete or stale data. +- Sanitize untrusted content and test keyboard, narrow-screen, and error states. +- Keep Git operations read-only when working in this repository. + +### Ask first + +- Add a backend, proxy, database, global state library, analytics, telemetry, + authentication, or push notifications. +- Persist Bobbin payloads or change the public route scheme. +- Use undocumented Bobbin endpoints beyond the documented compatibility + overlays. + +### Never + +- Call XRPC procedures or any endpoint that writes to a PDS or knot. +- Call `sh.tangled.repo.listSecrets`, store credentials, or expose secret + metadata. +- Render unsanitized Markdown, repository HTML, or executable SVG. +- Convert rkeys into invented sequential issue or pull numbers. + +## Risks and open questions + +- Bobbin and its lexicons are changing quickly. Package updates require a route + inventory review, generated-type compile check, and Hurl run. +- Coverage describes Hydrant ingestion but cannot prove that knot membership is + fresh. Infrastructure views must disclose this limitation. +- The public fixture will evolve. Shape-based smoke assertions and documented + fixture ownership reduce brittleness but do not remove it. +- Base16 palettes do not guarantee accessible semantic combinations. Reviewed + bundled themes must pass contrast tests; imported themes need warnings and a + recovery path. +- iOS distribution requires signing and Apple provisioning outside this + repository. The build can be verified before store publication is available. diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..74ee715 --- /dev/null +++ b/TODO.md @@ -0,0 +1,458 @@ +# Twisted implementation tickets + +These tickets implement the complete [product specification](ROADMAP.md) and +the [Bobbin API contract](docs/api.md). Work on one ticket per fresh agent +context. A ticket is complete only when its acceptance criteria and listed +checks pass. + +## Milestone 1: Foundations + +**Exit criterion:** The app has a typed, tested read boundary, stable shell, +theme engine, and live API contract checks. + +### T01 - Replace starter tests with a reliable test baseline + +**What to build:** Remove placeholder expectations and establish shared Vitest +and Cypress fixtures for route-level feature work. + +**Blocked by:** None - can start immediately + +**Acceptance criteria:** + +- [ ] Unit and component tests can mount Ionic route components without warnings. +- [ ] Cypress starts against the Vite app and intercepts XRPC requests. +- [ ] Starter copy and example fixtures are removed. + +**Verification:** `npm run test:unit -- --run && npm run lint` + +### T02 - Add the typed Bobbin client boundary + +**What to build:** Add atcute dependencies and one configurable XRPC boundary +with runtime record validation, abort support, pagination, and typed errors. + +**Blocked by:** T01 + +**Acceptance criteria:** + +- [ ] Tangled and Microcosm ambient declarations type every published query. +- [ ] Embedded records are validated with generated atcute schemas before use. +- [ ] Coverage and knot-proxy differences use narrow local overlays; views use no raw `fetch`. +- [ ] Unit tests cover validation, malformed records, aborts, and XRPC error mapping. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T03 - Add live Bobbin smoke tests with Hurl + +**What to build:** Create a read-only Hurl suite using the approved +`desertthunder.dev` fixture and document how to run it. + +**Blocked by:** None - can start immediately + +**Acceptance criteria:** + +- [ ] Tests cover coverage, identity, profile, repo lookup/listing, search, one aggregation, and one Git proxy query. +- [ ] Assertions use stable identifiers and shapes, never CIDs, counts, branch names, or mutable prose. +- [ ] Tests run sequentially with delay and bounded retry to respect the hosted rate limit. +- [ ] `package.json` exposes a `test:smoke` script without installing Hurl as a JavaScript dependency. + +**Verification:** `npm run test:smoke` + +### T04 - Build the Base16 theme engine + +**What to build:** Map validated Base16 schemes to semantic Ionic and app CSS +tokens, with bundled defaults, JSON import, selection, and persistence. + +**Blocked by:** T01 + +**Acceptance criteria:** + +- [ ] Invalid or incomplete schemes cannot replace the active theme. +- [ ] A reviewed default recovers from absent or corrupt persisted data. +- [ ] Bundled light and dark schemes meet WCAG AA for application-owned controls. +- [ ] Unit tests cover parsing, token mapping, persistence, and recovery. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T05 - Replace the starter screen with the application shell + +**What to build:** Add responsive navigation, route layouts, settings, back +behavior, safe areas, and stable deep-link routing for every specified domain. + +**Blocked by:** T01, T04 + +**Acceptance criteria:** + +- [ ] Desktop, narrow web, standalone PWA, and native-sized layouts remain usable. +- [ ] Routes safely encode handles, DIDs, AT-URIs, refs, and paths. +- [ ] Unknown routes and unsupported identifiers have useful recovery links. +- [ ] Theme and API-service settings persist independently. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T06 - Add coverage, caching, and shared request states + +**What to build:** Add in-memory request deduplication, explicit stale times, +cursor guards, coverage notices, and shared loading/error/empty components. + +**Blocked by:** T02, T05 + +**Acceptance criteria:** + +- [ ] Route changes abort obsolete work and concurrent identical calls share one request. +- [ ] `400`, `404`, `429`, `502`, `503`, offline, and malformed-data states remain distinct. +- [ ] Retryable errors preserve existing content and respect `Retry-After` when present. +- [ ] `ready: false` keeps partial indexed results visible with an explanation. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T07 - Render Markdown and record links safely + +**What to build:** Add sanitized Markdown, safe link handling, common record +headers, identifier copying, and canonical Tangled links. + +**Blocked by:** T01 + +**Acceptance criteria:** + +- [ ] Markdown uses Marked and DOMPurify with restricted URL schemes. +- [ ] Scriptable HTML, SVG, styles, and unsafe links are removed in security tests. +- [ ] Mentions, references, AT-URIs, DIDs, and known Tangled URLs link to local views. +- [ ] Unknown record types remain inspectable without unsafe generic HTML. + +**Verification:** `npm run test:unit -- --run && npm run build` + +## Milestone 2: Complete read-only client + +**Exit criterion:** Every public, user-relevant Bobbin record and query family +in the specification has a navigable read view. + +### T08 - Build discovery and filtered search + +**What to build:** Add identifier entry, mixed full-text results, every Bobbin +search filter, coverage state, and cursor pagination. + +**Blocked by:** T06, T07 + +**Acceptance criteria:** + +- [ ] Handle, DID, repo DID, AT-URI, and Tangled URL input routes correctly. +- [ ] Search preserves relevance order and supports NSID, author, repo, and date filters. +- [ ] Empty, invalid-filter, incomplete-index, and next-page states are tested. +- [ ] Search result records are schema-validated before rendering. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T09 - Build complete profile pages + +**What to build:** Resolve handle or DID and render profile metadata, pinned +repositories, owned repositories, links, and independently loading sections. + +**Blocked by:** T06, T07 + +**Acceptance criteria:** + +- [ ] Bidirectionally resolved handle, DID, profile fields, avatar, and links render safely. +- [ ] Pinned repo DIDs resolve without losing their configured order. +- [ ] Repository pagination and per-section failures do not blank the profile. +- [ ] The `desertthunder.dev` fixture works in a manual development check. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T10 - Build actor activity dashboards + +**What to build:** Expose every actor-oriented `*By` query as typed, paginated +activity sections linked to their subject records. + +**Blocked by:** T09 + +**Acceptance criteria:** + +- [ ] Comments, reactions, stars, issues, pulls, states, statuses, and ref updates are covered. +- [ ] Collaborator, label, pipeline, artifact, knot, and spindle authorings are covered. +- [ ] Filters supported by issue and pull `*By` endpoints are available. +- [ ] One section's failure does not block other activity sections. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T11 - Build repository overview pages + +**What to build:** Render repository identity, topics, links, knot, spindle, +languages, README, labels, counts, collaborators, and recent ref activity. + +**Blocked by:** T06, T07 + +**Acceptance criteria:** + +- [ ] Repo record AT-URI and repo DID remain visibly distinct and copyable. +- [ ] README Markdown is sanitized and absent README data has a compact empty state. +- [ ] Overview sections load independently and link to their full views. +- [ ] Canonical Tangled and clone/download actions are unambiguous and read-only. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T12 - Browse repository trees and blobs + +**What to build:** Add ref-aware directory navigation and safe text, binary, +large-file, and missing-blob views. + +**Blocked by:** T11 + +**Acceptance criteria:** + +- [ ] Directory navigation preserves ref and path in a shareable URL. +- [ ] Text has line numbers, anchors, wrap controls, and a tested size limit. +- [ ] Binary and oversized files offer download or canonical links without unsafe rendering. +- [ ] Tree and blob upstream failures retain repository navigation. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T13 - Browse branches, tags, commits, and ref updates + +**What to build:** Add paginated refs and commit history, path-filtered logs, +commit details, and ref-update records. + +**Blocked by:** T11 + +**Acceptance criteria:** + +- [ ] Branch and tag selection updates source and history routes. +- [ ] Commit cursors append once and preserve order. +- [ ] Author, time, hash, message, and changed-ref links remain readable on narrow screens. +- [ ] Empty repositories and missing refs have explicit states. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T14 - Render diffs, compares, and archive downloads + +**What to build:** Add single-ref diffs, two-revision comparison, readable patch +output, and streamed archive downloads. + +**Blocked by:** T11 + +**Acceptance criteria:** + +- [ ] Refs are encoded safely and comparison direction is clear. +- [ ] Large diffs degrade to download or canonical links without locking the UI. +- [ ] Archive bodies stream to download and are never buffered as app state. +- [ ] Range, cache, filename, and content-type metadata survive the API boundary. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T15 - Read issue lists and details + +**What to build:** Add repo issue filters, pagination, detail pages, derived +state, state history, labels, mentions, and references. + +**Blocked by:** T11 + +**Acceptance criteria:** + +- [ ] State and author filters round-trip through the URL. +- [ ] Lists use Bobbin's derived state and comment count. +- [ ] Details use rkeys as identifiers and never invent sequential numbers. +- [ ] State history names its author and timestamp when available. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T16 - Read pull lists and details + +**What to build:** Add repo pull filters, pagination, detail pages, derived +status, status history, target/source data, and patch links. + +**Blocked by:** T11 + +**Acceptance criteria:** + +- [ ] Open, closed, and merged filters round-trip through the URL. +- [ ] Lists use Bobbin's derived status and comment count. +- [ ] Pull rkeys remain the displayed identifiers. +- [ ] Source, target, comparison, and status-author links resolve correctly. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T17 - Read comments and reactions + +**What to build:** Add paginated discussion and reaction views for issues, +pulls, strings, and other valid subjects. + +**Blocked by:** T15, T16 + +**Acceptance criteria:** + +- [ ] Canonical feed records and documented legacy issue/pull comment aliases render. +- [ ] Comments use sanitized markup and link authors, mentions, and references. +- [ ] Reaction summaries and full actor lists agree with count/list endpoints. +- [ ] Independent pagination and failure handling work on each thread. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T18 - Read stars, follows, vouches, and collaborators + +**What to build:** Add relationship views for repo stars, actor follows and +vouches, and repo collaborators, with links to both ends of each edge. + +**Blocked by:** T09, T11 + +**Acceptance criteria:** + +- [ ] Inbound and actor-authored directions have distinct labels. +- [ ] Counts, distinct-author counts, lists, and cursors render consistently. +- [ ] Missing profiles or repos do not hide the relationship record. +- [ ] Profile and repo summaries link to the full relationship views. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T19 - Read pipelines, statuses, and artifacts + +**What to build:** Add repo/spindle pipeline lists, pipeline details, status +history, artifact metadata, and safe artifact downloads. + +**Blocked by:** T11 + +**Acceptance criteria:** + +- [ ] Repo and spindle subject forms are validated and labeled. +- [ ] Pipeline status history links actors and related records. +- [ ] Artifact downloads preserve upstream content metadata and avoid buffering large bodies. +- [ ] Empty, running, failed, unavailable, and incomplete-index states are covered. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T20 - Read labels and Tangled strings + +**What to build:** Add label definition/operation views and string detail, +listing, comment, and reaction flows. + +**Blocked by:** T07, T11 + +**Acceptance criteria:** + +- [ ] Label scope, definition, operation author, target, and history are inspectable. +- [ ] Repo label links open their definition when available. +- [ ] Strings render safely and expose their valid discussion subjects. +- [ ] Arbitrary scope identifiers are encoded and validated at the API boundary. + +**Verification:** `npm run test:unit -- --run && npm run build` + +### T21 - Read knots, spindles, memberships, and public keys + +**What to build:** Add public infrastructure views for owned services, members, +owners, versions, knot keys, actor public keys, and freshness limitations. + +**Blocked by:** T09, T11 + +**Acceptance criteria:** + +- [ ] Knot and spindle lists and memberships support every documented direction. +- [ ] Bobbin's extra knot proxy parameter uses a narrow typed overlay. +- [ ] Owners, versions, service keys, and actor public keys have copyable identifiers. +- [ ] Views disclose that Bobbin coverage does not prove knot-roster freshness. +- [ ] No secret-list or management endpoint is called. + +**Verification:** `npm run test:unit -- --run && npm run build` + +## Milestone 3: Web and PWA release quality + +**Exit criterion:** The complete web client is installable, accessible, +responsive, deterministic under test, and honest about offline or stale data. + +### T22 - Make Twisted an installable PWA + +**What to build:** Add manifest, install/update handling, icons, theme colors, +and an offline application shell with explicit online-data states. + +**Blocked by:** T05 + +**Acceptance criteria:** + +- [ ] Browser install checks pass with the final app identity and assets. +- [ ] A previously loaded shell opens offline and identifies unavailable live data. +- [ ] Service-worker updates never strand the app on mixed asset versions. +- [ ] API payloads are not persisted by the service worker. + +**Verification:** `npm run build` and a browser PWA audit + +### T23 - Cover complete user journeys in Cypress + +**What to build:** Add intercepted end-to-end journeys for discovery, profiles, +repos, source, collaboration, activity, infrastructure, settings, and failures. + +**Blocked by:** T08-T21 + +**Acceptance criteria:** + +- [ ] Tests never depend on the live Bobbin service. +- [ ] Fixtures cover success, partial, empty, invalid-record, offline, rate-limit, and upstream errors. +- [ ] Theme persistence, deep links, pagination guards, and back navigation are covered. +- [ ] At least one narrow viewport exercises every primary route family. + +**Verification:** Start `npm run dev`, then run `npm run test:e2e` + +### T24 - Complete accessibility and responsive review + +**What to build:** Audit every route family for keyboard, screen-reader, +contrast, zoom, reduced motion, touch targets, safe areas, and long content. + +**Blocked by:** T08-T23 + +**Acceptance criteria:** + +- [ ] All interactive elements have an accessible name, focus state, and logical order. +- [ ] Status changes and appended pages are announced without stealing focus. +- [ ] Reviewed themes meet WCAG 2.2 AA and imported low-contrast themes warn the user. +- [ ] Layout works at 320 CSS pixels, 200% zoom, and common tablet/desktop widths. + +**Verification:** `npm run test:unit -- --run && npm run test:e2e` plus manual assistive-technology review + +## Milestone 4: Native apps + +**Exit criterion:** Android and iOS packages launch the complete client and pass +platform smoke checks without product-specific forks. + +### T25 - Package and verify Android + +**What to build:** Add the Capacitor Android project, final identity/assets, +safe-area/status-bar behavior, external links, downloads, and back navigation. + +**Blocked by:** T22, T24 + +**Acceptance criteria:** + +- [ ] Debug build installs and launches on a supported emulator or device. +- [ ] Search, profile, repo, source, issue/pull, theme, and offline-shell smoke checks pass. +- [ ] External URLs and streamed downloads use an explicit safe platform flow. +- [ ] Native code contains no API secrets or feature fork. + +**Verification:** `npm run build && npx cap sync android` plus the documented Android debug build + +### T26 - Package and verify iOS + +**What to build:** Add the Capacitor iOS project with the same product behavior, +final assets, safe areas, status bar, external links, and downloads. + +**Blocked by:** T25 + +**Acceptance criteria:** + +- [ ] Simulator build launches on a supported iOS runtime. +- [ ] The Android smoke flows pass on iOS without platform-specific feature gaps. +- [ ] Rotation, dynamic type, safe areas, external links, and downloads behave correctly. +- [ ] Signing and provisioning requirements are documented without committing credentials. + +**Verification:** `npm run build && npx cap sync ios` plus the documented iOS simulator build + +### T27 - Run the release verification matrix + +**What to build:** Verify the complete contract and user experience across web, +PWA, Android, and iOS and resolve release-blocking regressions. + +**Blocked by:** T03, T23, T24, T25, T26 + +**Acceptance criteria:** + +- [ ] Lint, unit/component, build, Cypress, and Hurl checks pass. +- [ ] A maintainer reviews live-test failures for fixture drift or upstream outage before classifying them. +- [ ] Manual checks cover installation, update, offline shell, theme recovery, downloads, and canonical links. +- [ ] Documentation matches the shipped routes, commands, API exceptions, and platform support. + +**Verification:** Run every command in `ROADMAP.md` and the documented native build commands diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 0000000..b6d07a1 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,321 @@ +# Tangled API + +Twisted reads public Tangled data from Bobbin, Tangled's read-only XRPC +AppView. The hosted service is: + +```text +https://api.tangled.org +``` + +Every XRPC method lives at `/xrpc/{nsid}` and uses `GET`. Bobbin does not +require authentication. The hosted service allows cross-origin `GET`, `HEAD`, +and `OPTIONS` requests, so Twisted can call it from a browser or Capacitor +WebView. + +This document reflects Bobbin and `@atcute/tangled` as checked on 2026-08-01. +Bobbin is new and its contracts may change. Keep API calls behind one project +module so an upstream change does not leak into views. + +## Sources of truth + +Use these sources in this order: + +1. [`@atcute/tangled` lexicons](https://github.com/mary-ext/atcute/tree/trunk/packages/definitions/tangled/lexicons/sh/tangled) + define request parameters and response types. The package was at version + `2.0.13` and atcute commit + [`5474d2d`](https://github.com/mary-ext/atcute/commit/5474d2d9d502c4f52c8c13162fac8c2cd8613ec6) + during this review. +2. [Bobbin's router](https://tangled.org/tangled.org/core/blob/master/bobbin/crates/xrpc/src/lib.rs) + determines which lexicons the service implements. +3. [Tangled's Bobbin documentation](https://docs.tangled.org/bobbin) explains + runtime behavior, warm-up, and upstream failures. The + [announcement](https://blog.tangled.org/bobbin/) gives the architectural + background. + +The definition package contains Tangled queries and procedures that Bobbin does +not serve. A generated atcute type proves that a lexicon exists; it does not +prove that `api.tangled.org` implements it. In particular, Twisted must not call +the `sh.tangled.ci.*`, `sh.tangled.sync.*`, or `sh.tangled.git.temp.*` methods +through Bobbin. + +## Typed client + +Use `@atcute/client` for XRPC requests and register `@atcute/tangled` for its +ambient query declarations: + +```ts +import { Client, simpleFetchHandler } from "@atcute/client"; +import type {} from "@atcute/microcosm"; +import type {} from "@atcute/tangled"; + +export const tangled = new Client({ + handler: simpleFetchHandler({ service: "https://api.tangled.org" }), +}); + +const response = await tangled.get("sh.tangled.search.query", { + params: { q: "tangled", limit: 20 }, +}); + +if (!response.ok) { + throw new Error(response.data.message ?? response.data.error); +} + +const { hits, cursor } = response.data; +``` + +The equivalent project-wide registration is +`/// ` in a declaration file. Do not copy +generated interfaces into Twisted. Import a named record type from +`@atcute/tangled` when a view or mapper needs one. + +Bobbin's record views embed `value` as `unknown`. Narrow it with the generated +record schema before a view uses it: + +```ts +import { is } from "@atcute/lexicons"; +import { ShTangledRepo } from "@atcute/tangled"; + +if (!is(ShTangledRepo.mainSchema, response.data.value)) { + throw new Error("Bobbin returned an invalid repository record"); +} + +const repo: ShTangledRepo.Main = response.data.value; +``` + +`@atcute/tangled` does not yet define `sh.tangled.bobbin.getCoverage`. Keep the +following local exception beside the API client until upstream publishes that +lexicon: + +```ts +export interface BobbinCoverage { + ready: boolean; + eventsProcessed: number; + lastCursor: number; +} +``` + +Bobbin also exposes `com.bad-example.identity.resolveMiniDoc` for resolving a +handle or DID before constructing a Tangled profile AT-URI. Register +`@atcute/microcosm` to type this method; do not create another local identity +type. + +## Identifiers + +Bobbin uses three identifiers that should remain distinct in application code: + +- An actor DID, such as `did:plc:boltless`, identifies an account. +- A repo DID identifies a git repository as minted by its knot. It is not the + repository record's author DID. +- A record AT-URI includes the author DID, collection, and record key, such as + `at://did:plc:boltless/sh.tangled.repo/squid`. + +Single-record queries require a complete AT-URI. A handle or bare DID will not +work in `actor.getProfile`, `repo.getRepo`, `repo.getIssue`, or `repo.getPull`. +Git proxy calls also receive the repository record AT-URI from Twisted; Bobbin +resolves its knot and rewrites the parameter for the knot API. + +## Common response shapes + +Single-record methods return a record view: + +```ts +interface RecordView { + uri: string; + cid?: string; + value: T; +} +``` + +Batch lookups return `{ items: RecordView[] }` and accept at most 50 AT-URIs. +List methods return `{ items: RecordView[]; cursor?: string }`. Pass the +returned opaque cursor unchanged to fetch the next page. Most lists accept: + +| Parameter | Meaning | +| --------- | ----------------------------------------------------------- | +| `subject` | Required DID, AT-URI, or service identifier described below | +| `limit` | `1..1000`; defaults to `50` | +| `order` | `asc` or `desc` by `createdAt`; defaults to `desc` | +| `cursor` | Opaque cursor from the previous response | + +Count methods return `{ count: number; distinctAuthors: number }`. Counts and +lists can be lower bounds while Bobbin warms its in-memory index. + +An endpoint ending in `By` reverses the lookup. For example, +`listComments?subject={issue-uri}` lists comments on an issue, while +`listCommentsBy?subject={actor-did}` lists comments written by an actor. + +## Record lookups + +| NSID | Required parameter | Result | +| ---------------------------------- | --------------------------------------- | ---------------- | +| `sh.tangled.actor.getProfile` | `actor`: profile record AT-URI | One profile | +| `sh.tangled.actor.getProfiles` | `actors`: up to 50 profile AT-URIs | Profiles | +| `sh.tangled.repo.getRepo` | `repo`: repo record AT-URI | One repository | +| `sh.tangled.repo.getRepos` | `repos`: up to 50 repo record AT-URIs | Repositories | +| `sh.tangled.repo.getRepoByRepoDid` | `repoDid`: repo DID | One repository | +| `sh.tangled.repo.getIssue` | `issue`: issue record AT-URI | One issue | +| `sh.tangled.repo.getIssues` | `issues`: up to 50 issue record AT-URIs | Issues | +| `sh.tangled.repo.getPull` | `pull`: pull record AT-URI | One pull request | +| `sh.tangled.repo.getPulls` | `pulls`: up to 50 pull record AT-URIs | Pull requests | + +Issue and pull views include Bobbin's derived current state or status. Use those +fields for list badges and detail headers rather than deriving the latest value +from a separately fetched history. + +## Indexed lists and counts + +Each row names a record family, the `subject` required by its non-`By` methods, +and its implemented methods. Add `By` to each listed `list*` or `count*` method +when an actor-DID reverse lookup is available. + +| Family | Non-`By` subject | Methods | Actor reverse lookup | +| ----------------- | ----------------------------- | --------------------------------------------------- | -------------------- | +| Repositories | owner DID | `repo.listRepos`, `repo.countRepos` | No | +| Issues | repo DID | `repo.listIssues`, `repo.countIssues` | Yes | +| Pull requests | repo DID | `repo.listPulls`, `repo.countPulls` | Yes | +| Comments | issue, pull, or string AT-URI | `feed.listComments`, `feed.countComments` | Yes | +| Reactions | target record AT-URI | `feed.listReactions`, `feed.countReactions` | Yes | +| Stars | repo DID | `feed.listStars`, `feed.countStars` | Yes | +| Ref updates | repo DID | `git.listRefUpdates`, `git.countRefUpdates` | Yes | +| Followers | followed actor DID | `graph.listFollows`, `graph.countFollows` | Yes | +| Vouches | vouched-for actor DID | `graph.listVouches`, `graph.countVouches` | Yes | +| Collaborators | repo DID | `repo.listCollaborators`, `repo.countCollaborators` | Yes | +| Issue states | issue AT-URI | `repo.issue.listStates`, `repo.issue.countStates` | Yes | +| Pull statuses | pull AT-URI | `repo.pull.listStatuses`, `repo.pull.countStatuses` | Yes | +| Artifacts | repo or release identifier | `repo.listArtifacts`, `repo.countArtifacts` | Yes | +| Pipelines | repo or spindle identifier | `pipeline.listPipelines`, `pipeline.countPipelines` | Yes | +| Pipeline statuses | pipeline AT-URI | `pipeline.listStatuses`, `pipeline.countStatuses` | Yes | +| Knots | owner DID | `knot.listKnots`, `knot.countKnots` | No | +| Knot members | knot identifier | `knot.listMembers`, `knot.countMembers` | Yes | +| Spindles | owner DID | `spindle.listSpindles`, `spindle.countSpindles` | No | +| Spindle members | spindle identifier | `spindle.listMembers`, `spindle.countMembers` | Yes | +| Public keys | owner DID | `publicKey.listKeys`, `publicKey.countKeys` | No | +| Label definitions | scope identifier | `label.listDefinitions`, `label.countDefinitions` | No | +| Label operations | scope identifier | `label.listOps`, `label.countOps` | Yes | +| Strings | scope identifier | `string.listStrings`, `string.countStrings` | No | + +`repo.listIssues` accepts optional `author` and `state` filters. +`repo.listPulls` accepts optional `author` and `status` filters. Their `By` +counterparts accept the state or status filter but already use the subject as +the author. + +## Search + +`sh.tangled.search.query` searches Bobbin's in-memory full-text index. + +| Parameter | Requirement | +| ---------------- | ------------------------------------------------------------- | +| `q` | Required non-empty search text | +| `limit` | Optional `1..1000`, default `50` | +| `cursor` | Optional opaque pagination cursor | +| `nsid` | Optional record-collection filter | +| `author` | Optional author DID | +| `repo` | Optional repo DID | +| `since`, `until` | Optional RFC 3339 timestamps; `since` must not exceed `until` | + +The response is `{ hits, cursor? }`. Each hit contains `uri`, optional `cid`, +`nsid`, numeric relevance `score`, and the embedded `value`. Results are ordered +by descending relevance. + +## Git data proxied to knots + +Bobbin resolves the repo record, finds its knot, and streams these calls from +that knot. It does not cache the response. For Twisted, `repo` is the repo +record AT-URI. + +| NSID | Required parameters | Optional parameters | +| ---------------------------------- | ---------------------- | ----------------------------------------- | +| `sh.tangled.repo.getDefaultBranch` | `repo` | None | +| `sh.tangled.repo.branch` | `repo`, `name` | None | +| `sh.tangled.repo.branches` | `repo` | `cursor`, `limit` (`1..100`) | +| `sh.tangled.repo.tag` | `repo`, `tag` | None | +| `sh.tangled.repo.tags` | `repo` | `cursor`, `limit` (`1..100`) | +| `sh.tangled.repo.tree` | `repo`, `ref` | `path` | +| `sh.tangled.repo.blob` | `repo`, `ref`, `path` | `raw` | +| `sh.tangled.repo.log` | `repo`, `ref` | `path`, `cursor`, `limit` (`1..100`) | +| `sh.tangled.repo.diff` | `repo`, `ref` | None | +| `sh.tangled.repo.compare` | `repo`, `rev1`, `rev2` | None | +| `sh.tangled.repo.languages` | `repo` | `ref` (defaults to `HEAD`) | +| `sh.tangled.repo.archive` | `repo`, `ref` | `format` (defaults to `tar.gz`), `prefix` | + +Several git endpoints declare `*/*` output. Call atcute with `as: 'blob'`, +`as: 'bytes'`, or `as: 'stream'` instead of relying on JSON parsing. Bobbin +forwards range and cache validators and preserves response metadata including +`Content-Type`, `ETag`, `Last-Modified`, and `Content-Range`. + +Bobbin also proxies `sh.tangled.repo.listSecrets`, but Twisted has no user-facing +need for secret configuration and must not call it. + +Bobbin has a compatibility route for `sh.tangled.repo.describeRepo`, but its +Bobbin parameter differs from the current atcute lexicon. Twisted must use the +typed record lookup methods instead. + +The service-level `sh.tangled.owner`, `sh.tangled.knot.version`, and +`sh.tangled.knot.listKeys` methods require an extra `knot={host}` parameter when +called through Bobbin. They are useful for diagnostics rather than the first +client release. + +## Coverage and stale data + +Call `sh.tangled.bobbin.getCoverage` without parameters: + +```json +{ + "ready": true, + "eventsProcessed": 137863, + "lastCursor": 162959 +} +``` + +`ready: false` means Bobbin is still rebuilding its index. Single-record +lookups can remain accurate because Bobbin asks Slingshot for them, but indexed +lists, counts, and search may be incomplete. Twisted should show a non-blocking +"data is still loading" notice and retain any returned results. + +Coverage only describes Hydrant ingestion. It does not report whether a knot is +reachable or whether Bobbin's cached knot membership is current. + +## Public smoke-test fixture + +The project owner has approved their public Tangled data as a smoke-test and +development fixture: + +| Field | Value | +| ------------------- | --------------------------------------------------------------------- | +| Handle | `desertthunder.dev` | +| Actor DID | `did:plc:xg2vq45muivyy3xwatcehspu` | +| Profile AT-URI | `at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.actor.profile/self` | +| Twisted repo AT-URI | `at://did:plc:xg2vq45muivyy3xwatcehspu/sh.tangled.repo/3mho6hukiei22` | +| Twisted repo DID | `did:plc:4iw5fospv2asv3344au236ka` | + +Smoke tests may assert stable identity and record fields such as handle, DID, +AT-URI, collection, and `$type`. Do not pin CIDs, result counts, descriptions, +branch names, issue lists, or other mutable content. Run live tests in sequence +with a short delay because the hosted edge applies a pre-authentication rate +limit. + +## Errors + +XRPC errors use this JSON shape: + +```ts +interface XrpcErrorBody { + error: string; + message?: string; +} +``` + +Handle these classes in the client boundary: + +| Status | Typical meaning | UI behavior | +| ----------- | ------------------------------------------------------------------------ | ------------------------------------------------------ | +| `400` | Missing, malformed, or wrong-shaped identifier; invalid filter or cursor | Show a specific invalid-link or invalid-search message | +| `404` | Record or route was not found | Show the resource not-found state | +| `429` | Hosted edge rate limit | Honor `Retry-After` when present and offer retry | +| `502` | Slingshot or knot failed, disappeared, or returned invalid data | Show an upstream-unavailable state with retry | +| `503` | Bobbin shed the request under memory pressure | Offer retry; keep cached UI data | +| Other `5xx` | Bobbin failure | Show a generic service error and preserve navigation | + +Treat aborts as canceled navigation, not user-visible failures. Never retry +`400` or `404` automatically. Use bounded retries with backoff for `429`, `502`, +and `503`; honor `Retry-After` when present.