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
58 changes: 58 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# AGENTS.md

Guidance for AI coding agents working in this repo. (Humans: see
[`CONTRIBUTING.md`](./CONTRIBUTING.md) and [`docs/`](./docs/index.md).)

Surface is a deterministic gate that surfaces divergence between docs and code: you anchor a
sentence to the code it describes, and `surf check` blocks when that code's logic changes out
from under the prose. **This repo dogfoods Surface on its own source** — the gate runs on
`surf-core`/`surf-cli`.

## Where the context lives: `hubs/`

<!-- surf:hubs -->
[`hubs/`](./hubs/) is the governed context for this codebase. Each hub is markdown prose
describing an invariant, with frontmatter anchoring the claim to a specific symbol. Unlike code
comments, **hub prose is sealed by `surf check`** — if the anchored code changed since a human
last confirmed the prose, the gate fails. So the hubs are trustworthy and current in a way
comments are not, and they are the fastest accurate way to understand a part of the system.

**Read only the hubs you need — not the whole directory.** The hubs together describe the entire
codebase; reading all of them is wasteful context. They are split per module
(`hubs/cli-check.md`, `hubs/hash.md`, `hubs/resolve.md`, …), and each starts with a one-line
`summary:` in its frontmatter. Scan the filenames and summaries, then open only the hub(s)
covering the area you're working on.
<!-- /surf:hubs -->

`surf lint` enforces that this block stays — pointing at the hubs directory, never duplicating
or enumerating individual hubs.

Caveat (the tool's own honest limit): a green gate means *the anchored code hasn't changed
since last verified* — not that every sentence is true, and nothing about code no hub anchored.
If you read a hub claim, sanity-check it against the code it points at before relying on it.

## When you add or change a feature — keep the hubs honest

Run the loop (binary builds to `target/debug/surf`; see `CONTRIBUTING.md` for build commands):

1. Make the change.
2. `surf lint` — every anchor must resolve. Consider the advisory granularity warnings
(over/under-anchoring); they are nudges, not blocks.
3. `surf check` — if you touched code a hub anchors, it will report `DIVERGED`. Re-read the
claim. If the prose **still holds**, `surf verify` re-seals it (writes the new hash); if the
prose is **now false**, fix the prose first, then verify.
4. Added public behavior? Add a hub claim for it — the under-coverage warning flags public
functions with no claim. When you update a hub, update its *prose* to stay accurate, not just
the hash.
5. Record user-facing changes in [`CHANGELOG.md`](./CHANGELOG.md) under `[Unreleased]`.

Do not blindly `surf verify` to make the gate green — that is the rubber-stamping failure the
tool exists to prevent. Verify means "I read the prose and it is still true."

## Pointers

- [`hubs/`](./hubs/) — governed context, split per module; read only the hub(s) you need.
- [`CHANGELOG.md`](./CHANGELOG.md) — what changed; update `[Unreleased]`.
- [`docs/index.md`](./docs/index.md) — documentation map (guides, reference, concepts).
- [`CONTRIBUTING.md`](./CONTRIBUTING.md) — build, test, format, lint commands and layout.
- [`docs/surface-proposal.md`](./docs/surface-proposal.md) — the product spec (the `§` references in hubs).
66 changes: 66 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Changelog

All notable changes to Surface are documented here.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.0] - 2026-06-07

### Added
- `surf check` scoping: `--files <globs>` evaluates only claims whose anchored file(s) match,
and an explicit `--base <ref>` diff-scopes the gate to claims whose files changed since the
merge base. Omitting `--base` keeps a full check (enrichment falls back to `HEAD`).
- `--format json` for `surf lint` and `surf verify`, mirroring `surf check`. `lint` emits
structured findings; `verify` emits per-anchor outcomes (stamped/followed/unchanged/skipped)
plus counts.
- Advisory `surf lint` granularity warnings (never blocking): a near-whole-file anchor span, a
hub with too many anchors, and public functions in an anchored file that no claim covers.
- `surf lint` enforces the `AGENTS.md` pointer block: when `AGENTS.md` carries a
`<!-- surf:hubs -->` block, it must link the hubs directory (and that directory must exist) so
agents are pointed at the hubs to search — never duplicating or enumerating them. Opt-in;
blocks on a missing/dangling pointer.
- `surf suggest <globs>` — scans source for top-level public functions that no hub anchors yet
and prints a copy-pasteable starter hub. Suggestions only: never writes a file or stamps a hash.
- `surf_core::public_fns` — enumerates a file's top-level public functions (backs the
under-coverage warning and `surf suggest`).

