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
1 change: 1 addition & 0 deletions .skills/context-metrics.jsonl
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
{"actions": ["demote:Bus Conventions", "demote:Environment Variables", "demote:Project Layout", "demote:Server Lifecycle", "demote:Infrastructure", "demote:Retention", "demote:Conventions/logging", "index:Detail Docs", "relink:orphans"], "budget": 6000, "bytes": 14254, "delta_days": null, "delta_tokens": null, "docs_orphaned": 0, "docs_total": 9, "file": "AGENTS.md", "lines": 232, "links_dead": 0, "no_loss": "ok", "note": null, "over_budget": false, "repo": "replicator", "skill_commit": "3fc7b71", "skill_version": "1.2", "tokens": 5773, "tokens_exact": true, "tokens_live": 50759, "top_section": "Code Exploration Policy", "top_section_share": 18, "ts": "2026-08-06"}
{"actions": ["split:content-fetch-issuer-contract", "relocate:Settled->docs/plans", "restructure:payload headings", "index:Where the rest lives", "reindex:context-artifacts"], "budget": 6000, "bytes": 14582, "delta_days": 0, "delta_tokens": 123, "docs_orphaned": 0, "docs_total": 10, "file": "AGENTS.md", "lines": 234, "links_dead": 0, "no_loss": "ok", "note": "#24 doc split: issuer contract 14772->9966, new reference 7349; every live doc under the 10k per-doc budget", "over_budget": false, "repo": "replicator", "skill_commit": "3fc7b71", "skill_version": "1.2", "tokens": 5896, "tokens_exact": true, "tokens_live": 54697, "top_section": "Code Exploration Policy", "top_section_share": 18, "ts": "2026-08-06"}
2 changes: 1 addition & 1 deletion .skills/context-token-ratio
Original file line number Diff line number Diff line change
@@ -1 +1 @@
2.46
2.47
9 changes: 7 additions & 2 deletions .socraticodecontextartifacts.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,17 @@
{
"name": "design-plans",
"path": "./docs/plans/",
"description": "Founding MVP design (2026-06-25, adopted). The command/fact contracts as shipped in co-core v0.7.0 — ContentFetchCommand (URL-addressed, command_id as the wire idempotency key) and BlobAvailableEvent (content_fingerprint, blob_uri, size_bytes, media_type, url, optional command_id) — the stream taxonomy and DLQ naming, the temp-storage interface, the two-level idempotency argument, why fingerprint parity dissolves when Replicator is the sole fetcher, explicit MVP scope cuts, and the build sequence. The founding plan's three open questions were settled 2026-07-31 in a companion doc: blob_uri is a file:// URI over a two-level sharded, content-addressed path (<blob_dir>/<ab>/<cd>/<sha256>.bin, constant .bin extension, temp-file + os.replace so presence means complete); the MVP command issuer is an in-repo scripts/seed_fetch.py rather than Watcher or Archiver; and the MVP boundary is stop-at-fact, with archiver-writeback reclassified from MVP+ to won't-do — Archiver consumes blob_available and writes the SourceRevision itself (archiver#118, which also carries the info_source_id contract gap). Blob retention is out of MVP scope and tracked in #5."
"description": "Founding MVP design (2026-06-25, adopted). The command/fact contracts as shipped in co-core v0.7.0 — ContentFetchCommand (URL-addressed, command_id as the wire idempotency key) and BlobAvailableEvent (content_fingerprint, blob_uri, size_bytes, media_type, url, optional command_id) — the stream taxonomy and DLQ naming, the temp-storage interface, the two-level idempotency argument, why fingerprint parity dissolves when Replicator is the sole fetcher, explicit MVP scope cuts, and the build sequence. The founding plan's three open questions were settled 2026-07-31 in a companion doc: blob_uri is a file:// URI over a two-level sharded, content-addressed path (<blob_dir>/<ab>/<cd>/<sha256>.bin, constant .bin extension, temp-file + os.replace so presence means complete); the MVP command issuer is an in-repo scripts/seed_fetch.py rather than Watcher or Archiver; and the MVP boundary is stop-at-fact, with archiver-writeback reclassified from MVP+ to won't-do — Archiver consumes blob_available and writes the SourceRevision itself (archiver#118, which also carries the info_source_id contract gap). Blob retention is out of MVP scope and tracked in #5. Also holds the relocated resolution of the fetch_failed open question (2026-07-31, moved out of the issuer contract in #24): the consumer arrived in watcher#241, co-core shipped FetchFailedEvent in v0.7.2, and the resolution deliberately did NOT ship non-terminal facts (#9 §3, unbounded growth on an untrimmed broadcast stream) and deliberately left the rows with no usable command_id DLQ-only, because a fact naming no command closes nothing and one naming a foreign command_id would be worse than silence."
},
{
"name": "issuer-contract",
"path": "./docs/contracts/content-fetch-issuer-contract.md",
"description": "Normative contract for any service publishing content.fetch commands (seed_fetch.py today, Watcher from Phase 4) — and its permanent home, linked to rather than copied by issuer-side repos. Opens with the frame shape, because getting it wrong is the likeliest and quietest mistake: what lands on the stream is a co-core envelope produced by to_wire (key, payload as JSON, event_type, schema_version, occurred_at, content_type), never the model's fields flattened, and an XADD of top-level command_id/url fails from_wire into content.fetch.dlq silently. key is derived by to_wire (command_id for a command, content_fingerprint for a fact) and is not what Replicator dedupes on — that is the decoded payload.command_id — but it is what makes a DLQ entry correlatable, so an issuer can tail content.fetch.dlq with a plain XREAD and close a pending entry deterministically instead of waiting out a timeout. Then why the wire carries no info_source_id and what that pushes onto the issuer: mint a fresh ULID command_id per fetch occasion (a resource-stable id is silently deduped away for 24h by replicator:cmd:<id>); url is one-to-many onto InfoSources and therefore not a correlation key; persist the command_id -> domain map before publishing, outbox-style; correlation must be idempotent because a crash between publish and the dedupe SET emits a second fact under the same command_id; consumers must not dedupe their inbox on content_fingerprint, which is storage identity rather than correlation identity; there is no fetch_failed fact, so silence is the only failure signal and the issuer needs its own pending-entry reaper that re-issues rather than concluding loss; blob_uri is a host-local file:// URI whose 7-day TTL runs from last fetch-reference, not last consumer read. Carries the full failure taxonomy (which conditions dead-letter to content.fetch.dlq silently, which retry indefinitely, which park in the PEL under the byte ceiling), what Replicator does and does not guarantee, the unauthenticated-capability trust posture resting on Archiver's localhost bus ACL, and the deferred fetch_failed fact as a co-core change. Written for #8."
"description": "Normative contract for any service publishing content.fetch commands (seed_fetch.py today, Watcher from Phase 4) — and its permanent home, linked to rather than copied by issuer-side repos. Split in #24 into this read-through half and the lookup half in issuer-reference; both are normative and the path sibling repos link is this one. Opens with the frame shape, because getting it wrong is the likeliest and quietest mistake: what lands on the stream is a co-core envelope produced by to_wire (key, payload as JSON, event_type, schema_version, occurred_at, content_type), never the model's fields flattened, and an XADD of top-level command_id/url fails from_wire into content.fetch.dlq silently. key is derived by to_wire (command_id for a command, content_fingerprint for a fact, command_id:occurred_at for a failure) and is not what Replicator dedupes on — that is the decoded payload.command_id. Then the payload section: the command table with the tz-aware occurred_at rule, a summary of the request-options guards (refused not adjusted, identity untouched) pointing at the reference for the refusal list itself, the BlobAvailableEvent field table with the four rules for reading the enriched fields, and the FetchFailedEvent table with its reason tokens. Then the seven MUSTs, which are the contract proper: mint a fresh ULID command_id per fetch occasion (a resource-stable id is silently deduped away for 24h by replicator:cmd:<id>); persist the command_id -> domain map before publishing, outbox-style; url is one-to-many onto InfoSources and therefore not a correlation key; correlation must be idempotent because a crash between publish and the dedupe SET emits a second fact under the same command_id; consumers must not dedupe their inbox on content_fingerprint, which is storage identity rather than correlation identity; handle fetch_failed but keep a reaper anyway, because four conditions still produce nothing; and copy the bytes before the blob's 7-day TTL, which runs from last fetch-reference rather than last consumer read. Closes with what Replicator guarantees (store-then-publish, announce-then-ack, command_id echoed, the fingerprint is definitional, at-least-once) and what it does not (no non-terminal fact, no latency bound, no issue-rate promise under per-host pacing, no ordering, no cross-command dedupe, no retention on content.blobs). Written for #8, split for #24."
},
{
"name": "issuer-reference",
"path": "./docs/contracts/content-fetch-issuer-reference.md",
"description": "Lookup half of the content.fetch issuer contract, split out in #24 so the contract itself stays readable start to finish. Equally normative — a rule does not become advisory by living here. Carries: the request-options rules (#11) — header names lower-cased before the merge so the issuer wins rather than httpx sending two User-Agent lines, surrounding whitespace dropped from a value and nothing else, and the full refusal list, where every entry is refused rather than adjusted with a terminal fetch_failed / invalid_request_options before any request goes out (hop-by-hop headers, host and content-length which httpx derives, any proxy-*, names that are not RFC 9110 tokens, values with any byte outside \\x20-\\x7e because a CRLF is request splitting, two names differing only in case, more than 32 headers or 8192 bytes of them, and timeout_seconds that is non-finite or over REPLICATOR_MAX_FETCH_TIMEOUT_SECONDS because the consume path is serial). The reasoning behind the six enriched blob_available fields: None means nobody said rather than the default, status_code is always 2xx on this fact so a branch on == 200 drops a 203, verbatim excludes surrounding whitespace and an over-long value is dropped rather than truncated, they are per-occasion values on a fingerprint-keyed fact, and the live conditional-GET trap where a matching validator earns a body-less 304 that Replicator still closes as a terminal fetch_failed (#17). The full failure taxonomy table — every condition, Replicator's action, and what the issuer sees — with the three classes of silent row analysed (no usable command_id, silent only while in flight, and the duplicate-command_id row that is silent because the command already succeeded). The four silent conditions in full. How to read the DLQ with a plain XREAD and no consumer group, and the synthesized-record anomaly that carries no key. The provenance and trust posture: content.fetch is an unauthenticated capability resting entirely on Archiver's localhost bus ACL, which headers widens, and the two properties an issuer can rely on (refused before any request goes out; header values never reach the journal). Plus the co-core version history, what the envelope key is operationally for, and how pacing behaves at the deployed defaults."
},
{
"name": "boundaries-charter",
Expand Down
8 changes: 5 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,10 @@ Replicator is a **consumer** first. Follow the conventions co-core and the archi
requires `--production` for the one combination the live worker consumes — a
frame there is fetched for real.
- **Two normative contracts bound the wire and the roadmap**, both under
`docs/contracts/` and linked from sibling repos. `tests/test_boundaries.py`
enforces eight charter invariants in CI; change a charter and its tests
together.
`docs/contracts/` and linked from sibling repos. The issuer contract splits
into a read-through half and a lookup half (`-contract` / `-reference`, #24);
both are normative. `tests/test_boundaries.py` enforces eight charter
invariants in CI; change a charter and its tests together.

Blob paths, modes, and the retention sweep: [docs/STORAGE.md](docs/STORAGE.md).
Fakeredis's divergences from the live broker, and the keys an integration run may
Expand Down Expand Up @@ -229,4 +230,5 @@ are deliberately not JSON: [docs/STYLE.md](docs/STYLE.md).
- [docs/COMMANDS.md](docs/COMMANDS.md) — every runnable command, with flags
- [docs/SKILLS.md](docs/SKILLS.md) — vendored skill inventory and refresh procedure
- [docs/contracts/content-fetch-issuer-contract.md](docs/contracts/content-fetch-issuer-contract.md) — what a `content.fetch` producer must do; normative, linked from issuer repos
- [docs/contracts/content-fetch-issuer-reference.md](docs/contracts/content-fetch-issuer-reference.md) — its lookup half: the refusal list, the failure taxonomy, the silent conditions, trust posture
- [docs/contracts/replicator-boundaries.md](docs/contracts/replicator-boundaries.md) — what Replicator may become; run its three tests against any proposed capability
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@ The founding design lives in

**Issuing `content.fetch` commands?** Read
[`docs/contracts/content-fetch-issuer-contract.md`](docs/contracts/content-fetch-issuer-contract.md)
first — it is the normative issuer contract and its permanent home. Publish through co-core's
`to_wire`, never hand-rolled fields; and because the wire carries no domain identity, correlation is
entirely the issuer's job. Most ways of getting either wrong fail silently.
first — it is the normative issuer contract and its permanent home, with the refusal list, failure
taxonomy and trust posture in its companion
[`content-fetch-issuer-reference.md`](docs/contracts/content-fetch-issuer-reference.md).
Publish through co-core's `to_wire`, never hand-rolled fields; and because the wire carries no
domain identity, correlation is entirely the issuer's job. Most ways of getting either wrong fail
silently.

## Shape

Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ deploy/ — Systemd unit + deployment config

Each rule `AGENTS.md` states in one line, with the reasoning that makes it non-negotiable:

- **The issuer contract is written down and lives here.** `docs/contracts/content-fetch-issuer-contract.md` is the normative statement of what a `content.fetch` producer must do — per-occasion `command_id`, `url` is not a correlation key, persist the `command_id → domain` map before publishing, correlate idempotently, and keep a reaper as a backstop for the outcomes no fact can carry. Issuer-side repos (Watcher, Phase 4) link to it rather than copying it. Anything asserted there is asserted about this repo's code; change one, change both (#8).
- **The issuer contract is written down and lives here.** `docs/contracts/content-fetch-issuer-contract.md` is the normative statement of what a `content.fetch` producer must do — per-occasion `command_id`, `url` is not a correlation key, persist the `command_id → domain` map before publishing, correlate idempotently, and keep a reaper as a backstop for the outcomes no fact can carry. Its companion `docs/contracts/content-fetch-issuer-reference.md` (#24) carries the lookup half — the request-options refusal list, the failure taxonomy, the four silent conditions, and the trust posture — and is equally normative. Issuer-side repos (Watcher, Phase 4) link to the contract rather than copying it. Anything asserted in either is asserted about this repo's code; change one, change both (#8).
- **What Replicator is allowed to become is also written down.** `docs/contracts/replicator-boundaries.md` is the sibling charter: mechanism to Replicator, policy to the issuer, config over the bus, and an inbound admin HTTP API rejected by name. Run its three tests against any proposed capability, field, or setting before writing code — a database, domain vocabulary, or a write route is reached one defensible step at a time, not in one commit. `tests/test_boundaries.py` enforces eight invariants in CI, including the one that catches the regression review misses: an AST scan of `src/` for domain nouns in identifiers *and* string literals. Known violation recorded and pinned rather than omitted — `blob_uri` is host-local `file://` (#7). Change the charter and the tests together (#12).
- **`content.blobs` carries both outcomes.** `blob_available` on success, `fetch_failed` on a command closed without bytes (#9, co-core cannobserv#270 — v0.7.2). One stream so an issuer's single consumer group sees either. The reason is named at the *raise site* (`PermanentFetchError.reason`), never recovered from a message string, because three unrelated permanent conditions share one exception type. Three rows stay DLQ-only and permanently silent, and only one of them for want of an id: a frame that did not decode, a command whose `command_id` is blank (refused before the fetch — an empty id would otherwise take the dedupe key `replicator:cmd:` and make every later blank-id command a silent no-op, CR #6), and a frame that decoded to a **non-command payload** — the last is unreportable not because it lacks a `command_id` but because any it carries is *another command's* (`BlobAvailableEvent`'s names one that succeeded), so a terminal fact keyed on it would contradict a fact the issuer already applied (CR #1). `_close` refuses a correlator-less report at the one choke point rather than at each call site. Non-terminal facts are deferred (#9 §3): the stream is broadcast and nothing trims it, so a fact per reclaim during an origin outage is unbounded growth. `src/worker/reporter.py`.
- **`blob_available` carries the fetch, not just the bytes.** Six optional fields beyond the blob itself (#10, cannobserv#271 + #279 — v0.7.5): `final_url`, `status_code`, `fetched_at`, `content_type_raw`, `etag`, `last_modified`. Each is what Replicator holds at publish time and a broadcast consumer cannot recover now that fetching lives here rather than in Watcher. **`None` means nobody said it** — never a stand-in: `final_url` is never backfilled from `command.url` (an issuer could no longer tell "landed where I asked" from "nobody knows"), and `content_type_raw` is never backfilled with `DEFAULT_MEDIA_TYPE` (which is the value a consumer reads as "unknown, guess from the URL"). `media_type` keeps its normalized semantics beside the raw channel; the two are not interchangeable. `fetched_at` is stamped where the fetch *returns*, not at publish — `occurred_at` under a reclaim is minutes late. `status_code` is always 2xx on this fact, so it distinguishes 200 from 203/206 and is not a success branch. The passthroughs are **dropped over `MAX_HEADER_VALUE_LENGTH`, never truncated**: these are origin-controlled strings on a stream nothing trims, and a truncated ETag replayed in an `If-None-Match` is a validator that can never match. `src/worker/handler.py::_passthrough`.
Expand Down
2 changes: 1 addition & 1 deletion docs/COMMANDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ uv run python -m scripts.seed_fetch \
The script rejects a malformed `--header` and a repeated name (exit 2) but deliberately does
**not** pre-empt the worker's refusal list — sending a refused header is how the refusal is
exercised against a live worker. The full list is in
[`docs/contracts/content-fetch-issuer-contract.md`](contracts/content-fetch-issuer-contract.md).
[`docs/contracts/content-fetch-issuer-reference.md`](contracts/content-fetch-issuer-reference.md).

Watch the other side with `sudo journalctl -u replicator -f`.

Expand Down
Loading
Loading