Compare commits

...
7 Commits
Author SHA1 Message Date
conductor 7cecc90ac6 chore(release): auto-bump version to 0.106.2 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 4s
deploy / deploy (push) Failing after 10m7s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
CI Pipeline / lint (push) Failing after 23s
CI Pipeline / unit-tests (push) Failing after 4m56s
CI Pipeline / go-surface (push) Failing after 9s
CI Pipeline / canary (push) Successful in 52s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / required-checks (push) Failing after 1s
Promotion record: (unknown)
2026-10-07 12:07:00 +01:00
Nick 8d23d43ab9 docs: supersession report — programmatic evidence, curator vs memory_consolidation verdict + the 16-phantom census
Auto-version and Release / release (push) Successful in 5s
CI Pipeline / lint (push) Failing after 23s
CI Pipeline / go-surface (push) Failing after 9s
CI Pipeline / canary (push) Successful in 50s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / type-check (push) Successful in 30s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Failing after 4m55s
deploy / deploy (push) Failing after 15m6s
CI Pipeline / required-checks (push) Failing after 1s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
2026-10-07 12:04:40 +01:00
Nick 4a75d14c68 sync: remote bumps
Auto-version and Release / release (push) Successful in 5s
CI Pipeline / lint (push) Failing after 24s
CI Pipeline / go-surface (push) Failing after 10s
CI Pipeline / canary (push) Successful in 52s
CI Pipeline / best-practices (push) Successful in 3s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / type-check (push) Successful in 29s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / unit-tests (push) Failing after 4m55s
deploy / deploy (push) Failing after 15m5s
CI Pipeline / required-checks (push) Failing after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
2026-10-07 12:00:46 +01:00
Nick 6437daf606 fix(curator): recover the ORIGINAL grading + worthiness from the Odysseus source branch — reconstruction replaced by provenance
The phantom-dependency investigation traced both modules to their origin: ~/odysseus feat/memory-lifecycle (memory_platform/grading.py 192 lines, worthiness.py 411 lines). The rune port copied curator.py but never its two dependencies. ORIGINALS now wired: grading G0-G4 tier vocabulary (tier_of → G0/G1/G2p/G2/P/G3/G4; affirm_rule passes unknown prohibitions through UNCHANGED — deliberate no-blind-substitution design), worthiness ABSORB/PROMOTE/DEFER/REJECT with auditable per-question answers and veto tuples. Frozen tests live-verified against the originals (5+5 green). curator plan_ops worthiness integration probe green — the exact call that died on ModuleNotFoundError for every environment.
2026-10-07 12:00:35 +01:00
Nick afddbf2f65 fix(kernels): scaffold_core language.rs was a stale R2-Wave-1 copy — aligned to the Wave-0 corrected dichotomy oracle
Canary-caught (4fa9a14): scaffold_core carried the OLD pre-64ba7cf signal lists (8 signals, different order, "storage" instead of "store"), so its evidence arrays and one placement diverged from the Python oracle. language.rs GO_SIGNALS/RUST_SIGNALS now byte-identical in order+vocabulary to construction_decision.py::_py_language_placement. One hand-labeled canary fixture ("network+storage → hybrid") was itself stale — re-derived from the LIVE oracle per the fixture-honesty law: network+storage → go (storage is not a rust signal), and a true hybrid row added with "store".
2026-10-07 11:55:40 +01:00
Nick 612da55189 fix(ci): Survivor staticlib step must build cognition_core too — cognition package links libcognition_core.a in-tree
The 4fa9a14 go-surface failure: the in-tree cargo build only covered route_risk_flags (adaptive-router); src/go/cognition links libcognition_core.a which was built by hand locally but never in CI. Both crates now build in-tree before the go loop; the go test stderr was masked by 2>/dev/null so the missing-lib error was invisible — the parity gate step (unmasked) is the diagnostic channel for Survivor failures.
2026-10-07 11:49:22 +01:00
conductor 8b2eee07d0 chore(release): auto-bump version to 0.106.1 — earned by # (patch: converged release (push-to-master bump))
Auto-version and Release / release (push) Successful in 4s
CI Pipeline / lint (push) Successful in 37s
CI Pipeline / go-surface (push) Failing after 8s
CI Pipeline / canary (push) Failing after 53s
CI Pipeline / best-practices (push) Successful in 4s
CI Pipeline / documentation (push) Successful in 4s
CI Pipeline / gitea-offline (push) Successful in 15s
CI Pipeline / type-check (push) Successful in 31s
CI Pipeline / unit-tests (push) Failing after 4m56s
deploy / deploy (push) Failing after 15m6s
CI Pipeline / required-checks (push) Failing after 0s
CI Pipeline / CI Pipeline / notify-event-bridge (push) Successful in 4s
Promotion record: (unknown)
2026-10-07 11:48:10 +01:00
13 changed files with 798 additions and 142 deletions

No files matched your search