### Changed
- An explicit `surf check --base <ref>` now **diff-scopes** the gate to claims whose files
changed, in addition to feeding advisory `old_code`/`magnitude`. Previously `--base` only fed
enrichment and every claim was still checked. CI that passes `--base` explicitly will now check
fewer claims; omit `--base` for a full check.

## [0.1.1] - 2026-06-06

### Fixed
- `resolve`: resolve const-bound call-expression functions (TS/JS).
- `verify`: skip unchanged anchors instead of re-stamping, so a no-op verify leaves hub files
byte-identical.

### Changed
- Releases: dropped Intel macOS (`x86_64-apple-darwin`); prebuilt binaries cover macOS (Apple
Silicon) and Linux (x86_64). Other targets build from source.

## [0.1.0] - 2026-06-06

Initial release — the MVP gate that surfaces docs↔code divergence.

### Added
- AST-canonical hashing via bundled, version-pinned tree-sitter grammars: quiet on cosmetics
(formatting, comments, consistent renames), loud on logic. Advisory tree-edit `magnitude`.
- Anchor resolution: the `file > A > B` path grammar with `@N` disambiguation; scope-set
resolution so `Type > method` resolves uniquely.
- Hub format (markdown frontmatter), `surf.toml` workspace discovery, and per-claim combined
hashing across multi-site `at:` lists.
- Commands: `surf lint`, `surf check` (the gate), `surf verify` (re-seal, with `--follow` for
renames), plus `surf init` and `surf new` for authoring ergonomics.
- Language support: TypeScript/TSX, JavaScript/JSX, Rust, Python, and Go.
- Distribution: GitHub Action, pre-commit hook, and `install.sh`; Apache-2.0 license.

[Unreleased]: https://github.com/Connorrmcd6/surface/compare/v0.2.0...HEAD
[0.2.0]: https://github.com/Connorrmcd6/surface/compare/v0.1.1...v0.2.0
[0.1.1]: https://github.com/Connorrmcd6/surface/compare/v0.1.0...v0.1.1
[0.1.0]: https://github.com/Connorrmcd6/surface/releases/tag/v0.1.0
4 changes: 4 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,9 @@ cargo run -q -p surf-cli -- verify "surf-core/src/hash.rs > emit"
- `surf-cli/` — the `surf` binary: workspace discovery, the commands, all I/O.
- `docs/phases/` — how the MVP was built, one self-contained file per phase. Start with
`docs/phases/OVERVIEW.md`. The product spec is `docs/surface-proposal.md`.
- `docs/index.md` — the documentation map (guides, reference, concepts). `AGENTS.md` is the
on-ramp for AI coding agents.

Keep `surf-core` free of I/O so it stays reusable; put filesystem/git work in `surf-cli`.

When a change is user-facing, add a line to `CHANGELOG.md` under `[Unreleased]`.
4 changes: 2 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["surf-core", "surf-cli"]
resolver = "2"

