Skip to content

feat(qa): feature-engagement feedback loop — manifest + coverage scorer + forcing gate (WS0, all-WARN) - #1018

Merged
100yenadmin merged 2 commits into
mainfrom
feat/ws0-engagement-feedback-loop
Jun 18, 2026
Merged

feat(qa): feature-engagement feedback loop — manifest + coverage scorer + forcing gate (WS0, all-WARN)#1018
100yenadmin merged 2 commits into
mainfrom
feat/ws0-engagement-feedback-loop

Conversation

@100yenadmin

@100yenadmin 100yenadmin commented Jun 18, 2026

Copy link
Copy Markdown
Member

WS0 — the feature-engagement feedback loop (keystone of the sprint)

The blind spot this closes

Today an entire authored story subsystem (companion approval, camp downtime, faction questlines, the companion agenda, recorded decisions) can be 100% inert across a 5-persona sweep and the RRI still scores 10/10 — because no gate is engagement-coverage. A frozen-relationship run that narrated the companion but never moved a gauge passed; a run that seeded factions and never joined one passed; a run that never camped passed. This PR makes those "dead system" shapes a visible, queryable, gated signal.

All-WARN-first design (why this can't break the green pipeline)

Every system ships severity='warn', so the axis is strictly additive:

  • assert_behavioral.py emits engagement_<id> checks with fatal=Falsezero new fatals; every currently-green run stays green.
  • The new release_readiness.py story_engagement gate FAILS only on a FATAL inert system → all-WARN ⇒ it always passes when evidence is present (inert systems are reported, not gated).
  • When no persona score.json carries engagement_coverage (a legacy corpus), the gate is an evidence-gap SKIP — excluded from passed/total, so RRI math is byte-identical (mirrors the latency-gate skip). gates_total stays 11 / RRI 10.0 on the existing fixtures.

It would have caught the approval / camp deaths

The two proven failure shapes — a companion stuck at attitude_value 0 all run, and a multi-day run where nobody ever last_long_rest_day'd — are now companion_approval / camp_downtime inert-in-scope (precondition true, detector false), surfaced per-run in the behavioral gate and rolled up in the ENGAGEMENT RRI section with a fix hint.

The conditional N/A logic (keeps the loop from ever false-RED-ing)

A system is N/A (no occasion to engage) — never inert — when its precondition is false or unknown:

  • session_beats lives in the transcript, not the snapshot, so the signature takes it explicitly and every beats-keyed precondition defaults to N/A when it is None (the inject callsite passes None → safe under-detect, never a false-RED).
  • Solo party ⇒ companion systems N/A; factionless / non-joinable world ⇒ faction systems N/A; short run (below the per-system beats threshold) ⇒ N/A.
  • consequences_fired is owed only when a consequence's trigger_day is strictly < the final day; trigger_day == final_day is WARN-not-owed (may fire on the unseen last beat) ⇒ N/A; future-dated ⇒ N/A.
  • Under WORLDOS_GATE_COMBAT_SPRINT, all FATAL systems are skipped (mirrors assert_behavioral.py).

Deterministic, snapshot-derived — engine state only, never fiction

Every predicate reads only engine-mutated snapshot fields (attitude_value, last_long_rest_day, faction.joined/standing, narrative_arc.act, consequence.fired/trigger_day, the arc/agenda fired flags, campaign.decisions/quests/factions/*_arcs) or DM tool-counts — never prose (engine invariant #3). It reuses story_readout.structural_coverage_from_state / felt_shape_from_state so the shared buckets never drift; old snapshots round-trip (null-guards throughout, incl. absent/None narrative_arc).

The forcing meta-test

servers/engine/tests/test_feature_engagement_manifest.py mirrors test_tool_schema_budget.py: it asserts {s.id for s in SYSTEMS} == REVIEWED_SYSTEM_IDS (and severities/ids), so changing the tracked-system set is a deliberate, visible diff — the manifest can never silently drift out of coverage (the exact failure WS0 exists to prevent).

Deferred FATAL graduation

Graduation to FATAL is a FUTURE, post-sweep PR — after one real 5-persona sweep calibrates the inert/owed classification (the same discipline as flat_arc / caster_has_spellbook). The gate logic already enforces FATAL-inert ⇒ fail (forward-compat test included), so graduation is a one-line manifest change, not a gate rewrite.

2 BLOCKED systems (stay WARN regardless)

faction_arc + companion_quest_arc: a snapshot-only precondition can't tell seeded-but-locked from never-seeded beyond the joined latch — a known open spike. They are pinned WARN by BLOCKED_SYSTEM_IDS and the manifest test, and must never graduate until the spike is resolved.

The 10 tracked systems

companion_approval · camp_downtime · quests_objectives · acts_advance · consequences_fired · factions_membership · faction_arc* · companion_quest_arc* · companion_agenda · decisions_recorded (*BLOCKED)

Files

  • new qa/feature_engagement.py — pure module (SystemSpec manifest + engagement_coverage).
  • new servers/engine/tests/test_feature_engagement_manifest.py — forcing meta-test.
  • new qa/test_feature_engagement.py — table-driven behavior tests.
  • qa/assert_behavioral.py — additive engagement_<id> WARNs after structural_completeness.
  • qa/inject_structural_coverage.py — merge engagement_coverage into score.json (beats=None).
  • qa/scores_db.pyengagement_pct (REAL) + engagement_inert (TEXT); auto-ALTER backfills NULL.
  • qa/release_readiness.py — the story_engagement deterministic gate + ENGAGEMENT report section + signals.
  • qa/SCORING.md — documents the axis + WARN-first→FATAL discipline.
  • .github/workflows/ci.yml — adds test_feature_engagement.py to the qa CI allowlist.
  • qa/test_deterministic_rri_gate.py / qa/test_release_readiness.py — admit the additive evidence-gap skip; add WS0 gate behavior tests (absent⇒skip, all-WARN-inert⇒pass+report, cross-persona roll-up, FATAL⇒fail). gates_total/RRI invariants unchanged.

Tests (single-process, -p no:xdist)

  • qa/test_feature_engagement.py + servers/engine/tests/test_feature_engagement_manifest.py33 pass.
  • 299 targeted (engagement + structural + RRI + scores_db + behavioral + latency) — green.
  • Full servers/engine/tests/3000 pass.

Do not merge — for review.

Summary by CodeRabbit

Release Notes

  • New Features

    • Story engagement quality gate now evaluates authored system engagement during narrative progression
    • Engagement coverage metrics are tracked in release scoring ledger
  • Tests

    • Comprehensive engagement coverage test suite added with scenarios for solo parties, faction availability, and consequence timing

…er + forcing gate (WS0, all-WARN)

WS0 closes the keystone QA blind spot: today an entire authored story subsystem
(companion approval, camp downtime, faction questlines, the companion agenda,
decisions) can be 100% INERT across a 5-persona sweep and the RRI still scores
10/10 — no gate is engagement-coverage. A frozen-relationship run that narrated
the companion but never moved a gauge passed; a run that seeded factions and never
joined one passed.

WHAT THIS ADDS (all strictly additive; every system ships severity='warn' — ZERO
new fatals, so a currently-green pipeline stays green):

* qa/feature_engagement.py — a pure module: a SystemSpec manifest of the 10 authored
  story systems + engagement_coverage(state, tool_counts, session_beats) returning
  {coverage, engaged[], na[], inert[{id,why,severity}]}. ENGAGED if the detector is
  true; N/A if the precondition is false/unknown; INERT iff the precondition is TRUE
  and the detector FALSE (owed-but-dead — the signal). PURE-READ over engine-mutated
  snapshot state (attitude_value, last_long_rest_day, faction.joined/standing,
  narrative_arc.act, consequence.fired/trigger_day, the arc/agenda fired flags,
  campaign.decisions/quests/factions/*_arcs) or DM tool-counts — never fiction.
  Reuses story_readout.structural_coverage_from_state / felt_shape_from_state so the
  shared buckets never drift; old snapshots round-trip (null-guards throughout).

* servers/engine/tests/test_feature_engagement_manifest.py — the forcing meta-test
  (mirrors test_tool_schema_budget.py): asserts {s.id for s in SYSTEMS} ==
  REVIEWED_SYSTEM_IDS, every severity in {fatal,warn}, ids unique+snake_case,
  precondition/detector callable — so changing the manifest is a visible, reviewed diff.

* qa/test_feature_engagement.py — table-driven behavior tests: INERT-in-scope (the
  keystone), COMPLETE→engaged, and the conditional N/A logic (short run, solo party,
  factionless world, session_beats=None, combat-sprint), consequences due-vs-future
  (strict < final day), narrative_arc absent/None null-guard.

WIRING (additive at every callsite, defensive import → degrade-to-no-op):
* qa/assert_behavioral.py — emits one engagement_<id> WARN per inert system after
  structural_completeness. All-WARN ⇒ adds ZERO fatals.
* qa/inject_structural_coverage.py — merges score['engagement_coverage'] beside
  structural_coverage (session_beats=None → beats-keyed systems N/A — safe).
* qa/scores_db.py — adds engagement_pct (REAL) + engagement_inert (TEXT); auto-ALTER
  backfills NULL on a legacy db.
* qa/release_readiness.py — adds ONE deterministic gate 'story_engagement'. Cross-persona
  roll-up: owed-by-≥1 AND engaged-by-none ⇒ inert for the sweep; FAILS only on a FATAL
  inert system (all-WARN ⇒ always passes when evidence is present). EVIDENCE-GAP SKIP
  when no persona block carries engagement_coverage, so a legacy corpus's RRI is
  byte-identical (mirrors the latency-gate skip). Prints an ENGAGEMENT section naming
  each inert system + a fix hint.
* qa/SCORING.md — documents the axis, the forcing meta-test, the gate, and the
  WARN-first→FATAL graduation discipline (graduation is a FUTURE post-sweep PR).
* .github/workflows/ci.yml — adds test_feature_engagement.py to the qa CI allowlist.

DEFERRED (by design): FATAL graduation is a future post-sweep PR after one real sweep
calibrates the inert/owed classification. Two systems (faction_arc, companion_quest_arc)
are BLOCKED and stay WARN regardless — a snapshot-only precondition can't tell
seeded-but-locked from never-seeded (a known open spike).

Tests: 299 targeted (qa engagement + structural + RRI + scores_db + behavioral) +
3000 engine tests green, single-process (-p no:xdist). Two pre-existing exact
skipped_gates assertions updated to admit the additive story_engagement evidence-gap
skip; gates_total/RRI invariants (11 gates, 10.0) unchanged.
@coderabbitai

coderabbitai Bot commented Jun 18, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@100yenadmin, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 52 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3456518d-7b8f-4f0c-bc3a-3ab14d18a44f

📥 Commits

Reviewing files that changed from the base of the PR and between 417a2b2 and e7c22fa.

📒 Files selected for processing (4)
  • qa/SCORING.md
  • qa/feature_engagement.py
  • qa/inject_structural_coverage.py
  • qa/test_feature_engagement.py
📝 Walkthrough

Walkthrough

Introduces a WS0 "dead-system tracker" (qa/feature_engagement.py) that classifies 10 authored story systems as engaged, inert, or na using engine snapshots and tool counts. The scorer is wired into per-session behavioral gates and score injection, a new deterministic story_engagement release gate aggregates results across personas, the scores ledger schema is extended, and documentation, unit, manifest-guard, and integration tests cover all behavior.

Changes

WS0 Feature Engagement Coverage System

Layer / File(s) Summary
Core engagement scorer: Ctx, SystemSpec manifest, and engagement_coverage
qa/feature_engagement.py
Defines the Ctx parsing context and SystemSpec NamedTuple, implements the 10-system manifest with precondition/detector pairs, engagement_coverage classification logic (engaged/inert/na) with exception safety, WORLDOS_GATE_COMBAT_SPRINT fatal-skip hook, inert why-strings, and a CLI main.
Per-session gate wiring
qa/assert_behavioral.py, qa/inject_structural_coverage.py
Adds defensive imports and calls engagement_coverage in the behavioral gate (emitting chk("engagement_<id>") per inert system) and in inject_structural_coverage.py via compute_engagement() which writes the block to each persona's score.json.
story_engagement release gate, ledger schema, and output signals
qa/release_readiness.py, qa/scores_db.py
Adds "story_engagement" to DETERMINISTIC_GATES, introduces _ENGAGEMENT_FIX_HINTS, aggregates per-persona engagement blocks into a cross-sweep gate that fails only on fatal-inert systems, skips when no evidence is present, extends signals JSON with engagement partition fields and a console ENGAGEMENT section. Extends scores_db.py schema with engagement_pct (REAL) and engagement_inert (TEXT) columns and Markdown rendering.
Unit tests for engagement_coverage and manifest guard
qa/test_feature_engagement.py, servers/engine/tests/test_feature_engagement_manifest.py
Adds 19 unit tests covering inert/engaged/na classification, beat-threshold N/A, solo/factionless/nonjoinable N/A, WORLDOS_GATE_COMBAT_SPRINT behavior, consequence timing, null guards, empty snapshot regression, and tool-count-only engagement. Adds manifest-drift guard tests pinning IDs, cardinality, severity vocabulary, all-warn WS0 invariant, and callability.
Integration tests, CI wiring, and docs
qa/test_deterministic_rri_gate.py, qa/test_release_readiness.py, .github/workflows/ci.yml, qa/SCORING.md
Adds four story_engagement gate scenario tests (absent → skip, warn-inert → pass with reporting, cross-persona roll-up, fatal-inert → fail); updates skipped-gate expectations in release-readiness tests; registers test_feature_engagement.py in CI; documents WS0 semantics, invariants, and integration points in SCORING.md.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • electricsheephq/WorldOS#413: Introduced the RRI gate framework in qa/release_readiness.py that this PR extends by adding the story_engagement deterministic gate and its evidence-aggregation logic.

Poem

🐇 Hop hop, the rabbit checks each system's beat,
No story left untracked, no quest obsolete!
inert, engaged, or na—each one classified,
The manifest is locked, ten systems inside.
Fatal inerts fail the gate, warn ones just confess,
The dead-system tracker keeps WorldOS in progress! ✨

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 40.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The PR description provides comprehensive context covering the blind spot, all-WARN design rationale, conditional N/A logic, deterministic snapshot-only approach, manifest meta-test, and the 10 tracked systems. However, the required CLA/licensing checkbox section from the template is not completed. Complete the Licensing/CLA section by checking the two required checkboxes confirming CLA agreement and absence of confidential/third-party-restricted material.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The PR title accurately summarizes the main change: introducing a feature-engagement feedback loop (WS0) with manifest, coverage scorer, and forcing gate—all at WARN severity.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
qa/inject_structural_coverage.py (1)

106-125: 💤 Low value

Duplicate snapshot/transcript loading logic in compute_engagement().

This function duplicates lines 91-103 from compute(). While acceptable for clarity and defensive isolation, a small refactor could extract the shared snapshot+transcript resolution into a helper to reduce duplication.

Not blocking since both functions are small and the duplication is intentional for defensive isolation (a bug in one doesn't affect the other).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/inject_structural_coverage.py` around lines 106 - 125, The
compute_engagement() function contains duplicate snapshot and transcript loading
logic that is also present in the compute() function (lines 91-103). Extract the
shared snapshot resolution and transcript path handling logic into a separate
helper function (such as _load_snapshot_and_transcript() or
_resolve_snapshot_and_transcript()) that both compute() and compute_engagement()
can call. This reduces code duplication while maintaining the defensive
isolation between the two functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@qa/scores_db.py`:
- Around line 423-424: The schema columns engagement_pct and engagement_inert
are defined but the write path does not populate them. Locate where add_run() is
called in the code that processes score.json (likely in
inject_structural_coverage.py or the function that calls it), and extract the
engagement_coverage data from the scores. Calculate engagement_pct as the ratio
of engaged systems to total systems (engaged divided by engaged plus inert),
extract the inert system IDs and create engagement_inert as a comma-separated
string of those IDs, then pass both computed values as parameters to the
add_run() function call.

In `@qa/test_feature_engagement.py`:
- Around line 130-131: The variable `eng` in the coverage parsing statement is
assigned from unpacking block["coverage"].split("/") but never used, which
triggers the RUF059 lint warning. Replace `eng` with an underscore (_) to
indicate the variable is intentionally discarded and not needed for the
subsequent assertion that only uses the `exp` value.

---

Nitpick comments:
In `@qa/inject_structural_coverage.py`:
- Around line 106-125: The compute_engagement() function contains duplicate
snapshot and transcript loading logic that is also present in the compute()
function (lines 91-103). Extract the shared snapshot resolution and transcript
path handling logic into a separate helper function (such as
_load_snapshot_and_transcript() or _resolve_snapshot_and_transcript()) that both
compute() and compute_engagement() can call. This reduces code duplication while
maintaining the defensive isolation between the two functions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 30de4822-75b1-4cbf-90eb-18e8dedfee15

📥 Commits

Reviewing files that changed from the base of the PR and between c23dd98 and 417a2b2.

📒 Files selected for processing (11)
  • .github/workflows/ci.yml
  • qa/SCORING.md
  • qa/assert_behavioral.py
  • qa/feature_engagement.py
  • qa/inject_structural_coverage.py
  • qa/release_readiness.py
  • qa/scores_db.py
  • qa/test_deterministic_rri_gate.py
  • qa/test_feature_engagement.py
  • qa/test_release_readiness.py
  • servers/engine/tests/test_feature_engagement_manifest.py

Comment thread qa/scores_db.py
Comment on lines +423 to +424
("engagement_pct", "Engagement"),
("engagement_inert", "Inert systems"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's a write path that populates engagement_pct and engagement_inert
rg -n "engagement_pct|engagement_inert" qa/scores_db.py -B 5 -A 5
echo "---"
# Check if add_run or similar writes these fields
ast-grep --pattern 'def add_run($$$)'

Repository: electricsheephq/WorldOS

Length of output: 9137


🏁 Script executed:

# Search for calls to add_run with engagement fields
rg -n "add_run\s*\(" qa/ -A 20 | grep -E "(add_run|engagement_pct|engagement_inert)" | head -50

Repository: electricsheephq/WorldOS

Length of output: 5774


🏁 Script executed:

# Check if feature_engagement module exists and computes these values
find . -name "*feature_engagement*" -type f

Repository: electricsheephq/WorldOS

Length of output: 185


🏁 Script executed:

# Search for any assignment to engagement_pct or engagement_inert
rg -n "engagement_pct\s*=" --type py
rg -n "engagement_inert\s*=" --type py

Repository: electricsheephq/WorldOS

Length of output: 305


🏁 Script executed:

# Check feature_engagement.py for engagement_coverage function
head -100 qa/feature_engagement.py

Repository: electricsheephq/WorldOS

Length of output: 4815


🏁 Script executed:

# Search for calls to engagement_coverage function
rg -n "engagement_coverage" --type py -B 2 -A 2

Repository: electricsheephq/WorldOS

Length of output: 19906


🏁 Script executed:

# Check if engagement_coverage is being passed to add_run anywhere
rg -n "engagement_coverage.*add_run|add_run.*engagement" --type py -B 3 -A 3

Repository: electricsheephq/WorldOS

Length of output: 49


🏁 Script executed:

# Check if engagement_pct/engagement_inert are read from scores_db anywhere
rg -n "engagement_pct|engagement_inert" --type py -B 2 -A 2 | grep -v "scores_db.py"

Repository: electricsheephq/WorldOS

Length of output: 2243


🏁 Script executed:

# Check if there's any backfill/migration that might populate these fields
rg -n "backfill|migrate" qa/ --type py | head -20

Repository: electricsheephq/WorldOS

Length of output: 629


🏁 Script executed:

# Look at how engagement_coverage is actually used in release_readiness
sed -n '1144,1180p' qa/release_readiness.py

Repository: electricsheephq/WorldOS

Length of output: 2523


The schema columns engagement_pct and engagement_inert are defined but not actually populated by the write path.

The engagement_coverage block is computed by qa/feature_engagement.engagement_coverage() and stored in score.json by inject_structural_coverage.py, but the data is never transformed and written to the database via add_run(). The values would need to be extracted as engagement_pct = len(engaged)/(len(engaged)+len(inert)) and engagement_inert = comma-joined inert system ids, then passed to add_run(). Currently, no code performs this transformation or population step.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/scores_db.py` around lines 423 - 424, The schema columns engagement_pct
and engagement_inert are defined but the write path does not populate them.
Locate where add_run() is called in the code that processes score.json (likely
in inject_structural_coverage.py or the function that calls it), and extract the
engagement_coverage data from the scores. Calculate engagement_pct as the ratio
of engaged systems to total systems (engaged divided by engaged plus inert),
extract the inert system IDs and create engagement_inert as a comma-separated
string of those IDs, then pass both computed values as parameters to the
add_run() function call.

Comment on lines +130 to +131
eng, exp = block["coverage"].split("/")
assert int(exp) == len(block["engaged"]) + len(block["inert"])

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove the unused unpacked variable in coverage parsing.

On Line 130, eng is assigned but never used (RUF059), which can trip lint-strict CI.

Suggested patch
-    eng, exp = block["coverage"].split("/")
+    _eng, exp = block["coverage"].split("/")
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
eng, exp = block["coverage"].split("/")
assert int(exp) == len(block["engaged"]) + len(block["inert"])
_eng, exp = block["coverage"].split("/")
assert int(exp) == len(block["engaged"]) + len(block["inert"])
🧰 Tools
🪛 Ruff (0.15.17)

[warning] 130-130: Unpacked variable eng is never used

Prefix it with an underscore or any other dummy variable pattern

(RUF059)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@qa/test_feature_engagement.py` around lines 130 - 131, The variable `eng` in
the coverage parsing statement is assigned from unpacking
block["coverage"].split("/") but never used, which triggers the RUF059 lint
warning. Replace `eng` with an underscore (_) to indicate the variable is
intentionally discarded and not needed for the subsequent assertion that only
uses the `exp` value.

Source: Linters/SAST tools

… thread filter

Adversarial pre-merge review found 2 verified one-line majors (files already in the diff):
1. REVIEWED_SYSTEM_IDS was `frozenset(s.id for s in SYSTEMS)` — DERIVED from SYSTEMS, so the
   forcing meta-test compared SYSTEMS against itself, a tautology that could never fail,
   defeating the manifest-drift guard the keystone exists for. Now an INDEPENDENT hardcoded
   literal of the 10 ids; editing SYSTEMS without updating it is a real failure. (Stronger
   "auto-detect a brand-new untracked player-facing tool" forcing is a noted follow-up — needs
   a tool->system classification.)
2. consequences_fired false-INERTed on re-armed worldsim standing-thread consequences
   (worldsim.tick rolls trigger_day forward in place, fired=False forever) — _owed_consequences
   now excludes thread_id-tagged ones, matching the engine's own due/overdue contract
   (consequences.due `not c.thread_id`; scene_debt thread skip). +regression test.

Nits: guard the inject callsite in try/except (mirror assert_behavioral); tighten the
"byte-identical RRI" doc to "RRI math byte-identical; rri.json gains additive engagement_* keys".
Pre-graduation calibration (factions joinable, 'failed' stage status, acts has_arc, session_beats
from transcript) deferred to the WARN->FATAL graduation PR per the review. 114 WS0 tests green.
@100yenadmin
100yenadmin merged commit 4c710aa into main Jun 18, 2026
18 of 19 checks passed
100yenadmin added a commit that referenced this pull request Jun 19, 2026
…mparable across rulers (#1034)

The 2026-06 cycle materially tightened the scoring ruler (feature-engagement coverage scorer #1018,
acts felt-shape #1001/#1002, betrayal un-inversion #999, romance gate #997, dm_advanced_time unmask
#1024, gate-severity accuracy #1030), so a run scores LOWER under sc_d4b93982763a/lc_d7fcfddd5bf7
than under the v1.0.4 rulers — BY DESIGN (the scorer is a tightening feedback loop). Document the
ruler-version mechanism + history in SCORING.md §0 and annotate it in the v1.0.5-rc1 CHANGELOG, so
current numbers are never mis-compared to historic ones (every scores_db row is fenced by
scoring_config_version/lens_config_version). Stable-checkpoint hygiene per owner.

Co-authored-by: Eva <arncalso@gmail.com>
100yenadmin pushed a commit that referenced this pull request Jul 2, 2026
Add 5 progression/closure obligations to _compute_beat_obligations (the SOLE
every-beat cue seam ridden by persist_beat + scene_context.durable), keeping the
HARD mechanical loop from quietly stalling — the relationship cues (#1017/#1018/

  - party_stuck_one_location (med): 8+ act-local beats, <2 visited locations, no
    in-place-progression (byte-identical to assert_behavioral's party_traveled
    exception) -> travel_to / add_location.
  - combat_left_hanging (med): combat active but no living hostile (mirrors
    end_combat's order-based detection) -> end_combat. Owns the beat over
    xp_unawarded while combat is active.
  - xp_unawarded (med): xp-mode, NON-combat, living party member, a defeated
    monster still carrying xp_value>0 (proactive twin of the xp_not_orphaned
    FATAL) -> end_combat / award_xp.
  - clock_dm_frozen (low): substantial beats, day==1 & morning, not in combat,
    HONEST snapshot proxy; fires only when visited>=2 (party_stuck owns the clock
    otherwise) -> advance_time / long_rest / downtime.
  - quest_unresolved_late (med): substantial beats, a quest exists, zero quests
    completed AND no objective ever recorded done, anti-spam vs quest_resolvable/
    quest_stalled -> complete_objective / complete_quest.

All CUE-ONLY (Option A — no engine auto-action); pure reads of engine-mutated
gauges with defensive getattr (older/partial snapshot degrades a cue to skipped);
precedence gates collapse the worst case to ~2-3 cues. ADDITIVE: a fully-progressed
snapshot still yields [] (no obligations key). Pin _PARTY_STUCK_BEATS=8 to
assert_behavioral's SINGLE_SCENE_MIN_BEATS.

Tests (TDD): per-kind FIRE+CLEAR + a fully-progressed-snapshot empty-digest case
in test_beat_obligations.py; new deterministic qa/test_ws3a_progression_invariants.py
(NO LLM) proves each named verb MOVES its gauge on a real persisted campaign + the
cue fires-then-clears, wired into qa/fast_gate.sh. Skill docs (SKILL.md step-6b +
AGENT.md closure obligations) name the 5 cues.

fast_gate: GREEN (253 passed). focused suite: 83 passed.
100yenadmin added a commit that referenced this pull request Jul 2, 2026
…#1160)

* feat(engine): WS3a — DM-unavoidable per-beat progression/closure cues

Add 5 progression/closure obligations to _compute_beat_obligations (the SOLE
every-beat cue seam ridden by persist_beat + scene_context.durable), keeping the
HARD mechanical loop from quietly stalling — the relationship cues (#1017/#1018/

  - party_stuck_one_location (med): 8+ act-local beats, <2 visited locations, no
    in-place-progression (byte-identical to assert_behavioral's party_traveled
    exception) -> travel_to / add_location.
  - combat_left_hanging (med): combat active but no living hostile (mirrors
    end_combat's order-based detection) -> end_combat. Owns the beat over
    xp_unawarded while combat is active.
  - xp_unawarded (med): xp-mode, NON-combat, living party member, a defeated
    monster still carrying xp_value>0 (proactive twin of the xp_not_orphaned
    FATAL) -> end_combat / award_xp.
  - clock_dm_frozen (low): substantial beats, day==1 & morning, not in combat,
    HONEST snapshot proxy; fires only when visited>=2 (party_stuck owns the clock
    otherwise) -> advance_time / long_rest / downtime.
  - quest_unresolved_late (med): substantial beats, a quest exists, zero quests
    completed AND no objective ever recorded done, anti-spam vs quest_resolvable/
    quest_stalled -> complete_objective / complete_quest.

All CUE-ONLY (Option A — no engine auto-action); pure reads of engine-mutated
gauges with defensive getattr (older/partial snapshot degrades a cue to skipped);
precedence gates collapse the worst case to ~2-3 cues. ADDITIVE: a fully-progressed
snapshot still yields [] (no obligations key). Pin _PARTY_STUCK_BEATS=8 to
assert_behavioral's SINGLE_SCENE_MIN_BEATS.

Tests (TDD): per-kind FIRE+CLEAR + a fully-progressed-snapshot empty-digest case
in test_beat_obligations.py; new deterministic qa/test_ws3a_progression_invariants.py
(NO LLM) proves each named verb MOVES its gauge on a real persisted campaign + the
cue fires-then-clears, wired into qa/fast_gate.sh. Skill docs (SKILL.md step-6b +
AGENT.md closure obligations) name the 5 cues.

fast_gate: GREEN (253 passed). focused suite: 83 passed.

* fix(ws3a): xp_unawarded cue points only at the ledger-consuming path (end_combat) — bare award_xp invites duplicate awards (CodeRabbit Major, verified vs _award_kill_xp idempotent-zero at server.py:6919); tighten AGENT.md quest-stall wording

* docs+lint(ws3a): SKILL.md quest_unresolved_late guard precision (CodeRabbit); drop unused imports (code-quality)

* fix(ws3a): xp_unawarded points at award_xp (fires out-of-combat → end_combat inapplicable, evaos P2); fix combat_left_hanging mirror-site citation (evaos P3, 6467→~7009); SKILL.md 6b match; follow-up #1312 for engine reconcile of stranded kill-XP

---------

Co-authored-by: Eva <arncalso@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant