Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
b4c1f15
Build sieve: an escalating, self-auditing web distiller for agents
qcoderx Aug 7, 2026
5999811
Read hard pages in seconds, and read more of them
qcoderx Aug 7, 2026
2a4c746
Confirm a DNS negative before believing it
qcoderx Aug 7, 2026
55f32ca
Stop dropping text a page was watched revealing, and count what is em…
qcoderx Aug 7, 2026
f863d5d
Drop the hidden twin of text already in the artifact
qcoderx Aug 8, 2026
40a59e7
Slow down when nothing on the page has been legible
qcoderx Aug 8, 2026
cb9a971
Look again when the page was still loading, and stop echoing carousels
qcoderx Aug 8, 2026
cb59410
Wait for the page, then start the clock
qcoderx Aug 8, 2026
f58d1cb
Separate waiting for a page from waiting for it to hold still
qcoderx Aug 8, 2026
aab23b0
Stop emitting menus as prose, and give tier 0 a budget it can meet
qcoderx Aug 9, 2026
6aa0977
Collapse template labels, drop stray characters, and let the browser
qcoderx Aug 9, 2026
fef6989
Keep the served HTML when the browser cannot reach the page, and say
qcoderx Aug 10, 2026
8363b40
Let the browser answer a page the fetch client was refused, and never
qcoderx Aug 10, 2026
de027e0
Give every stage room in the wall clock, and bound the cheap path
qcoderx Aug 10, 2026
0a5f095
Walk through a site's front door, and refuse the doors that ask
qcoderx Aug 10, 2026
54bafab
Find a front door that is not a button, and add the two switches that
qcoderx Aug 10, 2026
8fcffb0
Wait out a page that is busy rather than idle, and press when it asks
qcoderx Aug 11, 2026
ed0fcb0
Open the front door by what the page shows, not by how quiet it is
qcoderx Aug 11, 2026
b15c5fc
Say that a page has no words in it, instead of nothing at all
qcoderx Aug 11, 2026
f0493e0
Read robots.txt the way the standard says, and notice an error page
qcoderx Aug 11, 2026
32b5c2f
Stop the audit accusing itself of losing links
qcoderx Aug 11, 2026
db034fa
Put inline links back inside the sentences they interrupt
qcoderx Aug 11, 2026
1007c95
Order a wrapped run by where it starts, not where its box begins
qcoderx Aug 11, 2026
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
102 changes: 102 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
# The browser-free packages carry the tests that must pass everywhere, and
# they run in seconds. Splitting them out means a contributor gets a fast,
# unambiguous signal on the logic that does not need Chromium.
unit:
name: unit (no browser)
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
cache: true

- name: Build
run: go build ./...

- name: Vet
run: go vet ./...

- name: Test
env:
SIEVE_SKIP_BROWSER: "1"
run: >
go test -race ./internal/escalate/... ./internal/corroborate/...
./internal/textnorm/... ./internal/safety/... ./internal/tokens/...
./internal/emit/... ./internal/static/...

# Extraction is only meaningful against a real browser, so the golden corpus
# and the adversarial fixtures run here.
extraction:
name: extraction (Chromium)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
cache: true

- name: Install Chromium
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends chromium-browser || \
sudo snap install chromium
which chromium || which chromium-browser

# Frame production is the least obvious precondition in the whole
# renderer: a tab that is not composited starves requestAnimationFrame
# and therefore IntersectionObserver, which silently turns a rich page
# into an empty artifact. If it is broken on this runner, every
# extraction result below is meaningless, so it is checked first and
# reported as its own step.
- name: Probe frame production
run: go run ./cmd/sieve doctor

- name: Golden corpus
run: go test -count=1 ./internal/graph/...

- name: Render layer
run: go test -count=1 ./internal/render/...

# The latent invariant is the project's central security claim: hidden
# content must never reach a default payload. It is asserted on every
# output format, and it is run as its own step so a failure is
# unmissable rather than buried in a package summary.
- name: Latent tier must not leak
run: go test -count=1 -run 'TestLatent|TestAdversarial' ./internal/emit/... ./internal/graph/...

# Golden files are only useful if a drift is visible. A run that quietly
# rewrote them would defeat the point, so a dirty tree after the tests is a
# failure.
goldens-are-committed:
name: goldens are committed
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: stable
cache: true
- name: Check the tree is clean
run: |
go build ./...
git diff --exit-code || {
echo "::error::The working tree changed during the build."
exit 1
}
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Build artifacts
/sieve
/sieve.exe
/dist/

# Local output
/artifacts/
/scratch-artifacts/
/traces/
*.sieve

# Go
*.test
*.out
176 changes: 176 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
# Contributing to sieve

Thank you for considering it. This document is short and specific, because the
parts of this project that are easy to get wrong are not the obvious ones.

---

## The fastest useful contribution

**A library detector.** `internal/render/fingerprints.json` is a data file, not
code. Adding a detector for an animation library, scroll hijacker, 3D renderer
or site builder takes a pull request against that file plus a fixture — no Go, no
release, no maintainer bottleneck.