[workspace.package]
version = "0.1.1"
version = "0.2.0"
edition = "2021"
license = "Apache-2.0"
repository = "https://github.com/Connorrmcd6/surface"
Expand Down
15 changes: 13 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,14 +131,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # plain checkout — do NOT set fetch-depth: 0
- uses: Connorrmcd6/surface@v0.1.0
- uses: Connorrmcd6/surface@v0.2.0
```

pre-commit — `.pre-commit-config.yaml`:

```yaml
- repo: https://github.com/Connorrmcd6/surface
rev: v0.1.0
rev: v0.2.0
hooks:
- id: surf-check
```
Expand Down Expand Up @@ -235,10 +235,21 @@ surf check --format json

- `surf init` — bootstrap a workspace: write `surf.toml` and create the hubs directory (idempotent).
- `surf new <name>` — scaffold a new empty hub under your hubs directory.
- `surf suggest <globs> [--format human|json]` — scan source globs for top-level public functions no hub anchors yet and print a copy-pasteable starter hub. Suggestions only — never writes or stamps.
- `surf lint [--format human|json]` — validate frontmatter and that every `at:` resolves to exactly one symbol. Warns on a renamed symbol (suggests `verify --follow`); blocks on ambiguous or vanished anchors. Also emits advisory granularity warnings (never blocking, §8): a near-whole-file anchor span, a hub with too many anchors, and public functions in an anchored file that no claim covers.
- `surf check [--format human|json] [--base <ref>] [--files <globs>]` — the gate. AST-canonical-hash each anchored span and compare to the stored hash; non-zero exit on any divergence. By default every claim is checked. `--base <ref>` scopes to claims whose anchored files changed since the merge base **and** recovers the advisory `old_code` / `magnitude` fields from that ref (omit it for a full check with enrichment against `HEAD`). `--files <globs>` scopes to claims whose anchored file(s) match a comma-separated glob (e.g. `surf-core/**`).
- `surf verify [<at>] [--follow] [--format human|json]` — re-seal after you've confirmed the prose still holds; writes the hash into the frontmatter. `<at>` limits to one anchor; `--follow` re-points a renamed single-symbol anchor and re-hashes in one step.

## Documentation

Full docs live in [`docs/`](docs/index.md):

- [Authoring hubs](docs/guides/authoring-hubs.md) — writing claims and anchors, choosing granularity, the verify loop.
- [CI integration](docs/guides/ci-integration.md) — the Action, the pre-commit hook, and scoping the gate to a PR.
- [Examples](docs/examples.md) — a minimal worked hub in each supported language.

Release history is in [`CHANGELOG.md`](CHANGELOG.md). AI agents working in this repo: see [`AGENTS.md`](AGENTS.md).

## Configuration

A `surf.toml` at the repo root marks the workspace — `surf` walks up from the current
Expand Down
68 changes: 68 additions & 0 deletions docs/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Examples

One minimal hub per supported language. Each shows an `at:` anchor and the rule that always
holds: **quiet on cosmetics (formatting, comments, consistent renames), loud on logic.** Run
`surf verify` once to seal the `hash`, then `surf check` gates it.

The anchor grammar and the verify loop are covered in [Authoring hubs](./guides/authoring-hubs.md).

## TypeScript

```yaml
anchors:
- claim: rotation is single-use; a reused token triggers global logout
at: src/auth/refresh.ts > TokenService > rotate
```

- Rename a local, reformat, add a comment → **no fire.**
- Change `if (token.used)` to `if (token.used || token.expired)` → **fires.**

## JavaScript / JSX

```yaml
anchors:
- claim: cart total applies the member discount before tax
at: src/cart.js > computeTotal
```

JS/JSX is parsed by the TS-family grammar, so `const computeTotal = (...) => { ... }` resolves
the same as a `function` declaration.

- Drop an `await`, flip `*` to `+` → **fires.**

## Rust

```yaml
anchors:
- claim: combining site hashes is order-sensitive
at: surf-core/src/hash.rs > combine_site_hashes
```

`Type > method` walks into an `impl`: `at: surf-cli/src/workspace.rs > Workspace > discover`.

- Change `<` to `<=`, alter a literal value → **fires.**

## Python

```yaml
anchors:
- claim: retries use exponential backoff capped at 30s
at: api/client.py > Client > _request
```

Decorators are transparent — `@retry` above `def _request` doesn't change resolution.

- Change the backoff base or the cap → **fires.**

## Go

```yaml
anchors:
- claim: the receiver validates the signature before decoding
at: internal/webhook.go > Handler > Verify
```

Go methods attach by receiver, so the anchor is `Type > Method` even though the method isn't
nested in the type. Top-level funcs/types use a single segment: `cmd/main.go > Run`.

- Reorder unrelated declarations → **no fire.** Change the comparison in `Verify` → **fires.**
123 changes: 123 additions & 0 deletions docs/guides/authoring-hubs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
# Authoring hubs

A **hub** is a markdown file whose frontmatter anchors sentences ("claims") to the code they
describe. This guide covers writing claims, the anchor grammar, choosing the right granularity,
and the verify loop. For the end-to-end first run, see the [Quickstart](../../README.md#quickstart).

## Anatomy of a hub

```yaml
---
summary: How auth refresh rotation works.
anchors:
- claim: refresh rotation is single-use; reuse triggers global logout
at: src/auth/refresh.ts > rotateRefreshToken
hash: 9b1c33ade8f1 # written by `surf verify`, not by hand
refs: []
---

# Auth

Prose a human (or agent) reads to understand this domain.
```

- **`claim`** — one sentence stating an invariant. Write what must stay true, not how the code
is structured. A claim that restates the implementation rots as fast as a comment.
- **`at`** — the anchor: where the claim's logic lives (grammar below).
- **`hash`** — the seal. Absent until you `surf verify`; the gate treats a hashless claim as
*unverified*.

Where hubs live is configured by the `hubs` glob in `surf.toml` (default `hubs/*.md`); keep them
central or co-locate them with code (`["**/_hub.md"]`).

