−
100%
+
fit
scroll
# Siphon Rules — Seeding Guidance **Day 350
· Φωτίζων (Lumen)** — companion
scroll to FID `siphon-rule-schema` gap
`seed-rules`. The siphon-rule registry,
worker, and retro pass are all wired and the
build is green. Auto-siphoning is *dormant*
until the first `sapphire=siphon-rule` Thing
exists in Portal. This scroll documents the
schema, lifecycle, and minting recipe — so
the operator (you) can seed rules deliberately
rather than from rote. --- ## Schema (locked
in FID siphon-rule-schema · ruby-fields ✓)
Rule fields distributed by gem canon: | Gem |
Field | Required | Purpose |
|-----|-------|----------|---------| |
**sapphire** | `"siphon-rule"` | yes |
archetype | | **onyx** | `<rule-name>` | yes |
identity — descriptive, kebab-case | |
**topaz** | `.priority` | yes | integer;
**lower runs first** | | **ruby** | `.active`
| yes | `true`/`false` master switch | |
**ruby** | `.filter_sapphire` | one filter
required | match Things with this sapphire | |
**ruby** | `.filter_onyx_contains` | optional
filter | substring match on onyx | | **ruby**
| `.filter_ruby_key` | optional filter |
`"<key>:<value>"` matches `ruby.<key> ==
value` | | **diamond** | `.target_box` | yes |
destination Box UUID | | **diamond** |
`.filter_ring` | optional filter | match
Things in this ring UUID | A rule is **valid**
iff: it has `target_box` AND at least one
`filter_*`. Multiple filters AND together.
Rules with no filters are skipped by the
loader. --- ## Lifecycle 1. **Create the
target Box first**: `▲ box create <name>
[description]` → returns a Box UUID. 2.
**Mint the rule Thing**. Two approaches: **a)
Via individual ▲ gem calls** (works today,
chord-validated): ``` ▲ gem <rule-name>
sapphire siphon-rule ▲ gem <rule-name> onyx
<rule-name> ▲ gem <rule-name> topaz.priority
100 ▲ gem <rule-name> ruby.active true ▲
gem <rule-name> ruby.filter_sapphire updated
▲ gem <rule-name> diamond.target_box
<box-uuid> ``` Note: ▲ gem updates existing
Things. To *create* a new Thing, either forge
it via another path first, or use direct
SQL/MCP create. **b) Direct minimal create +
gem fill** is cleanest for now. A future `▲
siphon rule new <name> <sapphire> <box>`
command would wrap this — not yet built (see
future-work below). 3. **Restart the daemon**
with `PORTAL_SIPHON_WORKER=1`: ```
PORTAL_SIPHON_WORKER=1 ▲ mesh deploy ``` The
registry loads at startup
(`siphon_rules::init()`); rules created
*after* startup are invisible until the next
restart. (FID `siphon-rule-registry` gap
`rule-reload` is deferred — adds hot-reload
via ArcSwap; daemon restart works for v1.) 4.
**Verify** — create a Thing matching a
rule's filters; check
`gem_values->'diamond'->>'box'` is populated
within a heartbeat. Daemon log shows: `△
siphon-worker: <uuid> → box <uuid> (rule
'...')`. 5. **Retro pass** (optional, opt-in):
- On daemon start: also set
`PORTAL_SIPHON_BOOTSTRAP=1`. - Or run live:
`△ siphon apply-rules` (no env var needed).
- Both route through `bootstrap_retro()`; each
rule processed in 1000-row batches. --- ##
What to seed first — operator decisions The
gap originally proposed seeding these core
sapphires: **updated, video, scroll, gap,
transaction** (NOT command-history — worker
explicitly skips that sapphire along with moon
to avoid feedback loops). Open questions for
the operator: - **One box per sapphire**, or
**thematic boxes** (e.g. one "media" box
catching both video + audio + image sapphires
via separate rules)? - **Active-by-default**
or **shadow mode**? You can seed rules with
`ruby.active=false` to keep them dormant while
inspecting via `△ siphon apply-rules`
dry-runs (the apply-rules command honors
active flag — inactive rules are filtered
out at load time, so a shadow rule is truly
inert). - **Priority strategy**: lower
priority = runs first. Recommend leaving
generic rules at `100` and specific ones at
`10–50` so they win. --- ## Safety net -
Worker is **opt-in** via
`PORTAL_SIPHON_WORKER` env var. If unset,
events fire but no rules evaluate. - Bootstrap
retro is **doubly opt-in** — needs both
`PORTAL_SIPHON_WORKER` AND
`PORTAL_SIPHON_BOOTSTRAP`. Production restarts
cannot accidentally siphon the 99k-Things
table. - `skip-already-boxed` is enforced in
both worker (in-memory check) and retro pass
(SQL WHERE clause). The worker never
overwrites operator-assigned boxes. - Box and
ring sapphires are excluded everywhere —
siphon never moves infrastructure. --- ##
Implementation pointers | Concern | File ·
Line | |---|---| | Registry struct + cache +
init | `src/things/siphon_rules.rs` | |
In-memory matcher |
`src/things/siphon_rules.rs` ·
`SiphonRule::matches`, `match_rule` | | Worker
subscriber loop | `src/server/siphon.rs` ·
`run`, `handle_created` | | Retro bulk apply |
`src/server/siphon.rs` · `bootstrap_retro`,
`apply_rule_batch` | | Spawn point |
`src/server/mod.rs` (search
`PORTAL_SIPHON_WORKER`) | | CLI surface |
`src/commands/compress.rs` ·
`cmd_stem_siphon` (`apply-rules` branch) | |
ThingMutated emit (ingest path) |
`src/db/mod.rs` · `create_thing_minimal` | |
ThingMutated emit (sacred docs) |
`src/db/rings.rs` · `create_sacred_document`
| --- ## Future work (open gaps remaining) -
**rule-reload** (siphon-rule-registry) —
refactor `OnceLock<Vec<_>>` →
`OnceLock<ArcSwap<Vec<_>>>`; subscribe to
`gem-updated` events on `sapphire=siphon-rule`
Things; swap registry contents live. Removes
the daemon-restart requirement when
adding/editing rules. -
**siphon-filter-expansion** (4 gaps) —
extend the *manual* `△ siphon` CLI parser
(compress.rs `cmd_stem_siphon`) to accept
`ring=`, `onyx_contains=`, `ruby_key=`
filters. The registry and worker already
support these filter types; this gap is purely
about exposing them on the manual one-off
command. --- > *"A rule unseeded is doctrine
without practice. Seed deliberately; > verify
before scaling."* — Φωτίζων, Day 350
△