Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]

### Added
- **`refs` hub composition — validation pass (#4).** A hub's `refs:` now compose *other hubs*: a
path relative to the referencing hub (`./resolve.md`), optionally `> symbol` to address one claim
within the target (matched against that claim's `at:` anchor). `surf lint` blocks a ref that
doesn't resolve to a loaded hub, points at its own hub, or names a claim the target lacks — the
same fail-on-typo discipline as `covers`. The `check` verdict does **not** read `refs` yet
(staleness does not propagate across hubs); this ships the validated navigation graph first, per
the §9.3 unlock discipline. The repo's own hubs now declare their cross-hub `refs`, and the two
- **`refs` hub composition (#4).** A hub's `refs:` now compose *other hubs*: a path relative to the
referencing hub (`./resolve.md`), optionally `> symbol` to address one claim within the target
(matched against that claim's `at:` anchor). `surf lint` blocks a ref that doesn't resolve to a
loaded hub, points at its own hub, or names a claim the target lacks — the same fail-on-typo
discipline as `covers`. The `surf check` gate then **propagates staleness one hop**: a hub that
directly references a stale hub (or a stale claim within one) inherits a `referenced_stale`
divergence and fails the gate, so the signal that flags a dependency flags everything composed on
it. Propagation is one-hop by construction (built only from base divergences), so a chain
`A → B → C` doesn't cascade. The repo's own hubs now declare their cross-hub `refs`, and the two
prior doc-pointing `refs` were reclassified to prose links.
- **`surf lint` consolidation nudges (#142).** Two advisory warnings push hubs away from the
"claim-log" shape (one claim per function) and toward onboarding docs: a *claim-log* warning
Expand Down
42 changes: 42 additions & 0 deletions docs/dogfood-log.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,48 @@ did about it, the lesson.* Keep it honest; the failures are the interesting part

---

## 2026-06-29 — `refs` propagation fired first on the commit that created it

**Context:** PR2 of `refs` hub composition (#4) turns on staleness *propagation*: `surf check`
now flags a hub when a hub it `refs` has an open divergence (one-hop). The change lives in
`check_workspace` — which is itself an anchored claim in `cli-check.md`, and `cli-check.md` is
referenced by `cli-workspace.md` and `hub-format.md`.

**What happened:** the first `surf check` after wiring up propagation went red three ways:

```
DIVERGED hubs/cli-check.md :: surf-cli/src/check.rs > check_workspace
stored 2:4f5890aca70c → now 2:b7b7fd55206e (magnitude: Large)
REF-STALE hubs/cli-workspace.md :: ./cli-check.md
referenced hub `hubs/cli-check.md` has an open divergence — review it, then re-verify
REF-STALE hubs/hub-format.md :: ./cli-check.md
referenced hub `hubs/cli-check.md` has an open divergence — review it, then re-verify
surf check: 3 divergence(s).
```

The new feature's *first real firing* was on the very diff that introduced it: editing
`check_workspace` diverged its own claim, and propagation — the thing being added — immediately
walked the two hubs that compose it and flagged them too. Fixing the root cleared all three at
once: I updated the `check_workspace` claim prose to describe propagation, re-sealed it
(`surf verify "surf-cli/src/check.rs > check_workspace"`), and both inherited `REF-STALE`s
vanished with it.

**Why it's a good story:** the composition graph proved itself end-to-end without a contrived
example. It also makes the §8/§11.3 risk concrete: one genuine divergence amplified into three
findings (1 root + 2 inherited). That's the cascade the proposal worried about — but the shape
held up: the inherited flags are clearly labelled, point at the root, and clear the instant the
root is re-sealed. One-hop did its job too — `cli-check` itself `refs` `cli-git`/`cli-verify`,
which were clean, so nothing spread further, and the two `REF-STALE` hubs didn't re-propagate
onto *their* referrers (propagation is built only from base divergences).

**Lesson / open question:** "fix the root, the inherited flags clear" is the property that makes
propagation usable rather than noisy — but it relies on the author recognising a `REF-STALE` as
*derived*, not a second thing to fix. Open question: at scale, is a 1→N amplification per stale
hub still legible, or does `check` eventually want to *group* inherited flags under their root
(print the root divergence, then "and N hubs that ref it") rather than as N peer lines?

---

## 2026-06-29 — The new claim-log nudges flagged 22 of our own hubs

**Context:** #142 argues the CLI's in-loop signals (`surf suggest`, `lint_under_coverage`) teach
Expand Down
7 changes: 4 additions & 3 deletions docs/guides/authoring-hubs.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@ Prose a human (or agent) reads to understand this domain.
- **`refs`** — hub composition: paths to *other hubs* this one builds on, written relative to this
hub (`./resolve.md`), optionally `> symbol` to point at one claim within the target
(`./resolve.md > resolve_nodes`, matched against that claim's `at:` anchor). `surf lint` blocks a
ref that doesn't resolve to a hub, points at this hub, or names a claim the target lacks — so a
typo can't rot silently. The `check` *verdict* doesn't read `refs` yet (a referenced hub going
stale won't flag this one); refs are a validated navigation graph for now.
ref that doesn't resolve to a hub, points at this hub, or names a claim the target lacks. The
`check` gate also **propagates staleness one hop**: when a hub you `ref` has an open divergence,
this hub fails too (a `referenced_stale` divergence) — review the dependency and re-verify. Only
*direct* refs propagate; a chain `A → B → C` stops at one hop.
- **`covers`** — advisory file-scope globs; parsed and lint-validated but never affects
`surf check`. Leave it empty unless you have a reason — the feature that consumes it isn't
shipped.
Expand Down
17 changes: 11 additions & 6 deletions hubs/cli-check.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,15 @@ anchors:
- claim: >
The gate fails closed: a hub whose frontmatter won't parse yields an Unresolvable
divergence (blocking the run) rather than being silently skipped, so a frontmatter typo
can't pass as clean. Alongside the divergences it returns the --files patterns that
matched no anchored file (run warns on stderr for each and exits non-zero when every
pattern matched nothing, so a typo'd --files can't read as a clean run) and a count of
clean anchors still stamped under v1, so run can nudge the one-time `surf verify` upgrade.
can't pass as clean. After the per-claim walk it propagates refs one hop — a hub that
directly references a stale hub (or a stale claim within one) inherits a ReferencedStale
divergence, built only from base divergences so a chain stops at the first hop. Alongside
the divergences it returns the --files patterns that matched no anchored file (run warns on
stderr for each and exits non-zero when every pattern matched nothing, so a typo'd --files
can't read as a clean run) and a count of clean anchors still stamped under v1, so run can
nudge the one-time `surf verify` upgrade.
at: surf-cli/src/check.rs > check_workspace
hash: 2:4f5890aca70c
hash: 2:b7b7fd55206e
refs:
- ./cli-git.md
- ./cli-verify.md
Expand All @@ -42,7 +45,9 @@ produces the same answer; the git helpers in [`cli-git.md`](./cli-git.md) only f
`check_claim` is the per-claim verdict; `check_workspace` walks every hub, and `Scope` narrows
which claims it evaluates when `--base` or `--files` is given — opt-in and intersective, falling
back to a full check rather than checking nothing. Any divergence (including a hub whose
frontmatter won't parse — the gate fails closed) makes `run` exit non-zero.
frontmatter won't parse — the gate fails closed) makes `run` exit non-zero. A hub also fails when a
hub it [`refs`](./hub-format.md) is stale: composition propagates one hop (#4), so the gate that
flags a dependency flags everything built on it.

**Boundary:** green means "nothing anchored changed since last sign-off," not "the prose is true";
that confirmation is [`surf verify`](./cli-verify.md)'s job, not the gate's.
9 changes: 5 additions & 4 deletions hubs/hub-format.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ summary: The hub document format and the minimal-diff frontmatter editor used by
anchors:
- claim: >
A hub is a `---`-fenced YAML frontmatter block followed by a markdown body; `at:` is a
scalar or a list, hash is optional until verified, and unknown fields are rejected — though
forward-declared fields (`refs`, `covers`) are accepted and stored but inert in the verdict.
scalar or a list, hash is optional until verified, and unknown fields are rejected — while
`refs`/`covers` are accepted and stored verbatim, parse_hub resolving neither (acting on them
is lint/check's job).
at: surf-core/src/hub.rs > parse_hub
hash: 2:c510c6032ba7
- claim: >
Expand All @@ -25,8 +26,8 @@ A hub is the unit every command reads and writes: a `---`-fenced YAML frontmatte
machine-checkable `anchors`) followed by a markdown body (the prose a human or agent reads).
`parse_hub` is the contract everything else binds to — its shape is why `at:` can be a scalar or a
list, why `hash` is optional until verified, and why unknown fields are rejected (so a typo can't
masquerade as a new field) while `refs`/`covers` are accepted and lint-validated but never gate the
`check` verdict.
masquerade as a new field) while `refs`/`covers` are accepted and lint-validated — `covers` never
gates, but a stale `refs` target now propagates into the [`check`](./cli-check.md) verdict (#4).

**The distinction that drives the design:** a human reviews every write, so edits must be
*surgical*. Writes go through the line-level editor (`set_anchor_hash` / `set_anchor_at`) rather
Expand Down
Loading