mirror of
https://github.com/got-feedBack/feedBack.git
synced 2026-09-11 22:04:30 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
244d5df42d |
@@ -0,0 +1,139 @@
|
||||
# Feature Specification: Progression Contributors — Plugin-Shipped Challenge Content And Custom Events
|
||||
|
||||
**Feature Branch**: `spec/011-progression-contributors`
|
||||
**Created**: 2026-06-12
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Design the deferred `contributor` slice of the progression domain (spec 010) so plugins can ship their own challenge/quest content and emit their own progression event types. Driving use case: the SlopScale practice plugin's leveling mechanisms (tempo-tier flips, clean-BPM personal bests, key travels, proof-loop claims)."
|
||||
|
||||
> Companion to spec 010 (`specs/010-progression-domain/spec.md`), which ships the core
|
||||
> progression engine with core-bundled content only and documents this slice as
|
||||
> deferred. The driving requirements come from SlopScale's progression design
|
||||
> (`progression-leveling-detail.md`, v0.7.23-dev): a plugin with a rich internal
|
||||
> ladder whose *proven outcomes* — a tempo-tier flip, a monotonic PB raise, a
|
||||
> key traveled, a verified voice-leading claim — are exactly the events a
|
||||
> Mastery-Rank challenge wants to count, and none of which are expressible in
|
||||
> spec 010's goal vocabulary (`minigame_run` carries only `game_id` + `score`).
|
||||
|
||||
## Clarifications
|
||||
|
||||
### Session 2026-06-12
|
||||
|
||||
- Q: How does contributed content reach the engine — runtime registration or declarative file? → A: Declarative. A plugin ships a progression content JSON in its own directory and references it from `plugin.json`; core loads it at plugin load with the same warn-and-skip validation as bundled content. No runtime content-mutation API in this slice (content stays reviewable, restart-stable, and inspectable).
|
||||
- Q: Can plugins append challenges to core paths' levels? → A: No (deferred). Appending would let a plugin silently change another path's level-up closure. Plugins contribute whole new paths and quest-pool entries only.
|
||||
- Q: Do contributed quests dilute the core daily/weekly rotation? → A: Yes, deliberately: contributed quest entries join the shared pools and the same deterministic rotation. Per-plugin bonus quest slots are deferred until dilution is observed to be a real problem.
|
||||
- Q: What happens to earned progress when a contributing plugin is disabled or uninstalled? → A: It persists. Mastery Rank never decreases (spec 010 invariant): contributed path levels keep counting, completed challenges stay completed, and the orphan-path rendering shipped in spec 010 (`_progression_overview`) already shows them. Active (incomplete) contributed content is hidden and the event whitelist shrinks.
|
||||
- Q: Are contributed rewards bounded? → A: Yes. Contributed quest `reward_db` is clamped to a per-quest cap with a load warning (core's richest weekly quest pays 300 dB; the cap is 500). Challenges award no dB directly, same as core content. Decibels remain earn-by-playing only — contributed content cannot create a money path.
|
||||
- Q: Who may emit a plugin's event types? → A: Anyone on the local honor system (constitution: single trusted user; validation guards accidents, not cheaters) — but the whitelist only admits event types declared by an ENABLED plugin's validated content, and the type namespace binds events to their declaring plugin.
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - A Practice Plugin Ships Its Own Path (Priority: P1)
|
||||
|
||||
The SlopScale developer ships a "Woodshed" instrument path whose challenges count his plugin's proven outcomes — "clear the Push rung on any pathway", "raise a clean-tempo PB past 90 BPM", "travel 3 keys on one pathway" — without any core code change, and players level that path into their Mastery Rank exactly like a core path.
|
||||
|
||||
**Why this priority**: This is the whole point of the slice — the richest progression content lives in plugins that know what "proven" means for their own mechanics, and core's job is counting, not understanding tempo tiers.
|
||||
|
||||
**Independent Test**: Install a fixture plugin declaring a path with `plugin_event` goals; emit matching events; the path levels and Mastery Rank rises; core content and tests are untouched.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a plugin whose `plugin.json` references a progression content file declaring path `slopscale.woodshed`, **When** the plugin loads, **Then** the path appears in `available_paths`, is selectable, and its challenges render on the Progress screen like core content.
|
||||
2. **Given** a challenge with goal `{type: "plugin_event", event: "slopscale.tier_cleared", match: {tier: 4}, target: 1}`, **When** the plugin records `slopscale.tier_cleared {pathway: "blues_foundation", tier: 4, bpm: 96}`, **Then** the challenge completes and a level-up follows the same required-count rules as core paths.
|
||||
3. **Given** numeric threshold params, **When** a goal declares `min: {bpm: 90}`, **Then** only events whose `bpm` payload is ≥ 90 advance it.
|
||||
4. **Given** content edits in a plugin update, **Then** new levels/challenges appear after restart with zero core changes, and previously completed challenges stay completed.
|
||||
5. **Given** mis-authored contributed content (duplicate ids, unknown goal types, missing event namespace), **Then** core logs warnings and skips the invalid entries — a plugin can never crash or block core progression (contrast: the plugin's own internal `assert`-and-throw guards are its business; contributed content is always degraded, never fatal).
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Namespaced Custom Events Through One Choke Point (Priority: P1)
|
||||
|
||||
A plugin records its proven outcomes as namespaced progression events through the existing surfaces — the backend `record_progression_event` context hook or the frontend `progression` capability `record-event` command — and the external whitelist extends automatically to the event types its validated content declares.
|
||||
|
||||
**Why this priority**: Without event intake there is nothing for contributed goals to count; without namespacing and whitelist discipline the single-authority model of spec 010 (server-derived `song_completed`) erodes.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** content declaring `events: ["slopscale.tier_cleared", "slopscale.pb_raised", "slopscale.key_traveled"]`, **When** the plugin is enabled, **Then** `POST /api/progression/events` (and the capability `record-event`) accepts exactly those types in addition to the spec 010 whitelist; unknown or undeclared types are still rejected with a safe outcome.
|
||||
2. **Given** an event type not prefixed with the declaring plugin's id, **When** content loads, **Then** the declaration is skipped with a warning (a plugin cannot claim another plugin's — or core's — namespace, including `song_completed`).
|
||||
3. **Given** the plugin is disabled, **When** its event type is posted, **Then** it is rejected like any unknown type, and nothing already earned changes.
|
||||
4. **Given** an event with a payload that violates the scalar/size rules of spec 010, **Then** it is rejected identically to v1 intake (same validation, same caps).
|
||||
5. **Given** a recorded plugin event, **Then** the outcome summary (completed challenges/quests, level-ups) returns to the caller and the standard lifecycle events fire, so the plugin can render the completion in its own UI (e.g. SlopScale's run-end recognizer surface).
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Contributed Quests Join The Rotation (Priority: P2)
|
||||
|
||||
A plugin contributes quest-pool entries ("hold a groove at 80 BPM for a daily", "clear two rungs this week") that rotate, reward dB, and feed `quest_completed` challenges exactly like core quests.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** contributed daily/weekly pool entries, **When** a new period instantiates, **Then** the deterministic rotation draws from the combined core+contributed pool (same period key → same selection, restart-safe).
|
||||
2. **Given** a contributed quest with `reward_db: 5000`, **When** content loads, **Then** the reward clamps to the cap (500) with a warning; completion awards through `award_xp(…, "quests")` like any quest.
|
||||
3. **Given** the contributing plugin is disabled mid-period, **Then** its live quest instances are hidden from the API payload (same as a quest removed from a core pool mid-period) and re-enabled content reappears without duplication.
|
||||
|
||||
---
|
||||
|
||||
### User Story 4 - Support Tooling Sees Contributors (Priority: P3)
|
||||
|
||||
A maintainer can tell which plugin contributed which paths/quests/event types, and see that plugin's content-load warnings, through the Capability Inspector and the progression diagnostics contributor.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** a contributing plugin, **Then** it appears in the `progression` pipeline as a `contributor` participant (manifest `capabilities.progression.roles: ["contributor", …]`), visible in the Inspector with its declared event types.
|
||||
2. **Given** content-load warnings for a plugin, **Then** the progression diagnostics snapshot (`slopsmith.progression.diag.v1`) attributes them to that plugin id, within the existing redaction rules (counts and ids only — no song filenames, no display names).
|
||||
3. **Given** an event recorded through the capability command, **Then** the decision log attributes it to the requesting plugin.
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- Two plugins (or a plugin and core) declaring the same path/challenge/quest/event id → first loaded wins, later duplicates skipped with warnings (deterministic: core first, then plugins alphabetically).
|
||||
- A contributed path selected by the player, then the plugin's content shrinks (level removed) → same rules as core content edits: completed stays completed, orphaned progress rows are harmless, max level drops only the *next* target.
|
||||
- `distinct`-style dedupe on plugin events → supported via the same `progress_detail` mechanism keyed on a declared payload field (e.g. `distinct_by: "pathway"` counts unique pathways), since "travel 3 keys" must not count the same key thrice.
|
||||
- Event flood from a buggy plugin → same per-event payload caps as v1; rate limiting is out of scope (honor system), but the 64 KB diagnostics cap and bounded recent-outcome history must hold.
|
||||
- Integrity guidance (unenforceable but specified): contributors SHOULD emit an event only when the underlying outcome was actually proven by their own standards — SlopScale's "a claim exists only when something was proven" anti-inflation rule is the reference model. Core's defense stays structural: namespacing, whitelisting, bounded rewards, and rank deriving only from challenge closure.
|
||||
- A `plugin_event` goal in CORE content referencing a plugin event type is allowed (core may count "clear a SlopScale rung" in a core path) and simply never advances while the plugin is absent — same satisfiable-later semantics as drums content in spec 010.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: A plugin MUST be able to contribute progression content by referencing a JSON file from its manifest; core MUST load it at plugin load through the same validation pipeline as bundled content (warn-and-skip, never fatal).
|
||||
- **FR-002**: Contributed ids (paths, challenges, quests, event types) MUST be namespaced under the contributing plugin's id; declarations outside that namespace MUST be skipped with a warning. Core ids and core event types (`song_completed`, `minigame_run`, `quest_completed`, …) are unclaimable.
|
||||
- **FR-003**: Contributed content MAY declare new paths and daily/weekly quest-pool entries; it MUST NOT modify core paths, core levels, core quests, or the shop. Shop contribution is explicitly out of scope.
|
||||
- **FR-004**: The engine MUST support a `plugin_event` goal type: `{type: "plugin_event", event: <namespaced type>, match?: {field: scalar, …}, min?: {field: number, …}, distinct_by?: <payload field>, target}` — equality on `match` fields, ≥ thresholds on `min` fields, optional unique-value counting via the existing `progress_detail` mechanism.
|
||||
- **FR-005**: The external event whitelist (HTTP + capability `record-event`) MUST extend to exactly the event types declared by enabled plugins' validated content, shrinking when a plugin is disabled; all other spec 010 intake rules (scalar payloads, size caps, safe rejection outcomes) apply unchanged.
|
||||
- **FR-006**: The backend `record_progression_event` context hook MUST keep working unchanged for trusted backend code, including for namespaced types.
|
||||
- **FR-007**: Contributed paths MUST integrate with Mastery Rank, the Progress screen, level-up closure, and the orphan-path rendering identically to core paths; earned levels and completions MUST persist across plugin disable/uninstall (rank never decreases).
|
||||
- **FR-008**: Contributed quest `reward_db` MUST clamp to 500 with a load warning; Decibels remain earn-by-playing only — contributed content MUST NOT introduce any purchase or exchange surface.
|
||||
- **FR-009**: Quest rotation MUST stay deterministic over the combined pool; a disabled plugin's live quest instances are hidden (not deleted) and reappear without duplication on re-enable within the same period.
|
||||
- **FR-010**: Contributing plugins MUST be able to declare the `contributor` role in their manifest `capabilities.progression` block; the runtime MUST surface them as participants of the `progression` pipeline with their declared event types, visible in the Capability Inspector.
|
||||
- **FR-011**: Progression diagnostics MUST attribute contributed content counts and load warnings per plugin id, within spec 010's redaction rules and size cap.
|
||||
- **FR-012**: Duplicate-id resolution MUST be deterministic: core content first, then plugins in alphabetical plugin-id order; later duplicates skip with warnings.
|
||||
- **FR-013**: All schema/storage changes MUST remain additive and idempotent; contributed challenges/quests reuse the spec 010 tables unchanged (namespaced ids are sufficient).
|
||||
- **FR-014**: Content contributed by a plugin whose manifest declares an unsupported progression-content schema version MUST be skipped with a warning (versioned: `progression-content.v1`).
|
||||
- **FR-015**: A plugin's outcome summary and lifecycle events for its own recorded events MUST be delivered to it like any requester, so contributed completions can render inside the plugin's own UI moments (e.g. a run-end modal).
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **Contributed Content Bundle**: The validated, namespaced set of paths/quests/event-type declarations one plugin ships (`progression-content.v1`).
|
||||
- **Plugin Event Type**: A namespaced progression event (`<plugin_id>.<name>`) declared by content; the unit the whitelist and `plugin_event` goals bind to.
|
||||
- **`plugin_event` Goal**: The content-side matcher (event + equality `match` + numeric `min` + optional `distinct_by` + `target`) that turns plugin outcomes into challenge/quest progress.
|
||||
- **Contributor Participant**: The plugin's `progression` pipeline membership (role `contributor`), carrying its declared event types for inspection.
|
||||
- **Orphaned Contribution**: Progress earned from content whose plugin is now absent — persisted, rank-counting, rendered via the spec 010 orphan path.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: The SlopScale pilot can express "clear the Push rung", "PB past 90 BPM (clean)", and "travel 3 distinct keys" as contributed challenges with zero core code changes — content + events only.
|
||||
- **SC-002**: Disabling/uninstalling the contributing plugin mid-progress leaves the full test suite green, Mastery Rank unchanged, and the Progress screen rendering without errors.
|
||||
- **SC-003**: Undeclared, foreign-namespace, and disabled-plugin event types are all rejected at external intake with safe outcomes (verified by API tests).
|
||||
- **SC-004**: Same period key + same combined pool yields identical quest rotation across restarts with contributed entries present.
|
||||
- **SC-005**: A deliberately mis-authored fixture bundle (duplicate ids, foreign namespace, absurd rewards, unknown goal type) loads as warnings only, with every invalid entry skipped and every valid sibling alive.
|
||||
- **SC-006**: The Inspector lists the contributor participant with its event types; diagnostics stay under the 64 KB cap with per-plugin warning attribution.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- Spec 010 is merged and its engine/tables/API are the substrate; this slice adds no new tables.
|
||||
- Single trusted local user (constitution): event intake remains honor-system; the controls here (namespacing, whitelist, clamps, determinism) guard accidents and content mistakes, not adversaries.
|
||||
- SlopScale remains the pilot consumer; its internal woodshed XP/levels stay plugin-internal by design (non-spendable practice evidence ≠ the dB wallet) — this slice transports its *proven outcomes*, not its ledger.
|
||||
- Per-plugin bonus quest slots, challenge injection into core path levels, plugin-contributed shop items, and runtime content registration are explicitly deferred.
|
||||
Reference in New Issue
Block a user