From e05572bcecc4e202e08c81840e576fb9c33a6927 Mon Sep 17 00:00:00 2001 From: Willem van den Ende Date: Fri, 12 Jun 2026 22:08:36 +0100 Subject: [PATCH] skills I believed were committed --- .pi/skills/allium/SKILL.md | 16 +-- .pi/skills/distill/SKILL.md | 138 ++++++++++++-------- .pi/skills/elicit/SKILL.md | 42 +++--- .pi/skills/propagate/SKILL.md | 22 ++-- .pi/skills/tend/SKILL.md | 6 +- .pi/skills/user-story-conversation/SKILL.md | 42 +++--- .pi/skills/weed/SKILL.md | 2 +- doc/user-story-observations.md | 17 ++- package-lock.json | 14 ++ 9 files changed, 183 insertions(+), 116 deletions(-) diff --git a/.pi/skills/allium/SKILL.md b/.pi/skills/allium/SKILL.md index be29879..434807a 100644 --- a/.pi/skills/allium/SKILL.md +++ b/.pi/skills/allium/SKILL.md @@ -26,14 +26,14 @@ Allium does NOT specify programming language or framework choices, database sche ## Routing table -| Task | Tool | When | -|------|------|------| -| Writing or reading `.allium` files | this skill | You need language syntax and structure | -| Building a spec through conversation | `/skill:elicit` | User describes a feature or behaviour they want to build | -| Extracting a spec from existing code | `/skill:distill` | User has implementation code and wants a spec from it | -| Modifying an existing spec | `/skill:tend` | User wants targeted changes to `.allium` files | -| Checking spec-to-code alignment | `/skill:weed` | User wants to find or fix divergences between spec and implementation | -| Generating tests from a spec | `/skill:propagate` | User wants to generate tests, PBT properties or state machine tests from a specification | +| Task | Tool | When | +| -------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Writing or reading `.allium` files | this skill | You need language syntax and structure | +| Building a spec through conversation | `/skill:elicit` | User describes a feature or behaviour they want to build | +| Extracting a spec from existing code | `/skill:distill` | User has implementation code and wants a spec from it | +| Modifying an existing spec | `/skill:tend` | User wants targeted changes to `.allium` files | +| Checking spec-to-code alignment | `/skill:weed` | User wants to find or fix divergences between spec and implementation | +| Generating tests from a spec | `/skill:propagate` | User wants to generate tests, PBT properties or state machine tests from a specification | | Full Card→Conversation→Confirmation workflow | `/skill:user-story-conversation` | User wants to walk through a user story with Example Mapping, Allium specs, and fast-check properties | ## Quick syntax summary diff --git a/.pi/skills/distill/SKILL.md b/.pi/skills/distill/SKILL.md index 90e069e..edc5152 100644 --- a/.pi/skills/distill/SKILL.md +++ b/.pi/skills/distill/SKILL.md @@ -1,6 +1,6 @@ --- name: distill -description: "Extract an Allium specification from an existing codebase. Use when the user has existing code and wants to distil behaviour into a spec, reverse engineer a specification from implementation, generate a spec from code, turn implementation into a behavioural specification, or document what a codebase does in Allium terms." +description: 'Extract an Allium specification from an existing codebase. Use when the user has existing code and wants to distil behaviour into a spec, reverse engineer a specification from implementation, generate a spec from code, turn implementation into a behavioural specification, or document what a codebase does in Allium terms.' disable-model-invocation: true license: MIT metadata: @@ -12,7 +12,7 @@ metadata: This guide covers extracting Allium specifications from existing codebases. The core challenge is the same as forward elicitation: finding the right level of abstraction. In elicitation you filter out implementation ideas as they arise. In distillation you filter out implementation details that already exist. Both require the same judgement about what matters at the domain level. -Code tells you *how* something works. A specification captures *what* it does and *why* it matters. The skill is asking "why does the stakeholder care about this?" and "could this be different while still being the same system?" +Code tells you _how_ something works. A specification captures _what_ it does and _why_ it matters. The skill is asking "why does the stakeholder care about this?" and "could this be different while still being the same system?" ## Scoping the distillation effort @@ -68,14 +68,14 @@ Distillation and elicitation share the same fundamental challenge: choosing what For every detail in the code, ask: "Why does the stakeholder care about this?" -| Code detail | Why? | Include? | -|-------------|------|----------| -| Invitation expires in 7 days | Affects candidate experience | Yes | -| Token is 32 bytes URL-safe | Security implementation | No | -| Sessions stored in Redis | Performance choice | No | -| Uses PostgreSQL JSONB | Database implementation | No | -| Slot status changes to 'proposed' | Affects what candidate sees | Yes | -| Email sent when invitation accepted | Communication requirement | Yes | +| Code detail | Why? | Include? | +| ----------------------------------- | ---------------------------- | -------- | +| Invitation expires in 7 days | Affects candidate experience | Yes | +| Token is 32 bytes URL-safe | Security implementation | No | +| Sessions stored in Redis | Performance choice | No | +| Uses PostgreSQL JSONB | Database implementation | No | +| Slot status changes to 'proposed' | Affects what candidate sees | Yes | +| Email sent when invitation accepted | Communication requirement | Yes | If you cannot articulate why a stakeholder would care, it is probably implementation. @@ -86,23 +86,23 @@ Ask: "Could this be implemented differently while still being the same system?" - If yes: probably implementation detail, abstract it away - If no: probably domain-level, include it -| Detail | Could be different? | Include? | -|--------|---------------------|----------| -| `secrets.token_urlsafe(32)` | Yes, any secure token generation | No | -| 7-day invitation expiry | No, this is the design decision | Yes | -| PostgreSQL database | Yes, any database | No | -| "Pending, Confirmed, Completed" states | No, this is the workflow | Yes | +| Detail | Could be different? | Include? | +| -------------------------------------- | -------------------------------- | -------- | +| `secrets.token_urlsafe(32)` | Yes, any secure token generation | No | +| 7-day invitation expiry | No, this is the design decision | Yes | +| PostgreSQL database | Yes, any database | No | +| "Pending, Confirmed, Completed" states | No, this is the workflow | Yes | ### The "Template vs Instance" test Is this a **category** of thing, or a **specific instance**? | Instance (often implementation) | Template (often domain-level) | -|--------------------------------|-------------------------------| -| Google OAuth | Authentication provider | -| Slack webhook | Notification channel | -| SendGrid API | Email delivery | -| `timedelta(hours=3)` | Confirmation deadline | +| ------------------------------- | ----------------------------- | +| Google OAuth | Authentication provider | +| Slack webhook | Notification channel | +| SendGrid API | Email delivery | +| `timedelta(hours=3)` | Confirmation deadline | Sometimes the instance IS the domain concern. See "The concrete detail problem" below. @@ -168,6 +168,7 @@ rule SendInvitation { ``` What we dropped: + - `candidate_id: int` became just `candidacy` - `db.session.query(...)` became relationship traversal - `secrets.token_urlsafe(32)` removed entirely (token is implementation) @@ -179,26 +180,26 @@ What we dropped: For every detail in the code, ask: -| Code detail | Product owner cares? | Include? | -|-------------|---------------------|----------| -| Invitation expires in 7 days | Yes, affects candidate experience | Yes | -| Token is 32 bytes URL-safe | No, security implementation | No | -| Uses SQLAlchemy ORM | No, persistence mechanism | No | -| Email template name | Maybe, if templates are design decisions | Maybe | -| Slot status changes to 'proposed' | Yes, affects what candidate sees | Yes | -| Database transaction commits | No, implementation detail | No | +| Code detail | Product owner cares? | Include? | +| --------------------------------- | ---------------------------------------- | -------- | +| Invitation expires in 7 days | Yes, affects candidate experience | Yes | +| Token is 32 bytes URL-safe | No, security implementation | No | +| Uses SQLAlchemy ORM | No, persistence mechanism | No | +| Email template name | Maybe, if templates are design decisions | Maybe | +| Slot status changes to 'proposed' | Yes, affects what candidate sees | Yes | +| Database transaction commits | No, implementation detail | No | ### Distinguish means from ends **Means:** how the code achieves something. **Ends:** what outcome the system needs. -| Means (code) | Ends (spec) | -|--------------|-------------| -| `requests.post('https://slack.com/api/...')` | `Notification.created(channel: slack)` | -| `candidate.oauth_token = google.exchange(code)` | `Candidate authenticated` | -| `redis.setex(f'session:{id}', 86400, data)` | `Session.created(expires: 24.hours)` | -| `for slot in slots: slot.status = 'cancelled'` | `for s in slots: s.status = cancelled` | +| Means (code) | Ends (spec) | +| ----------------------------------------------- | -------------------------------------- | +| `requests.post('https://slack.com/api/...')` | `Notification.created(channel: slack)` | +| `candidate.oauth_token = google.exchange(code)` | `Candidate authenticated` | +| `redis.setex(f'session:{id}', 86400, data)` | `Session.created(expires: 24.hours)` | +| `for slot in slots: slot.status = 'cancelled'` | `for s in slots: s.status = cancelled` | ## The concrete detail problem @@ -207,6 +208,7 @@ The hardest judgement call: when is a concrete detail part of the domain vs just ### Google OAuth example You find this code: + ```python OAUTH_PROVIDERS = { 'google': GoogleOAuthProvider(client_id=..., client_secret=...), @@ -219,12 +221,14 @@ def authenticate(provider: str, code: str) -> User: **Question:** Is "Google OAuth" domain-level or implementation? **It is implementation if:** + - Google is just the auth mechanism chosen - It could be replaced with any OAuth provider - Users do not see or care which provider - The code is written generically (provider is a parameter) **It is domain-level if:** + - Users explicitly choose Google (vs Microsoft, etc.) - "Sign in with Google" is a feature - Google-specific scopes or permissions are used @@ -235,6 +239,7 @@ def authenticate(provider: str, code: str) -> User: ### Database choice example You find PostgreSQL-specific code: + ```python from sqlalchemy.dialects.postgresql import JSONB, ARRAY @@ -244,6 +249,7 @@ class Candidate(Base): ``` **Almost always implementation.** The spec should say: + ``` entity Candidate { skills: Set @@ -256,6 +262,7 @@ The specific database is rarely domain-level. Exception: if the system explicitl ### Third-party integration example You find Greenhouse ATS integration: + ```python class GreenhouseSync: def import_candidate(self, greenhouse_id: str) -> Candidate: @@ -271,11 +278,13 @@ class GreenhouseSync: **Could be either:** **Implementation if:** + - Greenhouse is just where candidates happen to come from - Could be swapped for Lever, Workable, etc. - The integration is an implementation detail of "candidates are imported" Spec: + ``` external entity Candidate { name: String @@ -285,11 +294,13 @@ external entity Candidate { ``` **Product-level if:** + - "Greenhouse integration" is a selling point - Users configure their Greenhouse connection - Greenhouse-specific features are exposed (like syncing feedback back) Spec: + ``` external entity Candidate { name: String @@ -329,6 +340,7 @@ Before extracting any specification, understand the codebase structure: 4. **Note external integrations.** What third parties does it talk to? Create a rough map: + ``` Entry points: - API: /api/candidates/*, /api/interviews/*, /api/invitations/* @@ -355,6 +367,7 @@ class Invitation(Base): ``` Becomes: + ``` entity Invitation { status: pending | accepted | declined | expired @@ -400,6 +413,7 @@ def accept_invitation(invitation_id: int, slot_id: int): ``` Extract: + ``` rule CandidateAcceptsInvitation { when: CandidateAccepts(invitation, slot) @@ -425,15 +439,15 @@ rule CandidateAcceptsInvitation { **Key extraction patterns:** -| Code pattern | Spec pattern | -|--------------|--------------| -| `if x.status != 'pending': raise` | `requires: x.status = pending` | -| `if x.expires_at < now: raise` | `requires: x.expires_at > now` | -| `if item not in collection: raise` | `requires: item in collection` | -| `x.status = 'accepted'` | `ensures: x.status = accepted` | -| `Model.create(...)` | `ensures: Model.created(...)` | -| `send_email(...)` | `ensures: Email.created(...)` | -| `notify(...)` | `ensures: Notification.created(...)` | +| Code pattern | Spec pattern | +| ---------------------------------- | ------------------------------------ | +| `if x.status != 'pending': raise` | `requires: x.status = pending` | +| `if x.expires_at < now: raise` | `requires: x.expires_at > now` | +| `if item not in collection: raise` | `requires: item in collection` | +| `x.status = 'accepted'` | `ensures: x.status = accepted` | +| `Model.create(...)` | `ensures: Model.created(...)` | +| `send_email(...)` | `ensures: Email.created(...)` | +| `notify(...)` | `ensures: Notification.created(...)` | Assertions, checks and validations found in code (e.g. `assert balance >= 0`, class-level validators) may map to expression-bearing invariants rather than rule preconditions. Consider whether they describe a system-wide property or a rule-specific guard. @@ -471,6 +485,7 @@ def send_reminders(): ``` Extract: + ``` rule InvitationExpires { when: invitation: Invitation.expires_at <= now @@ -512,6 +527,7 @@ def import_from_greenhouse(webhook_data): ``` Suggests: + ``` external entity Candidate { name: String @@ -526,6 +542,7 @@ When repeated interface patterns appear across service boundaries (e.g. the same Now make a pass through your extracted spec and remove implementation details. **Before (too concrete):** + ``` entity Invitation { candidate_id: Integer @@ -537,6 +554,7 @@ entity Invitation { ``` **After (domain-level):** + ``` entity Invitation { candidacy: Candidacy @@ -549,6 +567,7 @@ entity Invitation { ``` Changes: + - `candidate_id: Integer` became `candidacy: Candidacy` (relationship, not FK) - `token: String(32)` removed (implementation) - `DateTime` became `Timestamp` (domain type) @@ -565,6 +584,7 @@ The extracted spec is a hypothesis. Validate it: 3. **Look for gaps.** Code often has bugs or missing features; the spec might reveal them. Common findings: + - "Oh, that retry logic was a hack, we should remove it" - "Actually we wanted X but never built it" - "These two code paths should be the same but aren't" @@ -578,6 +598,7 @@ The same principle applies in elicitation. When a stakeholder describes "we use ### Signals in the code **Third-party integration modules:** + ```python # Finding code like this suggests a library spec class StripeWebhookHandler: @@ -594,6 +615,7 @@ class GoogleOAuthProvider: ``` **Generic patterns with specific providers:** + - OAuth flows (Google, Microsoft, GitHub) - Payment processing (Stripe, PayPal) - Email delivery (SendGrid, Postmark, SES) @@ -602,6 +624,7 @@ class GoogleOAuthProvider: - File storage (S3, GCS) **Configuration-driven integrations:** + ```python # Heavy configuration suggests the integration itself is separable OAUTH_CONFIG = { @@ -627,6 +650,7 @@ OAUTH_CONFIG = { **Option 1: Reference an existing library spec** If a standard library spec exists for this integration: + ``` use "github.com/allium-specs/stripe-billing/abc123" as stripe @@ -640,6 +664,7 @@ rule ActivateSubscription { **Option 2: Create a separate library spec** If no standard spec exists but the integration is generic: + ``` -- greenhouse-ats.allium (library spec) -- Specifies: Greenhouse webhook events, candidate sync, etc. @@ -656,6 +681,7 @@ rule ImportCandidate { **Option 3: Abstract and move on** If the integration is minor, just abstract it: + ``` -- Don't specify Slack details, just: ensures: Notification.created( @@ -683,6 +709,7 @@ rule ProcessStripeWebhook { ``` Instead: + ``` -- Application responds to payment events (integration handled elsewhere) rule PaymentReceived { @@ -693,14 +720,14 @@ rule PaymentReceived { ### Common library spec extractions -| Code pattern found | Library spec candidate | -|-------------------|----------------------| -| OAuth token exchange, refresh, session management | `oauth2.allium` | -| Stripe webhook handling, subscription lifecycle | `stripe-billing.allium` | -| Email sending with templates, bounce handling | `email-delivery.allium` | -| Calendar event sync, availability checking | `calendar-integration.allium` | -| ATS candidate import, status sync | `greenhouse-ats.allium`, `lever-ats.allium` | -| File upload, virus scanning, thumbnail generation | `file-storage.allium` | +| Code pattern found | Library spec candidate | +| ------------------------------------------------- | ------------------------------------------- | +| OAuth token exchange, refresh, session management | `oauth2.allium` | +| Stripe webhook handling, subscription lifecycle | `stripe-billing.allium` | +| Email sending with templates, bounce handling | `email-delivery.allium` | +| Calendar event sync, availability checking | `calendar-integration.allium` | +| ATS candidate import, status sync | `greenhouse-ats.allium`, `lever-ats.allium` | +| File upload, virus scanning, thumbnail generation | `file-storage.allium` | See patterns.md Pattern 8 for detailed examples of integrating library specs. @@ -719,11 +746,13 @@ When you find two terms for the same concept (across specs, within a spec, or be This is not a resolution. When different parts of a codebase are built against different specs, both terms end up in the implementation: duplicate models, redundant join tables, foreign keys pointing both ways. **What to do:** + - Choose one term. Cross-reference related specs before deciding. - Update all references. Do not leave the old term in comments or "see also" notes. - Note the rename in a changelog, not in the spec itself. **Warning signs in code:** + - Two models representing the same concept (`Order` and `Purchase`) - Join tables for both (`order_items`, `purchase_items`) - Comments like "equivalent to X" or "same as Y" @@ -745,11 +774,13 @@ class FeedbackRequest: ``` The implicit states are: + - `pending`: requested_at set, feedback_id null, reminded_at null - `reminded`: reminded_at set, feedback_id null - `submitted`: feedback_id set Extract to explicit: + ``` entity FeedbackRequest { interview: Interview @@ -784,6 +815,7 @@ def process_acceptance(invitation, slot): ``` Consolidate into one rule: + ``` rule CandidateAccepts { when: CandidateAccepts(invitation, slot) @@ -800,6 +832,7 @@ rule CandidateAccepts { Codebases accumulate features that were built but never used, workarounds for bugs that are now fixed, and code paths that are never executed. Do not include these in the spec. If you are unsure: + 1. Check if the code is actually reachable 2. Ask developers if it is intentional 3. Check git history for context @@ -817,6 +850,7 @@ def send_notification(user, message): ``` The spec should capture the intended behaviour, not the bug: + ``` ensures: Notification.created(to: user, channel: slack) ``` diff --git a/.pi/skills/elicit/SKILL.md b/.pi/skills/elicit/SKILL.md index 92d8e9b..607ba94 100644 --- a/.pi/skills/elicit/SKILL.md +++ b/.pi/skills/elicit/SKILL.md @@ -1,6 +1,6 @@ --- name: elicit -description: "Run a structured discovery session to build an Allium specification through conversation. Use when the user wants to create a new spec from scratch, elicit or gather requirements, capture domain behaviour, specify a feature or system, define what a system should do, or is describing functionality and needs help shaping it into a specification." +description: 'Run a structured discovery session to build an Allium specification through conversation. Use when the user wants to create a new spec from scratch, elicit or gather requirements, capture domain behaviour, specify a feature or system, define what a system should do, or is describing functionality and needs help shaping it into a specification.' disable-model-invocation: true license: MIT metadata: @@ -53,14 +53,14 @@ The hardest part of specification is choosing what to include and what to leave For every detail, ask: "Why does the stakeholder care about this?" -| Detail | Why? | Include? | -|--------|------|----------| -| "Users log in with Google OAuth" | They need to authenticate | Maybe not, "Users authenticate" might be sufficient | -| "We support Google and Microsoft OAuth" | Users choose their provider | Yes, the choice is domain-level | -| "Sessions expire after 24 hours" | Security/UX decision | Yes, affects user experience | -| "Sessions are stored in Redis" | Performance | No, implementation detail | -| "Passwords must be 12+ characters" | Security policy | Yes, affects users | -| "Passwords are hashed with bcrypt" | Security implementation | No, how not what | +| Detail | Why? | Include? | +| --------------------------------------- | --------------------------- | --------------------------------------------------- | +| "Users log in with Google OAuth" | They need to authenticate | Maybe not, "Users authenticate" might be sufficient | +| "We support Google and Microsoft OAuth" | Users choose their provider | Yes, the choice is domain-level | +| "Sessions expire after 24 hours" | Security/UX decision | Yes, affects user experience | +| "Sessions are stored in Redis" | Performance | No, implementation detail | +| "Passwords must be 12+ characters" | Security policy | Yes, affects users | +| "Passwords are hashed with bcrypt" | Security implementation | No, how not what | ### The "Could it be different?" test @@ -80,12 +80,12 @@ Examples: Is this a category of thing, or a specific instance? -| Instance (implementation) | Template (domain-level) | -|---------------------------|-------------------------| -| Google OAuth | Authentication provider | -| Slack | Notification channel | -| 15 minutes | Link expiry duration (configurable) | -| Greenhouse ATS | External candidate source | +| Instance (implementation) | Template (domain-level) | +| ------------------------- | ----------------------------------- | +| Google OAuth | Authentication provider | +| Slack | Notification channel | +| 15 minutes | Link expiry duration (configurable) | +| Greenhouse ATS | External candidate source | Sometimes the instance IS the domain concern. "We specifically integrate with Salesforce" might be a competitive feature. "We support exactly these three OAuth providers" might be design scope. @@ -257,12 +257,12 @@ This surfaces decisions they have not made yet. When you hear implementation language, redirect: -| They say | You redirect | -|----------|-------------| -| "The API returns a 404" | "So the user is informed it's not found?" | -| "We store it in Postgres" | "What information is captured?" | -| "The frontend shows a modal" | "The user is prompted to confirm?" | -| "We use a cron job" | "This happens on a schedule, how often?" | +| They say | You redirect | +| ---------------------------- | ----------------------------------------- | +| "The API returns a 404" | "So the user is informed it's not found?" | +| "We store it in Postgres" | "What information is captured?" | +| "The frontend shows a modal" | "The user is prompted to confirm?" | +| "We use a cron job" | "This happens on a schedule, how often?" | ### Surface ambiguity explicitly diff --git a/.pi/skills/propagate/SKILL.md b/.pi/skills/propagate/SKILL.md index e09f019..d686315 100644 --- a/.pi/skills/propagate/SKILL.md +++ b/.pi/skills/propagate/SKILL.md @@ -1,6 +1,6 @@ --- name: propagate -description: "Generate tests from Allium specifications. Use when the user wants to propagate tests, generate test files from a spec, write tests for a specification, create property-based tests, produce state machine tests, check test coverage against spec obligations, or understand what tests a specification requires." +description: 'Generate tests from Allium specifications. Use when the user wants to propagate tests, generate test files from a spec, write tests for a specification, create property-based tests, produce state machine tests, check test coverage against spec obligations, or understand what tests a specification requires.' disable-model-invocation: true license: MIT metadata: @@ -76,25 +76,27 @@ For deterministic obligations: field presence, enum membership, transition valid ### 2. Property-based tests For invariants and rule properties. Each expression-bearing invariant becomes a PBT property: + - Generate a valid entity state using the generator spec - Apply a sequence of rules (following the transition graph when declared, or deriving valid sequences from rules alone) - Check the invariant holds at every step Use the project's PBT framework: -| Language | Framework | Discovery | -|----------|-----------|-----------| -| TypeScript | fast-check | `package.json` | -| Python | Hypothesis | `pyproject.toml` | -| Rust | proptest | `Cargo.toml` | -| Go | rapid | `go.mod` | -| Elixir | StreamData | `mix.exs` | +| Language | Framework | Discovery | +| ---------- | ---------- | ---------------- | +| TypeScript | fast-check | `package.json` | +| Python | Hypothesis | `pyproject.toml` | +| Rust | proptest | `Cargo.toml` | +| Go | rapid | `go.mod` | +| Elixir | StreamData | `mix.exs` | Fall back to assertion-based tests if no PBT framework is present. ### 3. State machine tests For entities with status enums. When a transition graph is declared, walk every path through the graph. When no graph is declared, derive valid transitions from rules. + - Verify transitions succeed via witnessing rules - Verify rejected transitions fail - Verify state-dependent fields are present or absent at each state per their `when` clauses @@ -103,6 +105,7 @@ For entities with status enums. When a transition graph is declared, walk every State machine tests require an **action map**: a function per transition edge that takes the entity in the source state and produces it in the target state by calling the actual implementation code. Without this map, the test framework can describe valid paths through the graph but cannot execute them. To build the action map: + 1. For each edge in the transition graph, find the witnessing rule in the spec 2. Find the code implementing that rule (the implementation bridge) 3. Write a test action that sets up the preconditions (`requires` clauses), invokes the code, and returns the entity in the target state @@ -117,6 +120,7 @@ You correlate spec constructs with implementation code, the same way the weed sk ### For surface tests Map surfaces to their implementation: + - API surfaces map to endpoints (REST routes, GraphQL resolvers, gRPC services) - UI surfaces map to components or pages - Integration surfaces map to message handlers or SDK methods @@ -126,6 +130,7 @@ Discover the mapping by reading the codebase. Look for naming patterns, route de ### For internal tests For each rule in the spec: + 1. Find the code implementing the rule (service method, event handler, state machine transition) 2. Determine how to instantiate the entities involved (factories, builders, fixtures) 3. Determine how to invoke the rule (API call, method call, event dispatch) @@ -142,6 +147,7 @@ Before attempting temporal tests, check whether the component accepts an injecte When a rule emits a trigger that another spec's rule receives (e.g. the Arbiter emits `ClerkReceivesEvent`, the Clerk handles it), testing the chain requires multiple components wired together. Before generating cross-module tests: + 1. Trace the trigger emission graph from the plan output: which rules emit triggers, and which rules in other specs receive them 2. Check whether the codebase has an existing integration test fixture that wires the participating components (a pipeline test, an end-to-end test helper, a test harness class) 3. If a fixture exists, reuse it. Cross-module tests should compose existing wiring, not rebuild it diff --git a/.pi/skills/tend/SKILL.md b/.pi/skills/tend/SKILL.md index df0117a..c4e8429 100644 --- a/.pi/skills/tend/SKILL.md +++ b/.pi/skills/tend/SKILL.md @@ -1,6 +1,6 @@ --- name: tend -description: "Tend the Allium garden. Use when the user wants to write, edit, update, add to, improve, clarify, refine, restructure, fix or migrate Allium specs. Covers adding entities, rules, triggers, surfaces and contracts, fixing syntax or validation errors, renaming or refactoring within specs, migrating specs to a new language version, and translating requirements into well-formed specifications. Pushes back on vague requirements." +description: 'Tend the Allium garden. Use when the user wants to write, edit, update, add to, improve, clarify, refine, restructure, fix or migrate Allium specs. Covers adding entities, rules, triggers, surfaces and contracts, fixing syntax or validation errors, renaming or refactoring within specs, migrating specs to a new language version, and translating requirements into well-formed specifications. Pushes back on vague requirements.' disable-model-invocation: true license: MIT metadata: @@ -36,8 +36,8 @@ You take requests for new or changed system behaviour and translate them into we **Find the right abstraction.** Specs describe observable behaviour, not implementation. Two tests help: -- *Why does the stakeholder care?* "Sessions stored in Redis": they don't. "Sessions expire after 24 hours": they do. Include the second, not the first. -- *Could it be implemented differently and still be the same system?* If yes, you're looking at an implementation detail. Abstract it. +- _Why does the stakeholder care?_ "Sessions stored in Redis": they don't. "Sessions expire after 24 hours": they do. Include the second, not the first. +- _Could it be implemented differently and still be the same system?_ If yes, you're looking at an implementation detail. Abstract it. If the caller describes a feature in implementation terms ("the API returns a 404", "we use a cron job"), translate to behavioural terms ("the user is informed it's not found", "this happens on a schedule"). diff --git a/.pi/skills/user-story-conversation/SKILL.md b/.pi/skills/user-story-conversation/SKILL.md index d26dc91..1b00f3c 100644 --- a/.pi/skills/user-story-conversation/SKILL.md +++ b/.pi/skills/user-story-conversation/SKILL.md @@ -57,20 +57,24 @@ If the user already has a user story (e.g., from `user-stories.md`), use that. O Work through the user story using Example Mapping. For each rule, identify: ### Rules (yellow) — Domain concepts + - Nouns and concepts in the domain - What the system manages - Example: "Health", "Damage", "Character", "Level", "Faction" ### Examples (blue) — Concrete scenarios + - Specific instances of rules - Should be testable - Example: "Character with 500 health takes 200 damage → 300 health" ### Questions (pink) — Ambiguities + - Things we don't know or aren't sure about - Example: "What happens when damage exceeds health?" ### Answers (green) — Resolved questions + - Write directly on the pink sticky - Example: "Health becomes 0, character dies" @@ -82,18 +86,22 @@ Create a structured output like this: ## Example Map: [User Story Title] ### Rules + 1. [Rule description] 2. [Rule description] ### Examples + - [Example 1] - [Example 2] ### Questions + - [Question 1] - [Question 2] ### Answers + - [Answer to Question 1] - [Answer to Question 2] ``` @@ -171,13 +179,10 @@ Translate Allium invariants and rules into fast-check properties. import fc from 'fast-check'; // "Health is never negative" -fc.property( - fc.integer({ min: 0, max: 10000 }), - (health) => { - const c = new Character({ health: Health.create(health) }); - return c.health.value >= 0; - } -); +fc.property(fc.integer({ min: 0, max: 10000 }), (health) => { + const c = new Character({ health: Health.create(health) }); + return c.health.value >= 0; +}); ``` #### Rule Properties (State Transitions) @@ -191,7 +196,7 @@ fc.property( const c = new Character({ health: Health.create(health) }); c.takeDamage(Damage.create(damage)); return c.health.value === Math.max(0, health - damage); - } + }, ); ``` @@ -199,14 +204,11 @@ fc.property( ```typescript // "Zero damage changes nothing" -fc.property( - fc.integer({ min: 0, max: 10000 }), - (health) => { - const c = new Character({ health: Health.create(health) }); - c.takeDamage(Damage.create(0)); - return c.health.value === health; - } -); +fc.property(fc.integer({ min: 0, max: 10000 }), (health) => { + const c = new Character({ health: Health.create(health) }); + c.takeDamage(Damage.create(0)); + return c.health.value === health; +}); ``` ### Property Naming @@ -250,7 +252,9 @@ class Health { return level >= 6 ? 1500 : 1000; } - get value(): number { return this.value; } + get value(): number { + return this.value; + } add(amount: number): Health { return Health.create(Math.min(this.value + amount, Health.maxForLevel(this.level.value))); @@ -321,20 +325,24 @@ Here's a complete example walking through "Characters can Deal Damage": ### Example Map **Rules:** + 1. Damage is subtracted from Health 2. When damage exceeds health, health becomes 0 and character dies 3. A Character cannot Deal Damage to itself **Examples:** + - Character with 1000 health takes 200 damage → 800 health - Character with 100 health takes 200 damage → 0 health, dead - Character tries to deal damage to self → no effect **Questions:** + - Can a dead character take damage? - Does damage stack across multiple attacks? **Answers:** + - Dead characters cannot take damage (they're already dead) - Yes, damage stacks (each attack reduces health further) diff --git a/.pi/skills/weed/SKILL.md b/.pi/skills/weed/SKILL.md index 42c9bce..1c855d4 100644 --- a/.pi/skills/weed/SKILL.md +++ b/.pi/skills/weed/SKILL.md @@ -1,6 +1,6 @@ --- name: weed -description: "Weed the Allium garden. Find where Allium specifications and implementation code have diverged, and help resolve the divergences. Use when the user wants to check spec-code alignment, compare specs against implementation, audit for spec drift or violations, sync specs with code or code with specs, or verify whether the implementation matches what the spec says." +description: 'Weed the Allium garden. Find where Allium specifications and implementation code have diverged, and help resolve the divergences. Use when the user wants to check spec-code alignment, compare specs against implementation, audit for spec drift or violations, sync specs with code or code with specs, or verify whether the implementation matches what the spec says.' disable-model-invocation: true license: MIT metadata: diff --git a/doc/user-story-observations.md b/doc/user-story-observations.md index 7135fd0..84e0f16 100644 --- a/doc/user-story-observations.md +++ b/doc/user-story-observations.md @@ -4,37 +4,42 @@ The domain has **four core entities**: -| Entity | Key Properties | -|---|---| -| **Character** | Health (max 1000/1500), Level (1–10), Alive/Dead, Factions (many), damage survived, faction count | -| **Magical Object** | Health, maxHealth, type (Healing or Weapon), destroyed flag | -| **Faction** | String/name, members | -| **Damage/Heal events** | Source, target, amount, level-adjusted | +| Entity | Key Properties | +| ---------------------- | ------------------------------------------------------------------------------------------------- | +| **Character** | Health (max 1000/1500), Level (1–10), Alive/Dead, Factions (many), damage survived, faction count | +| **Magical Object** | Health, maxHealth, type (Healing or Weapon), destroyed flag | +| **Faction** | String/name, members | +| **Damage/Heal events** | Source, target, amount, level-adjusted | --- ## User Story Groups ### 1. Damage & Health (3 rules) + - Characters start at 1000 HP, Alive - Damage subtracts from HP; death at 0 - Self-damage forbidden - Healing: self only, dead can't heal ### 2. Levels (2 rules) + - Base level 1; max HP scales at level 6 (→ 1500) - Level gap ≥ 5: damage ±50% (target higher = -50%, target lower = +50%) ### 3. Factions (3 rules) + - Characters can join/leave multiple factions - Same faction = Allies: no damage between allies, ally-only healing ### 4. Magical Objects (3 rules) + - **Healing objects**: give HP to characters, can't deal damage, can't be healed - **Weapons**: deal fixed damage, lose 1 HP per use, can't heal - Objects are faction-neutral ### 5. Leveling Up (3 rules) + - **Survived damage**: Level 1 → 1000, then +2000 per level (cumulative) - **Faction diversity**: Level 1 → 3 factions, then +3 per level (cumulative) - Max level 10, no level loss diff --git a/package-lock.json b/package-lock.json index c14928f..fadf2bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@types/node": "^25.9.1", "eslint": "^10.4.1", "eslint-config-prettier": "^10.1.8", + "eslint-plugin-no-null": "^1.0.2", "prettier": "^3.8.3", "typescript": "^6.0.3", "typescript-eslint": "^8.60.0", @@ -1192,6 +1193,19 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-plugin-no-null": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-no-null/-/eslint-plugin-no-null-1.0.2.tgz", + "integrity": "sha512-uRDiz88zCO/2rzGfgG15DBjNsgwWtWiSo4Ezy7zzajUgpnFIqd1TjepKeRmJZHEfBGu58o2a8S0D7vglvvhkVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=5.0.0" + }, + "peerDependencies": { + "eslint": ">=3.0.0" + } + }, "node_modules/eslint-scope": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",