Skip to content

port

Provenance

  • Source: .spec/spexcode/spec-forge/port/spec.md
  • Source SHA-256: 9906859fa3ab95c1d915404dc01142fb5938d7742a8ecb86cfbe8064127c6488

The seam of [[spec-forge]]: a single host-agnostic port naming the abstraction, with per-host drivers behind it. The name is the seam, never the vendor.

Unlike a projection, the port reads the forge. Its two verbs fetch a host's work objects — listIssues() → ForgeIssue[] (issues of all states, so closed work stays linkable, not just live issues) and listPRs() → ForgePR[] (open PRs). ForgeIssue is the small stable subset an issue collapses to on every host (number, title, body, url, state, labels, author, createdAt — each label keeps its name plus any host-provided background/text colors; the body is where the Spec: <id> marker lives; author/createdAt are what lets a forge issue stand beside a local issue thread as the same object in the unified Issue port, spec-cli's [[issues]], with a by and a created). It also carries the issue's comments (ForgeComment[]: author, createdAt, body — exactly what becomes a unified Issue's replies[]), riding the same list reads, not a second fetch path: the gh list asks for the comments JSON field (heavier per call, covered by [[forge-cache]]'s TTL), and the incremental window — whose REST rows carry only a comment count — fetches each commented updated issue's thread alongside (a since-window is a handful of issues, so that stays a handful of calls). ForgePR adds headRefName (the node/<id> branch = a free structural link) and closesIssues (the issue numbers it closes, for transitive linking). These vendor-neutral shapes are what let one port cover any host. A driver may also offer the optional incremental window listIssuesSince(sinceISO) — only issues updated since that moment — which lets [[forge-cache]]'s resident cache merge small deltas instead of full-listing every cycle; a driver without it is simply always full-listed. State casing is normalized to lowercase at the driver — platform differences (gh's GraphQL OPEN vs REST open) die at the adapter, never downstream.

A driver is the only thing that touches the network/CLI; it does no link resolution (that is host-agnostic, in [[links]]). The first real driver is github, which wraps the gh CLI — reusing the user's existing auth and gh's repo auto-detection rather than handling tokens itself. It fails loud: an absent or unauthenticated gh throws with gh's own message, so a broken gh never looks like an empty forge.

One caveat, scoped to a single optional field. closesIssues rides GitHub's closingIssuesReferences, a gh pr list JSON field that older gh builds don't know. Only the transitive link needs it; the two core links (the node/<id> PR branch and the Spec: issue marker) read baseline fields. So a gh too old for that one field must degrade only transitive linking, never take the whole driver down — otherwise [[forge-cache]]'s resident cache swallows the throw and the dashboard goes blank ([[dashboard-issues]]). The driver asks for the field and, only on gh's specific "unknown JSON field" rejection, retries without it (closesIssues empty) and warns once; every other failure (no gh, no auth, no repo) still throws loud — the degrade is that narrow field-version case alone, never a blanket swallow.

The port carries three write verbs, existing solely so the unified Issue port's cross-store actions (spec-cli's [[issues]]) go through this same seam — the driver stays the ONLY thing that touches the network, writes included, rather than a second vendor call-site growing in product code. A tiny driver registry exposes the forge issue store to callers as a store id (github, later gitlab, ...) — scoped to the repo's resolved host ([[forge-host]] picks which; a resolved host without a driver offers no forge store) — so product code chooses by store id and then talks to one host-agnostic driver shape: createIssue({title, body}) → {number, url} (promotion OR a dashboard New issue opened directly on the forge; gh wraps gh issue create) and createComment({number, body}) → {url} (the store-routed reply: commenting on a forge issue from any SpexCode surface; gh wraps gh issue comment) and closeIssue({number}) → {url} (the store-routed lifecycle close: closing a forge issue from the unified Issues page; gh wraps gh issue close). All fail loud. The tracer (links/forge-cache/the board fold) remains read-only end to end, and the deeper contract is untouched: nothing here ever writes a node's version or status (which stays git-derived) — a created issue or comment or closed issue is execution-plane work, never graph state.

Out of scope: link resolution ([[links]]), the CLI surface ([[forge-cli]]), and any second driver (gitlab/bitbucket wrapping their own CLI later).