## Bootstrapping with `surf suggest`

Authoring claims by hand is the main adoption cost. To get a head start, point `surf suggest` at
your source and it lists the top-level public functions no hub anchors yet, as a copy-pasteable
starter hub:

```sh
surf suggest "src/**/*.ts" # or --format json for tooling
```

It only suggests — it never writes a file or stamps a hash. Paste the block into a hub (or
`surf new <name>`), write a real claim sentence for each anchor you keep, delete the rest, then
`surf verify`. Treat it as a checklist of undocumented surface, not a mandate to anchor everything
(see granularity below).

## The anchor grammar

An anchor is a file path, then a `>`-separated symbol path:

```
src/service.ts > TokenService > rotate
```

- **One segment** points at a top-level symbol: `src/m.rs > parse_anchor`.
- **Nested segments** walk into scopes: a type and its `impl`/methods share a name, so
`Type` alone may be ambiguous while `Type > method` is unique.
- **`@N`** disambiguates genuine name collisions (1-based), e.g. two overloads:
`src/api.ts > handler@2`.
- **Multiple sites** — an `at:` list combines its sites into one hash, so the claim is stale if
*any* listed span changes:
```yaml
at:
- src/a.rs > foo
- src/b.rs > bar
```

Run `surf lint` to confirm every anchor resolves to exactly one symbol. Ambiguous or vanished
anchors **block**; a symbol that was merely renamed only **warns** and points you at
`surf verify --follow`.

## Choosing granularity

This is the central tension (proposal §8):

- **Under-anchor** → real drift slips through, because the changed logic wasn't anchored.
- **Over-anchor** → every incidental edit re-triggers verification, and humans start
rubber-stamping `verify` without reading — which defeats the tool.

`surf lint` emits advisory warnings (never blocking) to nudge you toward the middle:

- **Near-whole-file span** — the anchored symbol covers most of its file. Anchor a narrower
symbol so unrelated edits don't trip the claim.
- **Too many anchors in one hub** — split the hub; a long verify list invites rubber-stamping.
- **Uncovered public function** — a public function in a file the hub already anchors has no
claim. Either add one, or accept it as intentionally undocumented.

Rule of thumb: anchor the **smallest symbol whose logic the sentence is actually about.**

## The verify loop

`surf verify` is the human escape hatch: it re-seals a claim after *you* confirm the prose still
holds, writing the hash into the frontmatter (and touching only that line).

```sh
surf check # DIVERGED? a claim's anchored logic changed
# re-read the claim:
# still true → surf verify [<at>] (re-seal)
# now false → fix the prose first, then verify
surf verify --follow # renamed symbol: re-point the anchor and re-hash in one step
```

Verifying without reading is the failure mode the whole tool exists to prevent. A green gate
promises only "nothing anchored changed since last sign-off" — never that the prose is true.

## Hubs and `AGENTS.md`

Hubs are *declarative* domain briefings; `AGENTS.md` is *imperative* operating instructions for
coding agents. Keep them separate — don't copy hub prose into `AGENTS.md`. Instead, give
`AGENTS.md` a pointer block that sends agents to the hubs directory to search for what they need:

```markdown
<!-- surf:hubs -->
Context lives in [`hubs/`](./hubs/) — read only the hub(s) you need.
<!-- /surf:hubs -->
```

When that block is present, `surf lint` checks it links the configured hubs directory and that
the directory exists. It deliberately does **not** enumerate individual hubs — that would push an
agent to read everything instead of the one hub it needs.

See also: [CI integration](./ci-integration.md) · [Examples](../examples.md).
Loading