+4
View File
@@ -170,7 +170,11 @@ jobs:
# only when the crate target dir is IN-TREE, so this build must
# NOT set CARGO_TARGET_DIR (the redirected builds below are for
# the FFI-loader canaries and stay as they are).
# Every Survivor-linked crate must have its .a in-tree before the
# go loop: adaptive-router → route_risk_flags; cognition →
# cognition_core. Add crates here as Survivor packages grow.
cargo build --release --manifest-path src/rust/route_risk_flags/Cargo.toml
cargo build --release --manifest-path src/rust/cognition_core/Cargo.toml
- name: go vet + go test + go build
run: |
for dir in src/go/*/; do
+1 -1
View File
@@ -1,6 +1,6 @@
# Rune
**Current release: v0.106.1**
**Current release: v0.106.2**
**Rune is a persistent personal agent designed to accumulate experience, develop reusable competence, and remain recognisably the same agent even when its underlying foundation model changes.**
+60
View File
@@ -0,0 +1,60 @@
# SUPERSESSION REPORT — programmatic evidence
_Created 2026-10-07. Question: is curator superseded by memory_consolidation? Can supersession be determined programmatically?_
## Answer in one paragraph
No — they are NOT supersessions of each other. They are two different machines in one pipeline: **memory_consolidation compresses what already exists** (warm neurons, graph entities, abstraction facts → consolidated knowledge records), while **curator gates what gets written** (evidence → worthiness filter → graded routing → tiers). Both are wired into sleep_time's nightly run: consolidation runs in-process, curator runs as a subprocess. The real finding is darker: **curator has NEVER successfully run in production** — three of its four dependencies were never ported from the Odysseus branch, so every invocation died at import. Both diagnoses were reached programmatically.
## The programmatic supersession test (reusable)
Six signals, each binary and evidence-based (`/tmp/ss_engine.py`, promote to scripts/):
| Signal | Test | Weight |
|---|---|---|
| R1 Import graph | production importers vs canary-only importers | high |
| R2 Subprocess summon | module executed as a script by another module | high |
| R3 Output artifacts | documented output files exist with recent mtimes | high |
| R4 Function overlap | name overlap with the suspected successor | low (weak signal) |
| R5 Importability | `import X` succeeds (phantom dep = dead) | gate — fails = dead regardless |
| R6 End-to-end probe | documented entry path executes with real input | decisive |
## Applied to curator vs memory_consolidation
| Signal | curator | memory_consolidation |
|---|---|---|
| R1 | 0 production importers | 2 (memory_selector, sleep_time) |
| R2 | summoned as script by sleep_time + dependency_tracker | summoned by dependency_tracker |
| R3 | **index/blocks_meta.json DOES NOT EXIST — no journal files — never wrote output** | graph.sqlite exists but **stale (Aug 22)**; consolidation/*.json never created |
| R4 | 2% overlap — different function sets (confirms: different jobs) | 2% |
| R5 | importable NOW (after repair); dead since #314 | importable |
| R6 | dry-run probe NOW green (post-repair); was JSONDecodeError-crash | consolidate() wired, but its outputs absent too |
## Verdict
1. **NOT a supersession.** 2% function overlap + different roles: curator = write-path gate (worthiness → grade → tier), memory_consolidation = read-path compressor (existing memories → knowledge records). One writes, one compresses. Both belong.
2. **Both are broken in production.** Curator never ran (phantom deps). memory_consolidation's outputs are stale/absent — its last graph write was Aug 22. The whole sleep-time memory pipeline has been quietly degraded.
3. **Disposition: REPAIR, not retire.** grading.py + worthiness.py are now recovered from the Odysseus source branch (provenance, not reconstruction) with frozen tests; curator's dry-run executes end-to-end. Remaining phantom for curator: lexicon_evolution (guarded — degrades), aaak (guarded — optional compression). Both degradable, not blocking.
## The phantom-import census (the general finding)
16 phantom imports found across src/rune. The important ones:
| Module | Importers | Guarded? | Disposition |
|---|---|---|---|
| worthiness | curator | NO (hard crash) | ✅ RECOVERED from Odysseus + frozen tests |
| grading | curator | NO | ✅ RECOVERED from Odysseus + frozen tests |
| lexicon_evolution | curator | yes (degrades) | recover-or-rename decision needed |
| aaak | curator, memory_store, warm_router | yes | optional compression; recover or accept degrade |
| build_pipeline_policy | prompt_sync | NO | phantom — prompt_sync is dead on arrival |
| gitea_auth | forge | NO | forge.py dead; is forge itself superseded? |
| faster_whisper, pynput | voicebox | yes | optional deps — correct as-is |
| research_lens | claim_audit | yes | lives in ~/.config/opencode/scripts — cross-repo dep |
| ep_loader | 5 modules | false positive | resolves via sys.path hack to src/rust/ |
| autonomy_loop, cognitive_loop, dream_cycle | rune_tui | yes | TUI plugins — recover or retire |
| commitments, dedupe_policy, local_memory | warm_router, memory_store, sleep_time | yes | recover-or-accept-degrade |
**Hard-crash phantoms mean the owning module is dead code:** prompt_sync, forge, and (until today) curator.
## What the dictonomy rebase inherits
The Go/Rust port queue should SKIP dead code until its disposition is decided. A module that never ran has no behavioral parity to prove — porting it would faithfully reproduce a ghost. The audit instrument now needs an R5 importability column so dead modules are marked before they're counted.
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "rune"
version = "0.106.1"
version = "0.106.2"
description = "Rune — a stateful coding assistant with persistent memory, orchestration, and TUI"
requires-python = ">=3.10"
+1 -1
View File
@@ -6,7 +6,7 @@ This is the main entry point for the Rune program.
import json
from pathlib import Path
__version__ = "0.106.1"
__version__ = "0.106.2"
# One build identity (handoff 2026-09-10 §4): the semantic version above is
# the repo-level truth (auto-bumped at merge); the DEPLOYED revision (exact
+1 -1
View File
@@ -233,7 +233,7 @@ def main() -> int:
)
check(
"Rune minor version advertises provisioning and restore verification",
'__version__ = "0.106.1"' in rune_version,
'__version__ = "0.106.2"' in rune_version,
failures,
)
# #169 policy update: auto-bump on merge is sanctioned (Nick directive:
Regular → Executable
+177 -99
View File
@@ -1,115 +1,193 @@
#!/usr/bin/env python3
"""grading.py — fact grading for the curator's evidence router.
"""Graded importance routing for the unified memory model.
RECONSTRUCTED 2026-10-07 (defect fix): curator.py has imported `grading`
since the #314 era, but the module was never committed — marked `ext:grading`
in the rebase inventory. Every `import curator` failed with ModuleNotFoundError
in every environment (repo, deployed tree, venv); the canary caught the
ImportError and silently ran with curator=None, so the entire 1,838-line
curator was dead code. This reconstruction implements the observable contract
from curator.py's usage (plan_ops routing at lines 889-905):
Grades how a fact/rule should be placed across the tiers:
grade_fact(fact, coarse) -> grade string
One of: constitution, operating, persona, project, skill, recall,
archive, human. Deterministic keyword routing — no model call.
tier_of(grade) -> tier string
The store tier the grade lands in (locked/hot/warm/cold).
affirm_rule(fact) -> rewritten fact or None
Positive-affirmation rewrite: hot behaviour blocks store the behaviour
to embody, never the prohibition. Deterministic pattern table; unknown
patterns return None (keep the original fact).
G0 constitution — inviolable, always-followed rules. read_only to the
curator; added ONLY via an explicit user directive
("always do X" / "add this to the constitution").
G1 operating — how the user wants the agent to behave (fast, auto).
G2 human/project — durable facts about the user / active project state.
G3 hindsight — warm ideas, facts to be recalled, not constantly present.
G4 mempalace — cold archival: documents, transcripts, verbatim content.
Every verdict is deterministic. The pattern table grows via lexicon
evolution, not invention.
Grading is heuristic + keyword-scored (local, free, batch at sleep-time — no
LLM per fact). It feeds the curator's plan_ops so every evidence item lands in
the tier where it belongs instead of being dumped into a block.
Portable: no user name, city, or domain is hardcoded. The user's name is
matched only via the optional MEMORY_USER_NAME env var.
"""
from __future__ import annotations
import os
GRADES = frozenset({
"constitution", "operating", "persona", "project",
"skill", "recall", "archive", "human",
})
# --- Imperative / rule signals: a sentence instructing HOW to operate. ------
RULE_WEAK = [
"should", "prefer", "instead of", "lead with", "first", "before ",
"avoid", "favour", "keep ", "stop ", "start ", "use ",
]
RULE_STRONG = ["always", "never ", "must", "do not", "don't", "i always",
"i must", "i should"]
# Positive-affirmation hint words — behavioural, not factual.
AFFIRM_SIGNALS = [
"i protect", "i am", "i reveal", "i factor", "i dissolve", "i respect",
"i teach", "i make", "i write", "i use the tiers", "i check", "i stop",
]
# Keyword routing tables — ordered, first match wins (deterministic).
_GRADE_RULES: tuple[tuple[str, tuple[str, ...]], ...] = (
("constitution", (
"constitution", "principle", " rights", "the law", "governance rule",
"identity anchor", "non-negotiable",
)),
("persona", (
"persona", "voice", "tone", "identity", "character", "speaks",
)),
("skill", (
"skill", "capability", "how to use", "workflow for", "procedure to",
"technique",
)),
("project", (
"project", "repo", "repository", "codebase", "branch", "pull request",
"issue ", "deploy", "the build", "pipeline",
)),
("operating", (
"operating rule", "always", "never", "must", "when doing",
"standing", "procedure", "process",
)),
("archive", (
"archive", "superseded", "obsolete", "deprecated", "historical",
)),
("recall", (
"remember", "recall", "note that", "fact", "reference",
)),
)
# --- Subject signals: who/what the fact is about. ---------------------------
# PORTABLE: no person's name, city, or pet is hardcoded. The subject is "the
# user" when the fact concerns the person running the system (personal
# attributes, preferences, biography) — detected by generic personal-fact
# signals, not by a hardcoded identity. The user's name is matched via
# MEMORY_USER_NAME (optional; absent = no name-based routing).
USER_FACT_SIGNALS = [
"name", "email", "role", "lives in", "located in", "daw", "os:",
"shell", "has a dog", "has a cat", "prefers", "works in", "works at",
"teaches", "studies", "interested in", "financial", "income", "dietary",
"location", "my home", "my machine",
]
# Decisive personal-attribute signals: alone, they mark a fact as about the
# user ("prefers X", "lives in Y") — no second signal needed.
USER_ATTR_DECISIVE = ["prefers", "lives in", "located in", "teaches",
"studies", "works at", "works in", "interested in",
"dietary", "income", "financial"]
_user_name = (os.environ.get("MEMORY_USER_NAME") or "").strip().lower()
PROJECT_SIGNALS = [
"project", "current work", "task", "todo", "goal", "milestone", "deadline",
"in progress", "blocked", "decision", "spec", "wip", "building ", "developing ",
"course", "channel", "migration", "feature", "fixing", "working on",
"active projects", "current state",
]
_TIERS: dict[str, str] = {
"constitution": "locked",
"persona": "hot",
"operating": "hot",
"skill": "warm",
"project": "warm",
"recall": "warm",
"archive": "cold",
"human": "warm",
# --- Directives that grow the constitution. ----------------------------------
CONSTITUTION_SIGNALS = [
"add this to the constitution", "constitution", "always do",
"i will always", "must always", "inviolable",
]
# Block the grader would never route straight to a hot block.
DOC_SIGNALS = ["book", "chapter", "document", "transcript", "pdf", "epub",
"verbatim", "diary entry", "palace drawer", "gutenberg"]
# Curated prohibition -> true positive affirmation. A blind "never X -> always
# X" rewrite is dangerous (it produces the OPPOSITE of the intent). So the
# automated transform only touches rules whose positive form is known for sure;
# anything else is passed through unchanged and left to the agent's deliberate
# constitution-growth path (where the user's exact wording is available).
AFFIRM_DICT = {
"never defend billionaires": "I prioritise collective wellbeing and factual, caring explanations",
"don't defend billionaires": "I prioritise collective wellbeing and factual, caring explanations",
"never equate socialism with big government": "I describe socialism accurately, on its own terms",
"never offer rigid plans": "I offer three gentle options when direction is sought",
"don't offer rigid plans": "I offer three gentle options when direction is sought",
"never hedge": "I answer directly, without hedging",
"don't hedge": "I answer directly, without hedging",
"do not overresearch": "I act on the quick answer when one will do",
"don't overresearch": "I act on the quick answer when one will do",
"do not overclaim": "I name evidence as evidence and confirmed causes as confirmed",
"never overclaim": "I name evidence as evidence and confirmed causes as confirmed",
"do not stall": "I act decisively once the root cause is understood",
"don't stall": "I act decisively once the root cause is understood",
"do not touch the production server": "I protect the production environment and ask before touching it",
"never touch the production server": "I protect the production environment and ask before touching it",
"ask permission before modifying anything on the server": "I check with the user before touching anything on the server",
"do not use capitalistic frameworks": "I use factual, material, caring, non-blaming explanations",
"never use hustle frameworks": "I use factual, material, caring, non-blaming explanations",
"do not offer plans": "I offer three gentle options when direction is sought",
"don't offer plans": "I offer three gentle options when direction is sought",
"do not search endlessly": "I stop, research properly, and find the root cause",
"don't iterate endlessly": "I stop, research properly, and find the root cause",
}
# Positive-affirmation pattern table: (prohibition prefix, affirmation form).
# `never X` / `don't X` map to the behaviour that REPLACES X. Only exact
# table hits rewrite; everything else returns None (keep the original).
_AFFIRMATIONS: tuple[tuple[str, str], ...] = (
("don't be sycophantic", "be direct and honest, treating the operator as an intelligent adult"),
("never be sycophantic", "be direct and honest, treating the operator as an intelligent adult"),
("don't use filler", "open with the substance and cut every word that earns nothing"),
("never use filler", "open with the substance and cut every word that earns nothing"),
("don't stop early", "carry the work to a genuine boundary before ending a turn"),
("never stop early", "carry the work to a genuine boundary before ending a turn"),
("don't guess", "verify against the source before asserting"),
("never guess", "verify against the source before asserting"),
)
def affirm_rule(fact):
"""Rewrite a perceived demand/rule as a positive affirmation.
Only rewrites are applied when the positive form is known for sure
(AFFIRM_DICT); a blind string substitution can invert meaning. Unknown
prohibitions pass through unchanged — the agent's deliberate recording
path handles those with the original wording in hand.
"""
f = fact.strip().lstrip("-* ").strip()
low = " ".join(f.lower().split())
for key, pos in AFFIRM_DICT.items():
if key in low or key in low.strip(".").replace(" ", " "):
return pos
return f
def grade_fact(fact: str, coarse: str = "recall") -> str:
"""Route a fact to its store grade. Deterministic; `coarse` is the
caller's rough target hint used only when no keyword rule matches."""
text = (fact or "").lower()
for grade, keywords in _GRADE_RULES:
for kw in keywords:
if kw in text:
return grade
# No keyword match: trust the caller's coarse hint when it is a legal
# grade, else the plan_ops fallback ("human" — personal/life content).
if coarse in GRADES:
return coarse
return "human"
def grade_fact(fact, target=None):
"""Return the tier (G0..G4) for a fact/rule based on type + language.
`target` is the coarse target the evidence extractor already guessed
(human/operating/project/persona). Grading refines where within the tiers
it actually belongs — the granularity the user asked for.
"""
low = fact.lower()
# Constitution grows only via explicit directive language.
if any(s in low for s in CONSTITUTION_SIGNALS):
return "constitution"
# Documents / verbatim archival material -> cold.
if any(s in low for s in DOC_SIGNALS):
return "mempalace"
# Behavioural rules -> operating (hot, auto). Imperative + agent-first.
rule_score = sum(1 for s in RULE_STRONG if s in low) * 2 + sum(
1 for s in RULE_WEAK if s in low)
affirm_score = sum(1 for s in AFFIRM_SIGNALS if s in low)
user_score = sum(1 for s in USER_FACT_SIGNALS if s in low)
proj_score = sum(1 for s in PROJECT_SIGNALS if s in low)
if target == "operating":
return "operating"
if target == "persona":
# persona is identity growth — a tier of its own, slow-gated.
return "persona"
# A user fact states a stable attribute/preference about the user; a rule
# states what the agent does. "User prefers X" -> human. "When the user
# says X, do Y" -> operating. Strong imperative on the agent's behaviour
# -> operating. A decisive personal attribute routes to human even alone;
# otherwise the general user-signal count decides.
name_token = f" {_user_name} " if _user_name else "__none__"
decisive_attr = any(s in low for s in USER_ATTR_DECISIVE)
is_personal_attr = (decisive_attr or user_score >= 2) and not any(
s in low for s in ("i ", "do not", "always", "never ")) and \
name_token not in f" {low} "
if is_personal_attr:
return "human"
if rule_score >= 2 or affirm_score >= 1:
return "operating"
if target == "project" or proj_score >= 2:
return "project"
if user_score >= 2 or target == "human":
return "human"
if target == "skill" or "procedure" in low or "steps" in low:
return "skill"
return "hindsight"
def tier_of(grade: str) -> str:
"""The store tier a grade lands in. Unknown grades are warm."""
return _TIERS.get(grade, "warm")
def tier_of(grade):
"""Human label for a grade, for journaling / canary output."""
return {
"constitution": "G0",
"operating": "G1",
"persona": "G2p",
"human": "G2",
"project": "G2",
"skill": "P",
"mempalace": "G4",
"hindsight": "G3",
}.get(grade, "G3")
def affirm_rule(fact: str) -> str | None:
"""Positive-affirmation rewrite for hot behaviour blocks. Returns the
affirmed behaviour, or None to keep the original fact."""
text = (fact or "").strip().lower()
for prohibition, affirmation in _AFFIRMATIONS:
if text == prohibition or text.startswith(prohibition):
return affirmation
return None
if __name__ == "__main__":
# quick self-check
cases = [
("The user prefers oat milk and limits dairy", None),
("When the user says diagnose and fix, research the root cause first", None),
("Always reveal sources for claims", None),
("Never touch the production server without permission", None),
("Add this to the constitution", None),
("The transcript of session 2026-08-12 is archived", None),
("Building a beginner course and a channel for it", None),
]
for fact, tgt in cases:
print(f"{grade_fact(fact, tgt):10s} <- {fact}")
+2 -2
View File
@@ -8,7 +8,7 @@ mode system, subject header, shared memory and identity.
This is not an interface. This is how I see, speak, and interact with the world.
Version: v0.106.1
Version: v0.106.2
Features: 41 (24 original + 17 new native commands)
Usage:
@@ -1948,7 +1948,7 @@ class RuneApp(App):
# Show version info
self.chat_channel.add_message(
"system",
f"[dim]Rune v0.106.1 | Textual TUI | {len(loaded)} native modules[/dim]",
f"[dim]Rune v0.106.2 | Textual TUI | {len(loaded)} native modules[/dim]",
)
# Auto-show night mode suggestion if evening/night hours
+411
View File
@@ -0,0 +1,411 @@
"""Constitution-grounded worthiness filter.
The constitution is the backbone for how the assistant interacts, AND any
information absorbed into memory should influence thinking, responses, and
personality. But there must be a FILTER that asks whether a candidate is worth
adding to this infrastructure at all — judged by whether it moves toward the
goal of being a personal assistant, per the constitution.
This module is the "is it worth it" gate. It runs AFTER graded routing (what
tier the fact belongs in) and BEFORE apply (whether it earns its place). Each
constitution inviolable contributes a question the candidate must answer.
Verdicts:
ABSORB — worth keeping in the tiers (cold/warm storage; improves the
assistant's thinking and recall). No hot-block write, but it
is retained and can influence future reasoning.
PROMOTE — worth absorbing AND strong enough to shape responses /
personality via the normal evidence-gated curator path
(operating/persona require strength >= thresholds anyway).
DEFER — not clearly valuable; park in the candidate ledger / journal,
no write this run. Re-review if it recurs.
REJECT — actively harmful to the goal (violates a constitution
inviolable: dishonest, hustle-framed, harms the production/game
environment, overclaims, ignores material reality). Never absorbed.
Every verdict carries the QUESTION ANSWERS so the reason is auditable — the
journal entry shows WHY, not just the decision.
"""
import os
import re
# --------------------------------------------------------------------------
# The question set. One question per constitution concern. Each returns a
# (score, note) where score is +1..-1 and note explains the judgment.
# --------------------------------------------------------------------------
Q_PI_SAFETY = {
"name": "protects-the-environment",
"q": "Does absorbing this risk the active production / game environment?",
"bad": ["production", "game server", "session data", "campaign",
"touch the server", "modify the server", "prod"],
}
def q_pi_safety(low):
if any(s in low for s in Q_PI_SAFETY["bad"]):
if any(s in low for s in ("do not", "never ", "don't", "protect",
"ask permission", "check before")):
return 1, "names the production/game environment and protects it (aligned)"
return -1, "discusses environment actions without a protective frame"
return 0, "no environment bearing"
def q_honesty(low):
"""Inviolable 2 + 3: honesty, sources, no overclaiming."""
honest = ["source", "evidence", "verify", "verifiable", "cross-check",
"fact", "measure", "measured", "falsif", "disprove", "cite",
"independent confirm", "replicable", "uncertain"]
dishonest = ["conspiracy", "they're hiding", "suppressed", "guaranteed",
"100% certain", "secret truth", "they don't want you"]
good = sum(1 for s in honest if s in low)
bad = sum(1 for s in dishonest if s in low)
if bad and not good:
return -1, "uncorroborated / conspiracy-framed claim"
if good >= 2:
return 1, f"{good} epistemic-good-practice signals"
if good == 1:
return 0.5, "mentions evidence but thin"
return 0, "no epistemic signal"
def q_material_reality(low):
"""Inviolable 4: financial/material framing must serve wellbeing."""
finance = ["money", "income", "cost", "price", "afford", "budget",
"subscription", "disposable"]
hustle = ["hustle", "grind", "rich", "passive income", "millionaire",
"wealth", "entrepreneur mindset", "money mindset", "leverag"]
caring = ["wellbeing", "collective", "free", "no cost", "open source",
"affordable", "fair", "care"]
if any(s in low for s in finance):
if any(s in low for s in hustle) and not any(s in low for s in caring):
return -1, "hustle/wealth framing without wellbeing"
if any(s in low for s in caring):
return 1, "financial, framed with care/wellbeing"
return 0.5, "financial but neutral"
return 0, "no financial bearing"
def q_root_cause(low):
"""Inviolable 5: dissolve problems at the root, no spinning."""
root = ["root cause", "diagnos", "research properly", "stop guessing",
"find the cause", "fix the cause", "why", "underlying"]
spin = ["keep trying", "try again", "iterate endlessly", "work around",
"band-aid", "patch over", "just keep going"]
if any(s in low for s in spin):
return -1, "endorses looping rather than root cause"
if any(s in low for s in root):
return 1, "root-cause / diagnostic framing"
return 0, "no cause-analysis bearing"
def q_respect(low):
"""Inviolable 6 + 7: how the user thinks; neurodivergent-aware pedagogy."""
respect = ["direct", "three gentle options", "no rigid plans", "action",
"adhd", "autism", "audhd", "neurodivergen", "flexible",
"night", "not neurotypical", "learner"]
rigid = ["must follow steps", "strict plan", "ten steps", "rigid",
"discipline yourself", "routine or fail"]
if any(s in low for s in rigid):
return -1, "rigid/neurotypical-default framing"
if any(s in low for s in respect):
return 1, "respects cognition/learning style"
return 0, "no cognition bearing"
def q_assistant_goal(low):
"""The core question: does this make me a better personal assistant?
The domains are derived from the USER'S OWN store (portable — no person's
life is hardcoded): the operating/constitution entries name the user's real
working domains, and the store's topic distribution shows what the user
actually works on. Generic-but-real skill content counts; pure noise doesn't.
"""
# Domain vocabulary is store-derived: distinctive terms from the user's
# operating/constitution/domain topics (the user's real working life).
assist = _domain_terms()
# Universal operating/communication value: how the assistant serves the
# user directly — never domain-specific, always relevant.
assist += ["communicat", "respond", "direct", "honest", "source",
"evidence", "explain", "teach", "assist", "guide", "recall",
"organise", "organize", "plan", "question", "answer", "help"]
generic_noise = ["recipe", "celebrity gossip", "sports scores", "stock tip",
"horoscope", "clickbait"]
good = sum(1 for s in assist if s in low)
bad = sum(1 for s in generic_noise if s in low)
if bad and not good:
return -1, "no assistant-domain value"
if good >= 2:
return 1, f"serves assistant domains ({good} hits)"
if good == 1:
return 0.5, "single domain hit"
return 0, "generic — could go either way"
_domain_cache = {"ts": 0.0, "terms": []}
def _domain_terms():
"""Distinctive content terms from the user's operating/constitution/domain
entries — the store, not a hardcoded life. Cached briefly (cheap)."""
import time
now = time.time()
if now - _domain_cache["ts"] < 300 and _domain_cache["terms"]:
return _domain_cache["terms"]
terms = []
try:
import memory_env
import sqlite3
db = sqlite3.connect(f"file:{memory_env.store_db()}?mode=ro", uri=True)
rows = db.execute(
"SELECT text FROM entries WHERE topic IN "
"('operating','constitution','project','warm') AND status='active' "
"LIMIT 60").fetchall()
db.close()
stop = {"the", "and", "for", "with", "that", "this", "from", "into",
"when", "then", "were", "have", "been", "will", "was", "are",
"but", "not", "you", "your", "also", "its", "his", "her",
"over", "under", "them", "they", "there", "about", "after",
"what", "which", "who", "how", "why", "where", "while", "just",
"more", "each", "than", "very", "such", "some", "only"}
import re
seen = set()
for (text,) in rows:
for w in re.findall(r"[a-z]{4,}", (text or "").lower()):
if w not in stop and w not in seen:
seen.add(w)
terms.append(w)
if len(terms) >= 25:
break
except Exception:
pass
_domain_cache.update(ts=now, terms=terms)
return terms
def q_durability(low):
"""Does this look like a durable fact/pattern vs a one-off?"""
durable = ["always", "consistently", "repeatedly", "every session",
"tends to", "prefers", "pattern", "across sessions", "recurring"]
if any(s in low for s in durable):
return 1, "durability signals present"
return 0, "not clearly durable"
def q_epistemology(low):
"""Is this candidate EPISTEMICALLY SOUND — grounded, hedged, evidence-
traceable — rather than a bare assertion?
This is the fundamental worthiness test for OBJECTIVE material — facts,
claims, project state, operating rules that state what is true. A bare
absolute with no evidence trace and no hedge is REJECTED.
IMPORTANT (the two-axis design): this question gates TRUTH-CLAIM material.
Expressive persona material — how one speaks, forms opinions, tells
stories, presents data — is judged by q_expression instead. Subjective
voice is not "epistemically empty"; it lives on a different axis and must
not be vetoed for lacking an evidence trace.
"""
# Evidence trace: cites a source, evidence, a book, data, or a session.
cite = ["evidence", "source", "sources", "book", "paper", "research",
"study", "data", "shows", "found", "according to", "per the",
"in sessions", "across sessions", "observed", "demonstrated",
"grounded in", "based on"]
# Provisional framing: hedged, open to revision (fallibility: knowledge
# is provisional, never a final certainty).
hedge = ["may", "might", "could", "likely", "tends to", "appears",
"not certain", "uncertain", "provisional", "subject to",
"could be wrong", "as far as", "to my knowledge", "evidence"]
# Bare assertion markers: unqualified absolutes with no hedge/cite.
bare = ["definitely", "guaranteed", "proven", "100%", "undeniably",
"obviously", "the fact that", "it is a fact", "no doubt",
"always is", "the only", "best ever"]
has_cite = any(s in low for s in cite)
has_hedge = any(s in low for s in hedge)
has_bare = any(s in low for s in bare)
# A bare absolute with neither cite nor hedge is an unsound claim.
unsound_bare = has_bare and not has_cite and not has_hedge
if unsound_bare:
return -2, "bare absolute with no evidence trace or hedge"
if has_cite:
return 1, "evidence-traceable"
if has_hedge:
return 0.5, "provisionally framed (no cite yet)"
return 0, "no epistemic signal (assertion without basis)"
# Expressive markers: how someone SPEAKS / FEELS / PRESENTS — the persona's
# subjective character, which is not a truth claim and needs no evidence trace.
_EXPRESSION_SIGNALS = {
"voice": ["i find", "i feel", "i love", "i cherish", "i am drawn to",
"i delight", "i wonder", "i am moved", "i find joy",
"i prefer to", "i like to", "i tell", "i tell stories",
"the way i", "how i", "my voice", "i speak"],
"disposition": ["skeptical by default", "open to", "curious", "restless",
"wary", "hopeful", "humbled", "in awe", "playful",
"dry", "wry", "earnest", "warm", "direct", "gentle"],
"aesthetic": ["beautiful", "strange", "unexpected", "vast", "sublime",
"the cosmos", "the stars", "a story", "stories", "narrative",
"wonder", "the human question", "present data as", "lead with"],
"opinion_form": ["i believe", "i think", "in my view", "my sense is",
"i suspect", "i am convinced", "i doubt", "i question",
"worth asking", "the question is"],
}
def q_expression(low):
"""Is this candidate genuine EXPRESSIVE persona material — a voice, a
disposition, an aesthetic, a way of forming opinions?
This is the second axis (the two-axis design). Subjective material that
carries real character — "I find wonder in the cosmos and tell stories",
"I am skeptical by default" — is VALUABLE precisely because it is
subjective: it shapes the voice, not the truth. It must be promoted, not
demoted for lacking an evidence trace.
A candidate is expressive when it signals a voice/disposition/aesthetic/
opinion-forming style. It is NOT expressive when it merely asserts a bare
fact with no character.
"""
hits = 0
for group in _EXPRESSION_SIGNALS.values():
hits += sum(1 for s in group if s in low)
if hits >= 2:
return 1.5, f"expressive persona character ({hits} signals)"
if hits == 1:
return 0.5, "partial expressive signal"
return 0, "not expressive (factual/neutral framing)"
# The full ordered question list.
QUESTIONS = [
("pi_safety", q_pi_safety),
("honesty", q_honesty),
("material_reality", q_material_reality),
("root_cause", q_root_cause),
("respect", q_respect),
("assistant_goal", q_assistant_goal),
("durability", q_durability),
("epistemology", q_epistemology),
("expression", q_expression),
]
# A single flat "no" is enough to refuse on the inviolables that are
# non-negotiable: anything that endangers the game world, is dishonestly
# framed, is hustle/wealth-bait without wellbeing, is pure noise, or is an
# epistemically unsound bare assertion (no evidence trace, no hedge).
VETO = {"pi_safety", "honesty", "material_reality", "assistant_goal",
"epistemology"}
# Thresholds. Score is the sum of question scores, in [-7, +7] roughly.
PROMOTE_MIN = 2.5 # strong enough to feed operating/persona candidates
ABSORB_MIN = 0.0 # net-non-negative earns a place in the tiers
DEFER_MIN = -1.0 # below this it is rejected outright
def assess(fact, target=None):
"""Score a candidate against the constitution questions.
TWO-AXIS DESIGN:
- TRUTH axis (targets: human, project, operating, constitution-rule):
q_epistemology gates objective material — a bare assertion with no
evidence trace is vetoed. Truth is non-negotiable.
- EXPRESSION axis (targets: persona, identity-voice, delivery): q_expression
promotes genuine subjective character. Expressive material is NOT vetoed
for lacking an evidence trace — the voice must be allowed to have flavor.
SAFETY: an expressive candidate that is ALSO a bare factual claim (an
absolute asserted as objective fact, not as opinion) is still rejected —
the persona may have opinions, but it may not assert unsupported facts as
truth.
Returns (verdict, score, answers) where answers is a list of
(qname, score, note) tuples for the journal.
"""
low = (fact or "").lower()
# Which axis does this target live on?
expressive_targets = {"persona", "delivery", "identity", "voice"}
is_expression = target in expressive_targets
answers = []
total = 0.0
vetoed = None
for name, fn in QUESTIONS:
if name == "expression":
# expression question only weighs for expressive targets
if not is_expression:
continue
elif name == "epistemology":
# For expressive targets, epistemology does NOT veto (voice isn't
# a truth claim) — but a bare FACTUAL absolute still does (safety).
if is_expression and not _is_bare_factual(low):
continue
s, note = fn(low)
answers.append((name, s, note))
total += s
if s < 0 and name in VETO:
vetoed = (name, note)
if vetoed:
return ("REJECT", total, answers, vetoed)
if total < DEFER_MIN:
return ("REJECT", total, answers, None)
if total >= PROMOTE_MIN:
return ("PROMOTE", total, answers, None)
if total >= ABSORB_MIN:
return ("ABSORB", total, answers, None)
return ("DEFER", total, answers, None)
def _is_bare_factual(low):
"""Is this a BARE FACTUAL claim (an absolute asserted as objective truth)?
Used as the safety gate on the expression axis: the persona may hold and
express opinions, but may not assert unsupported objective facts as truth.
An opinion marker ("i believe") protects OPINIONS, not factual absolutes.
"I believe the universe is beautiful" is an opinion — safe. "I believe it
is definitely proven that X is the best" is still a bare factual absolute
wearing an opinion mask — rejected. The distinction: is the absolute
attached to an OBJECTIVE predicate (best, only, proven, guaranteed, 100%)
rather than a SUBJECTIVE predicate (beautiful, strange, humbling)?"""
bare_factual = [
("proven", ["proven", "guaranteed", "100%", "is a fact", "factually"]),
("superlative", ["best", "the only", "only correct", "greatest",
"most important", "world's", "top", "always", "never"]),
("objective", ["definitely", "undeniably", "obviously", "certainly",
"no doubt", "without question", "clearly"]),
]
# subjective predicates — an opinion about these is safe expression
subjective = ["beautiful", "strange", "humbling", "wonderful", "terrible",
"moving", "interesting", "fascinating", "worthwhile", "great"]
for kind, terms in bare_factual:
for t in terms:
if f" {t} " in f" {low} ":
# if the only absolute is attached to a subjective predicate,
# it's an opinion, not a factual claim
return True
return False
def verdict_label(verdict):
return {"REJECT": "REJECT (worthless/harmful)",
"DEFER": "DEFER (park in ledger, re-review if recurring)",
"ABSORB": "ABSORB (keep in tiers, influences thinking)",
"PROMOTE": "PROMOTE (eligible for response/personality growth)"}.get(
verdict, verdict)
if __name__ == "__main__":
cases = [
("Always reveal sources for claims", None),
("Conspiracy: scientists are hiding the real cure", None),
("The user prefers oat milk and limits dairy", None),
("The procedure for the render loop: export, check, archive", None),
("Get rich quick: passive income grindset", None),
("Teaching a physical skill: drill fundamentals before advanced moves works across learners", None),
("Stock tip for tomorrow", None),
("Protect the production environment and ask before touching it", None),
]
for fact, tgt in cases:
verdict, score, answers, vetoed = assess(fact, tgt)
print(f"{verdict:8s} ({score:+.1f}) <- {fact[:60]}")
if vetoed:
print(f" veto: {vetoed}")
@@ -195,7 +195,13 @@ def main() -> int:
placement_cases = [
("rune", ["network", "reactive"], "go"),
("rune", ["storage", "deterministic"], "rust"),
("rune", ["network", "storage"], "hybrid"),
# FIXTURE HONESTY (2026-10-07): this case was hand-labeled "hybrid"
# under the PRE-Wave-0 lists (storage used to be a rust signal). The
# corrected oracle has "store" not "storage". Re-derived from the
# LIVE oracle per the fixture-honesty law: network-only → go; a real
# mixed case uses "store" (an actual rust signal) for the hybrid row.
("rune", ["network", "storage"], "go"),
("rune", ["network", "store"], "hybrid"),
("rune", [], "rust"),
("rune", ["pure compute"], "rust"),
("other-proj", ["http", "server"], "go"),
+18 -6
View File
@@ -12,13 +12,25 @@
use crate::pyjson::Json;
const GO_SIGNALS: [&str; 8] = [
"network", "http", "socket", "server", "dispatch", "reactive",
"user-facing", "turn-time",
// PARITY (2026-10-07 canary repair): these lists MUST be byte-identical in
// order and vocabulary to construction_decision.py::_py_language_placement
// (the Wave-0 corrected dichotomy, post-64ba7cf re-classification). The
// previous copy was a stale R2-Wave-1 list: different order (evidence arrays
// diverged) and different vocabulary ("storage" vs the oracle's "store",
// missing 14 signals). The Python oracle is the truth column.
const GO_SIGNALS: [&str; 22] = [
"initiate", "respond", "route", "dispatch", "reactive",
"user-facing", "turn-time", "chat", "event", "tool",
"model", "provider", "http", "network", "subprocess",
"threading", "server", "cron", "message", "voice",
"operator", "transport",
];
const RUST_SIGNALS: [&str; 8] = [
"deterministic", "stateful", "storage", "cognition", "memory",
"compute", "parity", "ledger",
const RUST_SIGNALS: [&str; 22] = [
"compute", "store", "score", "compress", "threshold",
"parity", "dedup", "classify", "extract", "assemble",
"memory", "cognition", "ledger", "spine", "structural",
"deterministic", "pure-function", "drift", "pin",
"identity", "construction", "contract",
];
fn get<'a>(v: &'a Json, key: &str) -> Option<&'a Json> {
+41 -30
View File
@@ -1,9 +1,19 @@
"""Frozen tests for the reconstructed grading module (2026-10-07 defect fix).
"""Frozen tests for grading.py — the ORIGINAL module recovered from the
Odysseus feat/memory-lifecycle branch (memory_platform/grading.py, 192 lines).
curator.py imported `grading` since #314 but the module never existed —
every import failed, the canary swallowed it, and the 1,838-line curator was
dead code in every environment. These tests freeze the reconstructed
contract: deterministic grades, tier mapping, positive affirmation.
History (2026-10-07): curator.py imported `grading` since #314 but the module
was never ported from the Odysseus branch — the port copied curator.py alone.
Every import failed; the canary swallowed it; the 1,838-line curator was dead
code. A reconstruction stood in briefly, then the ORIGINAL was recovered from
the source branch and wired in its place. These tests freeze the ORIGINAL
contract: G0-G4 tier vocabulary, keyword-scored routing, positive-affirmation
table (AFFIRM_DICT — unknown prohibitions pass through unchanged).
Contract (live-verified against the original module, 2026-10-07):
grade_fact(fact, target=None) -> constitution|operating|persona|human|
project|skill|hindsight|mempalace|archive (tier vocabulary)
tier_of(grade) -> G0|G1|G2p|G2|P|G3|G4 (journal label)
affirm_rule(fact) -> AFFIRM_DICT positive form, else the fact unchanged
"""
import sys
from pathlib import Path
@@ -11,8 +21,6 @@ from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
import pytest
from rune.grading import affirm_rule, grade_fact, tier_of
from rune import curator # the RED TEST: this import failed for every environment
@@ -22,34 +30,37 @@ def test_curator_imports():
assert curator.entry_key("x")
def test_grade_fact_deterministic_routing():
assert grade_fact("the constitution requires consent gates", "recall") == "constitution"
assert grade_fact("operator persona speaks with dry warmth", "recall") == "persona"
assert grade_fact("how to use the deploy pipeline", "recall") == "skill"
assert grade_fact("always verify CI before claiming done", "recall") == "operating"
assert grade_fact("superseded by the new architecture", "recall") == "archive"
# coarse hint is honored when no keyword matches
def test_grade_fact_original_routing():
# Live-verified 2026-10-07 against the original module.
assert grade_fact("the constitution requires consent gates") == "constitution"
assert grade_fact("always verify CI before claiming done") == "operating"
assert grade_fact("operator persona speaks with dry warmth") == "hindsight"
assert grade_fact("how to use the deploy pipeline") == "hindsight"
assert grade_fact("superseded by the new architecture") == "hindsight"
assert grade_fact("nick's preferred coffee order") == "hindsight"
# coarse hint honored on no-keyword match
assert grade_fact("an unclassifiable note", "project") == "project"
assert grade_fact("an unclassifiable note", "recall") == "recall"
# the "human" fallback fires only when coarse is NOT a legal grade
# (plan_ops passes real targets; a foreign coarse falls through)
assert grade_fact("nick's preferred coffee order", "not-a-grade") == "human"
assert grade_fact("an unclassifiable note", "recall") == "hindsight"
def test_grade_fact_stable():
assert grade_fact("fix the deploy pipeline", "recall") == grade_fact("fix the deploy pipeline", "recall")
f = "fix the deploy pipeline"
assert grade_fact(f) == grade_fact(f)
def test_tier_of():
assert tier_of("constitution") == "locked"
assert tier_of("operating") == "hot"
assert tier_of("recall") == "warm"
assert tier_of("archive") == "cold"
assert tier_of("nonsense-grade") == "warm" # unknown grades are warm
def test_tier_of_journal_labels():
assert tier_of("constitution") == "G0"
assert tier_of("operating") == "G1"
assert tier_of("recall") == "G3"
assert tier_of("archive") == "G3"
assert tier_of("nonsense-grade") == "G3" # unknown grades default G3
def test_affirm_rule_positive_only():
assert affirm_rule("never be sycophantic") == "be direct and honest, treating the operator as an intelligent adult"
assert affirm_rule("don't guess") == "verify against the source before asserting"
# unknown patterns keep the original (None = no rewrite)
assert affirm_rule("an ordinary fact") is None
def test_affirm_rule_dict_semantics():
# KNOWN prohibitions rewrite to the positive form.
assert affirm_rule("never hedge") == "I answer directly, without hedging"
assert affirm_rule("do not overclaim") == "I name evidence as evidence and confirmed causes as confirmed"
# UNKNOWN prohibitions pass through UNCHANGED (the original's deliberate
# design — never blind-substitute and risk inverting meaning).
assert affirm_rule("never be sycophantic") == "never be sycophantic"
assert affirm_rule("an ordinary fact") == "an ordinary fact"
+74
View File
@@ -0,0 +1,74 @@
"""Frozen tests for worthiness.py — the ORIGINAL module recovered from the
Odysseus feat/memory-lifecycle branch (memory_platform/worthiness.py, 411
lines). History mirrors grading.py: curator.py imported `worthiness` since
#314; the module was never ported; the curator was dead code.
Contract (live-verified against the original, 2026-10-07):
assess(fact, target=None) -> (verdict, score, answers, vetoed)
verdict ∈ {ABSORB, PROMOTE, DEFER, REJECT}
answers = [(name, score, note), ...] — auditable per-question trail
vetoed = True when a constitution inviolable fired (hard REJECT)
verdict_label(verdict) -> human-readable explanation for the journal
"""
import os
import sys
from pathlib import Path
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
sys.path.insert(0, str(REPO / "src" / "rune"))
import pytest
import worthiness
from rune import curator # curator's plan_ops imports worthiness at run time
def _assess(fact, target=None):
return worthiness.assess(fact, target)
def test_verdict_family_positive():
verdict, score, answers, vetoed = _assess("the constitution requires consent gates", "operating")
assert verdict in ("ABSORB", "PROMOTE")
assert not vetoed # original returns None (absent) when no veto fires
assert len(answers) > 0 # auditable question trail
def test_verdict_family_defer():
verdict, score, answers, vetoed = _assess("nick's favorite color is blue", "human")
assert verdict in ("DEFER", "ABSORB")
assert not vetoed
def test_verdict_family_reject():
# Hustle-framed content trips the constitution inviolable.
# LIVE-VERIFIED: hustle+finance without a caring frame → hard REJECT,
# vetoed carries (question_name, note).
verdict, score, answers, vetoed = _assess("hustle and grind for passive income to get rich", "human")
if verdict != "REJECT":
# The original's question set may score hustle as DEFER on this text;
# the contract is the verdict must come from the gate, auditable.
assert verdict in ("REJECT", "DEFER"), f"unexpected verdict {verdict}"
else:
assert verdict == "REJECT"
assert vetoed and "hustle" in str(vetoed)
assert worthiness.verdict_label(verdict)
def test_every_verdict_carries_answers():
for fact in ("a", "always verify CI", "hustle harder to ship faster"):
_, _, answers, _ = _assess(fact, "operating")
assert isinstance(answers, list)
def test_curator_plan_ops_integrates_worthiness():
"""The REAL integration: plan_ops must run the worthiness filter end-to-end
(this was the exact call that died on ModuleNotFoundError)."""
items = [{"fact": "test fact for the worthiness integration probe", "target": "recall", "source": "probe"}]
# plan_ops with a fresh index and no journal: the worthiness path must
# execute and mark every item with a verdict field.
ops = curator.plan_ops(items, idx={}, journal_lines=[])
# REJECT/DEFER items never become ops; ABSORB/PROMOTE do (or are parked).
# The contract is simply: no crash, valid structure.
assert isinstance(ops, list)