Summary
Add an optional origin field so a hub that travels across repositories (shared/vendored/published docs) doesn't throw false UNRESOLVED divergences in a repo where its anchored code doesn't exist. Borrowed from fiberplane/drift's origin = "github:org/repo" binding field: when a hub's declared origin isn't the current repo, surf check skips its anchors instead of failing the gate.
This is the lightweight, non-registry version of cross-repo support. It does not reopen the "Cross-repo registry" deferral in the proposal (§9.3, polyrepo-at-scale) - it's a single string field plus a skip rule, no resolver, no network, no catalog.
Why
A hub anchored to src/auth/refresh.ts > rotateRefreshToken is meaningful only in the repo that contains that file. If the same hub is copied/published into another repo (an OKF bundle shared across services, a docs monorepo, a vendored spec), every anchor resolves to a missing file. Today that path becomes DivergenceKind::Unresolvable ("UNRESOLVED, run surf lint") in surf-cli/src/check.rs (read_site fails at surf-cli/src/workspace.rs:141), which blocks the gate with false positives. origin lets the foreign repo cleanly skip anchors it can't own, while the home repo still governs them.
Bonus: origin is a provenance breadcrumb - it records where a hub's governance natively lives, which pairs well with OKF portability.
Design
1. Declaring the current repo's identity
Core stays pure/deterministic (no network, no git - see surf-core/src/config.rs docs). Add to surf.toml:
origin = "github:Connorrmcd6/surface"
- New optional field
origin: Option<String> on Config (surf-core/src/config.rs:11). deny_unknown_fields is on, so adding the field is the safe way to accept it.
- Optional CLI fallback (I/O, lives in
surf-cli): if surf.toml omits origin, derive from git remote get-url origin and normalize to host:owner/repo. Keep this out of core. If neither is present, the feature is inert (every hub is treated as native, today's behavior).
2. Declaring a hub's home
Add optional origin: Option<String> to Frontmatter (surf-core/src/hub.rs:22), sibling to refs/covers. Hub-level (applies to all the hub's anchors) is the right granularity - hubs travel as whole docs. Serialize with skip_serializing_if = "Option::is_none" so existing hubs stay byte-identical.
3. The skip rule
In check_workspace (surf-cli/src/check.rs:57), before a hub's anchors are resolved:
- If
hub.origin is Some(o) and workspace origin is Some(w) and o != w (normalized compare) -> skip all anchors in that hub. Do not emit Unresolvable; do not count toward the gate.
- Report skips on stderr as advisory (
surf check: skipped N anchor(s) from hub X (foreign origin "..." != "...")), matching how --files/v1 nudges already print to stderr so JSON stdout stays clean.
- If either side is
None, behave exactly as today (native).
- If
o == w, check normally - origin is inert metadata in the home repo.
Normalization: strip scheme variance (github:, git@github.com:, https://github.com/, trailing .git) to a canonical host/owner/repo before compare. Small pure helper in core with unit tests.
4. JSON envelope
Add an advisory skipped list (hub, origin, reason) to the --format json report (surf-core/src/report.rs, CheckReport) so downstream layers see what was skipped rather than silently absent. Bump the envelope version note in docs/reference/how-it-works.md#the-json-seam if the shape changes.
Acceptance criteria
Affected code
surf-core/src/config.rs - Config.origin.
surf-core/src/hub.rs - Frontmatter.origin.
- New pure helper (normalize + compare) in core +
lib.rs re-export.
surf-cli/src/check.rs - skip rule in check_workspace; surf-cli/src/workspace.rs - optional git-remote fallback for workspace origin.
surf-core/src/report.rs - advisory skipped in CheckReport.
docs/reference/configuration.md, docs/guides/authoring-hubs.md, docs/reference/how-it-works.md.
Open questions
- Per-claim override. Hub-level is the MVP. Do we ever need per-claim
origin (a hub anchored partly into a vendored dep)? Defer unless a real case appears.
- Silent skip vs explicit opt-in. Should a foreign hub with no matching code be skipped silently, or require
origin to be present to avoid an accidental green? Recommend: skip only when origin is explicitly declared and mismatched; a missing file with no origin stays Unresolvable (fail closed) - so origin is an intentional "this doc is foreign here" signal, never an accidental mute.
Summary
Add an optional
originfield so a hub that travels across repositories (shared/vendored/published docs) doesn't throw falseUNRESOLVEDdivergences in a repo where its anchored code doesn't exist. Borrowed from fiberplane/drift'sorigin = "github:org/repo"binding field: when a hub's declared origin isn't the current repo,surf checkskips its anchors instead of failing the gate.This is the lightweight, non-registry version of cross-repo support. It does not reopen the "Cross-repo registry" deferral in the proposal (§9.3, polyrepo-at-scale) - it's a single string field plus a skip rule, no resolver, no network, no catalog.
Why
A hub anchored to
src/auth/refresh.ts > rotateRefreshTokenis meaningful only in the repo that contains that file. If the same hub is copied/published into another repo (an OKF bundle shared across services, a docs monorepo, a vendored spec), every anchor resolves to a missing file. Today that path becomesDivergenceKind::Unresolvable("UNRESOLVED, run surf lint") insurf-cli/src/check.rs(read_sitefails atsurf-cli/src/workspace.rs:141), which blocks the gate with false positives.originlets the foreign repo cleanly skip anchors it can't own, while the home repo still governs them.Bonus:
originis a provenance breadcrumb - it records where a hub's governance natively lives, which pairs well with OKF portability.Design
1. Declaring the current repo's identity
Core stays pure/deterministic (no network, no git - see
surf-core/src/config.rsdocs). Add tosurf.toml:origin: Option<String>onConfig(surf-core/src/config.rs:11).deny_unknown_fieldsis on, so adding the field is the safe way to accept it.surf-cli): ifsurf.tomlomitsorigin, derive fromgit remote get-url originand normalize tohost:owner/repo. Keep this out of core. If neither is present, the feature is inert (every hub is treated as native, today's behavior).2. Declaring a hub's home
Add optional
origin: Option<String>toFrontmatter(surf-core/src/hub.rs:22), sibling torefs/covers. Hub-level (applies to all the hub's anchors) is the right granularity - hubs travel as whole docs. Serialize withskip_serializing_if = "Option::is_none"so existing hubs stay byte-identical.3. The skip rule
In
check_workspace(surf-cli/src/check.rs:57), before a hub's anchors are resolved:hub.originisSome(o)and workspace origin isSome(w)ando != w(normalized compare) -> skip all anchors in that hub. Do not emitUnresolvable; do not count toward the gate.surf check: skipped N anchor(s) from hub X (foreign origin "..." != "...")), matching how--files/v1 nudges already print to stderr so JSON stdout stays clean.None, behave exactly as today (native).o == w, check normally -originis inert metadata in the home repo.Normalization: strip scheme variance (
github:,git@github.com:,https://github.com/, trailing.git) to a canonicalhost/owner/repobefore compare. Small pure helper in core with unit tests.4. JSON envelope
Add an advisory
skippedlist (hub, origin, reason) to the--format jsonreport (surf-core/src/report.rs,CheckReport) so downstream layers see what was skipped rather than silently absent. Bump the envelope version note indocs/reference/how-it-works.md#the-json-seamif the shape changes.Acceptance criteria
originparses on bothConfig(surf.toml) andFrontmatter(hub), optional, and round-trips byte-identically when absent.github:owner/repo,git@github.com:owner/repo.git, andhttps://github.com/owner/repoto one canonical form (unit-tested).surf check: a hub whoseorigin!= workspace origin skips its anchors, exits 0 (no false UNRESOLVED), and prints an advisory stderr line.origin== workspace origin (or absent on either side) is checked exactly as today - covered by a regression test.skippedlist, not as divergences.surf lintdoes not flag foreign-origin anchors as unresolvable.docs/reference/configuration.md(theoriginkey),docs/guides/authoring-hubs.md(huborigin), and a short "docs that travel" note; clarifyoriginis orthogonal torefs(hub->hub composition) and is not the deferred cross-repo registry.Affected code
surf-core/src/config.rs-Config.origin.surf-core/src/hub.rs-Frontmatter.origin.lib.rsre-export.surf-cli/src/check.rs- skip rule incheck_workspace;surf-cli/src/workspace.rs- optional git-remote fallback for workspace origin.surf-core/src/report.rs- advisoryskippedinCheckReport.docs/reference/configuration.md,docs/guides/authoring-hubs.md,docs/reference/how-it-works.md.Open questions
origin(a hub anchored partly into a vendored dep)? Defer unless a real case appears.originto be present to avoid an accidental green? Recommend: skip only whenoriginis explicitly declared and mismatched; a missing file with no origin staysUnresolvable(fail closed) - so origin is an intentional "this doc is foreign here" signal, never an accidental mute.