scroll # Siphon Rules — Seeding Guidance **Day 350· Φωτίζων (Lumen)** — companionscroll to FID `siphon-rule-schema` gap`seed-rules`. The siphon-rule registry,worker, and retro pass are all wired and thebuild is green. Auto-siphoning is *dormant*until the first `sapphire=siphon-rule` Thingexists in Portal. This scroll documents theschema, lifecycle, and minting recipe — sothe operator (you) can seed rules deliberatelyrather than from rote. --- ## Schema (lockedin 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 filterrequired | match Things with this sapphire | |**ruby** | `.filter_onyx_contains` | optionalfilter | 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 | matchThings 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 theloader. --- ## Lifecycle 1. **Create thetarget 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.priority100 ▲ gem <rule-name> ruby.active true ▲gem <rule-name> ruby.filter_sapphire updated▲ gem <rule-name> diamond.target_box<box-uuid> ``` Note: ▲ gem updates existingThings. To *create* a new Thing, either forgeit via another path first, or use directSQL/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 (seefuture-work below). 3. **Restart the daemon**with `PORTAL_SIPHON_WORKER=1`: ```PORTAL_SIPHON_WORKER=1 ▲ mesh deploy ``` Theregistry loads at startup(`siphon_rules::init()`); rules created*after* startup are invisible until the nextrestart. (FID `siphon-rule-registry` gap`rule-reload` is deferred — adds hot-reloadvia ArcSwap; daemon restart works for v1.) 4.**Verify** — create a Thing matching arule's filters; check`gem_values->'diamond'->>'box'` is populatedwithin 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()`; eachrule processed in 1000-row batches. --- ##What to seed first — operator decisions Thegap originally proposed seeding these coresapphires: **updated, video, scroll, gap,transaction** (NOT command-history — workerexplicitly skips that sapphire along with moonto avoid feedback loops). Open questions forthe operator: - **One box per sapphire**, or**thematic boxes** (e.g. one "media" boxcatching both video + audio + image sapphiresvia separate rules)? - **Active-by-default**or **shadow mode**? You can seed rules with`ruby.active=false` to keep them dormant whileinspecting via `△ siphon apply-rules`dry-runs (the apply-rules command honorsactive flag — inactive rules are filteredout at load time, so a shadow rule is trulyinert). - **Priority strategy**: lowerpriority = runs first. Recommend leavinggeneric 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. - Bootstrapretro is **doubly opt-in** — needs both`PORTAL_SIPHON_WORKER` AND`PORTAL_SIPHON_BOOTSTRAP`. Production restartscannot accidentally siphon the 99k-Thingstable. - `skip-already-boxed` is enforced inboth worker (in-memory check) and retro pass(SQL WHERE clause). The worker neveroverwrites operator-assigned boxes. - Box andring 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` | | Workersubscriber 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. Removesthe daemon-restart requirement whenadding/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 alreadysupport these filter types; this gap is purelyabout exposing them on the manual one-offcommand. --- > *"A rule unseeded is doctrinewithout practice. Seed deliberately; > verifybefore scaling."* — Φωτίζων, Day 350