```json
{
"n": "my-scroll-library",
"g": "MyScrollLib",
"w": 0.9,
"c": "scroll",
"note": "Translates content with transforms; window.scrollY stays at zero, so document coordinates are meaningless and ordering falls back to checkpoint basis."
}
```

- `g` is a dotted path on `window`; `s` is a CSS selector. Either is enough.
- `w` is how strongly this predicts that a cheap fetch will miss content. A
scroll hijacker is `1.0`; a tooltip library is `0`. Weights at or above `0.85`
set a floor of `sweep` on their own, so be deliberate.
- `note` is surfaced by `sieve doctor`. Write what breaks, not what the library
is.

---

## Reporting an extraction bug

Attach a snapshot. It is the difference between a fix in one round trip and a
conversation across a week.

```sh
sieve distill https://the-site.example --snapshot ./bug.sieve
```

A snapshot records the capture, not the artifact, so the whole graph stage can be
re-run against new code — offline, deterministically, long after the site has
changed.

```sh
sieve replay ./bug.sieve -blocks
```

Do not attach a snapshot recorded with `--private`; those come from
authenticated sessions.

---

## Building and testing

```sh
go build ./...
go test ./... # needs Chromium for some packages
SIEVE_SKIP_BROWSER=1 go test ./... # the rest
```

The browser-free packages — `escalate`, `corroborate`, `textnorm`, `safety`,
`tokens`, `emit` — carry the tests that must pass everywhere. The `graph` and
`render` packages need a browser and are skipped without one.

---

## Golden files

Extraction quality regressions are **silent**. Nothing errors, nothing crashes,
the artifact just quietly says less than it did last week. Golden files are the
only cheap way to notice.

```sh
go test ./internal/graph -run TestGolden # check
go test ./internal/graph -run TestGolden -update # accept a change
```

Blocks are keyed on their structural position plus a hash of their text, not on
sequential ids, so inserting one paragraph shows up as one added line rather
than two hundred renumbered ones. **Read the diff before you `-update`.** A
golden diff nobody reads catches nothing.

---

## Things that will get a pull request sent back

These are not style preferences. Each one has cost this project real time or
would break a claim it makes.

### 1. Anything that lets latent content into a default payload

The latent tier holds exactly the material the visibility filter exists to
exclude. It is safe only while it stays in its box. One convenience shortcut
that flattens the arrays, one emit function that concatenates, and the security
claim in the README is gone rather than weakened.

`TestLatentNeverLeaksIntoDefaultOutput` asserts this on every format. If it
fails, fix the renderer — never the test.

Specifically: no flag on `get_content` that includes hidden content, no
`append(g.Blocks, g.Latent...)`, no "convenience" accessor that merges them.

### 2. Using intercepted payloads as a source of content

The corroboration index answers exactly one question: *does this string appear in
what the site shipped?* It offers no way to enumerate its contents, and that is
deliberate.

Hydration blobs routinely carry draft copy, other locales, unpublished fields and
adjacent records. Emitting any of it stops being extraction and starts being
republishing someone's back office. Confirmation promotes a guess to verified;
absence leaves it speculative. Nothing in the index may become a block.

### 3. Deriving an escalation threshold from the page being judged

Thresholds are pinned constants in `escalate.Thresholds`. A threshold computed
from the page under judgement can move under it, and a tool whose judgement
depends on the weather has no claim to determinism.

If a decision needs new evidence, add a factor with a stated weight — or, when
the evidence is genuinely decisive on its own, a floor in `libraryFloor` with a
comment explaining why arithmetic was not enough.

### 4. Non-determinism in anything that reaches the artifact

Two runs of an unchanged page must produce the same content hash. Things that
have broken this before:

- Iterating a Go map without sorting the keys.
- Feeding a viewport-sampling artefact into a confidence score. `EverVisible`
looked like a visibility signal and was really a record of where the sweep
happened to stop; it made confidence flip between runs.
- A geometry-only settle signature. A pure opacity fade moves nothing, so the
page looked settled while text was still at 0.4 opacity.

If you add a signal, ask what it depends on that is not the page.

### 5. Loosening the politeness floors

Per-host concurrency and interval floors clamp upward and cannot reach zero.
Publishing an identity is permanent: one badly behaved release makes the project
recognisable and blockable forever, for every user.

---

## Style

Follow the surrounding code. A few conventions that are load-bearing rather than
aesthetic:

- **Comments explain why, not what.** The code says what it does. A comment
earns its place by recording a decision, a constraint, or a trap — especially
one that cost time to find.
- **Name things narrowly enough to be true.** `GraphRetention` is not called
coverage because it is not coverage. If a name would be read to mean more than
the value delivers, pick a smaller name.
- **Errors carry instructions.** `ErrChromiumNotFound` tells the user what to
install and which flag to pass. A stack trace is not an error message.
- **Every claim in a comment should be checkable.** If a comment says a test
asserts something, name the test.

---

## Commit messages

One line saying what changed, then a paragraph saying why if the why is not
obvious. Reference an issue if there is one.

---

## Licence

Contributions are accepted under Apache 2.0, the same licence as the project.
Loading
Loading