skills I believed were committed

This commit is contained in:
Willem van den Ende 2026-06-12 22:08:36 +01:00
parent 8450f90e89
commit e05572bcec
9 changed files with 183 additions and 116 deletions

View File

@ -26,14 +26,14 @@ Allium does NOT specify programming language or framework choices, database sche
## Routing table ## Routing table
| Task | Tool | When | | Task | Tool | When |
|------|------|------| | -------------------------------------------- | -------------------------------- | ----------------------------------------------------------------------------------------------------- |
| Writing or reading `.allium` files | this skill | You need language syntax and structure | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 | | 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 ## Quick syntax summary

View File

@ -1,6 +1,6 @@
--- ---
name: distill 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 disable-model-invocation: true
license: MIT license: MIT
metadata: 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. 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 ## 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?" For every detail in the code, ask: "Why does the stakeholder care about this?"
| Code detail | Why? | Include? | | Code detail | Why? | Include? |
|-------------|------|----------| | ----------------------------------- | ---------------------------- | -------- |
| Invitation expires in 7 days | Affects candidate experience | Yes | | Invitation expires in 7 days | Affects candidate experience | Yes |
| Token is 32 bytes URL-safe | Security implementation | No | | Token is 32 bytes URL-safe | Security implementation | No |
| Sessions stored in Redis | Performance choice | No | | Sessions stored in Redis | Performance choice | No |
| Uses PostgreSQL JSONB | Database implementation | No | | Uses PostgreSQL JSONB | Database implementation | No |
| Slot status changes to 'proposed' | Affects what candidate sees | Yes | | Slot status changes to 'proposed' | Affects what candidate sees | Yes |
| Email sent when invitation accepted | Communication requirement | Yes | | Email sent when invitation accepted | Communication requirement | Yes |
If you cannot articulate why a stakeholder would care, it is probably implementation. 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 yes: probably implementation detail, abstract it away
- If no: probably domain-level, include it - If no: probably domain-level, include it
| Detail | Could be different? | Include? | | Detail | Could be different? | Include? |
|--------|---------------------|----------| | -------------------------------------- | -------------------------------- | -------- |
| `secrets.token_urlsafe(32)` | Yes, any secure token generation | No | | `secrets.token_urlsafe(32)` | Yes, any secure token generation | No |
| 7-day invitation expiry | No, this is the design decision | Yes | | 7-day invitation expiry | No, this is the design decision | Yes |
| PostgreSQL database | Yes, any database | No | | PostgreSQL database | Yes, any database | No |
| "Pending, Confirmed, Completed" states | No, this is the workflow | Yes | | "Pending, Confirmed, Completed" states | No, this is the workflow | Yes |
### The "Template vs Instance" test ### The "Template vs Instance" test
Is this a **category** of thing, or a **specific instance**? Is this a **category** of thing, or a **specific instance**?
| Instance (often implementation) | Template (often domain-level) | | Instance (often implementation) | Template (often domain-level) |
|--------------------------------|-------------------------------| | ------------------------------- | ----------------------------- |
| Google OAuth | Authentication provider | | Google OAuth | Authentication provider |
| Slack webhook | Notification channel | | Slack webhook | Notification channel |
| SendGrid API | Email delivery | | SendGrid API | Email delivery |
| `timedelta(hours=3)` | Confirmation deadline | | `timedelta(hours=3)` | Confirmation deadline |
Sometimes the instance IS the domain concern. See "The concrete detail problem" below. Sometimes the instance IS the domain concern. See "The concrete detail problem" below.
@ -168,6 +168,7 @@ rule SendInvitation {
``` ```
What we dropped: What we dropped:
- `candidate_id: int` became just `candidacy` - `candidate_id: int` became just `candidacy`
- `db.session.query(...)` became relationship traversal - `db.session.query(...)` became relationship traversal
- `secrets.token_urlsafe(32)` removed entirely (token is implementation) - `secrets.token_urlsafe(32)` removed entirely (token is implementation)
@ -179,26 +180,26 @@ What we dropped:
For every detail in the code, ask: For every detail in the code, ask:
| Code detail | Product owner cares? | Include? | | Code detail | Product owner cares? | Include? |
|-------------|---------------------|----------| | --------------------------------- | ---------------------------------------- | -------- |
| Invitation expires in 7 days | Yes, affects candidate experience | Yes | | Invitation expires in 7 days | Yes, affects candidate experience | Yes |
| Token is 32 bytes URL-safe | No, security implementation | No | | Token is 32 bytes URL-safe | No, security implementation | No |
| Uses SQLAlchemy ORM | No, persistence mechanism | No | | Uses SQLAlchemy ORM | No, persistence mechanism | No |
| Email template name | Maybe, if templates are design decisions | Maybe | | Email template name | Maybe, if templates are design decisions | Maybe |
| Slot status changes to 'proposed' | Yes, affects what candidate sees | Yes | | Slot status changes to 'proposed' | Yes, affects what candidate sees | Yes |
| Database transaction commits | No, implementation detail | No | | Database transaction commits | No, implementation detail | No |
### Distinguish means from ends ### Distinguish means from ends
**Means:** how the code achieves something. **Means:** how the code achieves something.
**Ends:** what outcome the system needs. **Ends:** what outcome the system needs.
| Means (code) | Ends (spec) | | Means (code) | Ends (spec) |
|--------------|-------------| | ----------------------------------------------- | -------------------------------------- |
| `requests.post('https://slack.com/api/...')` | `Notification.created(channel: slack)` | | `requests.post('https://slack.com/api/...')` | `Notification.created(channel: slack)` |
| `candidate.oauth_token = google.exchange(code)` | `Candidate authenticated` | | `candidate.oauth_token = google.exchange(code)` | `Candidate authenticated` |
| `redis.setex(f'session:{id}', 86400, data)` | `Session.created(expires: 24.hours)` | | `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` | | `for slot in slots: slot.status = 'cancelled'` | `for s in slots: s.status = cancelled` |
## The concrete detail problem ## 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 ### Google OAuth example
You find this code: You find this code:
```python ```python
OAUTH_PROVIDERS = { OAUTH_PROVIDERS = {
'google': GoogleOAuthProvider(client_id=..., client_secret=...), '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? **Question:** Is "Google OAuth" domain-level or implementation?
**It is implementation if:** **It is implementation if:**
- Google is just the auth mechanism chosen - Google is just the auth mechanism chosen
- It could be replaced with any OAuth provider - It could be replaced with any OAuth provider
- Users do not see or care which provider - Users do not see or care which provider
- The code is written generically (provider is a parameter) - The code is written generically (provider is a parameter)
**It is domain-level if:** **It is domain-level if:**
- Users explicitly choose Google (vs Microsoft, etc.) - Users explicitly choose Google (vs Microsoft, etc.)
- "Sign in with Google" is a feature - "Sign in with Google" is a feature
- Google-specific scopes or permissions are used - Google-specific scopes or permissions are used
@ -235,6 +239,7 @@ def authenticate(provider: str, code: str) -> User:
### Database choice example ### Database choice example
You find PostgreSQL-specific code: You find PostgreSQL-specific code:
```python ```python
from sqlalchemy.dialects.postgresql import JSONB, ARRAY from sqlalchemy.dialects.postgresql import JSONB, ARRAY
@ -244,6 +249,7 @@ class Candidate(Base):
``` ```
**Almost always implementation.** The spec should say: **Almost always implementation.** The spec should say:
``` ```
entity Candidate { entity Candidate {
skills: Set<String> skills: Set<String>
@ -256,6 +262,7 @@ The specific database is rarely domain-level. Exception: if the system explicitl
### Third-party integration example ### Third-party integration example
You find Greenhouse ATS integration: You find Greenhouse ATS integration:
```python ```python
class GreenhouseSync: class GreenhouseSync:
def import_candidate(self, greenhouse_id: str) -> Candidate: def import_candidate(self, greenhouse_id: str) -> Candidate:
@ -271,11 +278,13 @@ class GreenhouseSync:
**Could be either:** **Could be either:**
**Implementation if:** **Implementation if:**
- Greenhouse is just where candidates happen to come from - Greenhouse is just where candidates happen to come from
- Could be swapped for Lever, Workable, etc. - Could be swapped for Lever, Workable, etc.
- The integration is an implementation detail of "candidates are imported" - The integration is an implementation detail of "candidates are imported"
Spec: Spec:
``` ```
external entity Candidate { external entity Candidate {
name: String name: String
@ -285,11 +294,13 @@ external entity Candidate {
``` ```
**Product-level if:** **Product-level if:**
- "Greenhouse integration" is a selling point - "Greenhouse integration" is a selling point
- Users configure their Greenhouse connection - Users configure their Greenhouse connection
- Greenhouse-specific features are exposed (like syncing feedback back) - Greenhouse-specific features are exposed (like syncing feedback back)
Spec: Spec:
``` ```
external entity Candidate { external entity Candidate {
name: String 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? 4. **Note external integrations.** What third parties does it talk to?
Create a rough map: Create a rough map:
``` ```
Entry points: Entry points:
- API: /api/candidates/*, /api/interviews/*, /api/invitations/* - API: /api/candidates/*, /api/interviews/*, /api/invitations/*
@ -355,6 +367,7 @@ class Invitation(Base):
``` ```
Becomes: Becomes:
``` ```
entity Invitation { entity Invitation {
status: pending | accepted | declined | expired status: pending | accepted | declined | expired
@ -400,6 +413,7 @@ def accept_invitation(invitation_id: int, slot_id: int):
``` ```
Extract: Extract:
``` ```
rule CandidateAcceptsInvitation { rule CandidateAcceptsInvitation {
when: CandidateAccepts(invitation, slot) when: CandidateAccepts(invitation, slot)
@ -425,15 +439,15 @@ rule CandidateAcceptsInvitation {
**Key extraction patterns:** **Key extraction patterns:**
| Code pattern | Spec pattern | | Code pattern | Spec pattern |
|--------------|--------------| | ---------------------------------- | ------------------------------------ |
| `if x.status != 'pending': raise` | `requires: x.status = pending` | | `if x.status != 'pending': raise` | `requires: x.status = pending` |
| `if x.expires_at < now: raise` | `requires: x.expires_at > now` | | `if x.expires_at < now: raise` | `requires: x.expires_at > now` |
| `if item not in collection: raise` | `requires: item in collection` | | `if item not in collection: raise` | `requires: item in collection` |
| `x.status = 'accepted'` | `ensures: x.status = accepted` | | `x.status = 'accepted'` | `ensures: x.status = accepted` |
| `Model.create(...)` | `ensures: Model.created(...)` | | `Model.create(...)` | `ensures: Model.created(...)` |
| `send_email(...)` | `ensures: Email.created(...)` | | `send_email(...)` | `ensures: Email.created(...)` |
| `notify(...)` | `ensures: Notification.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. 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: Extract:
``` ```
rule InvitationExpires { rule InvitationExpires {
when: invitation: Invitation.expires_at <= now when: invitation: Invitation.expires_at <= now
@ -512,6 +527,7 @@ def import_from_greenhouse(webhook_data):
``` ```
Suggests: Suggests:
``` ```
external entity Candidate { external entity Candidate {
name: String 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. Now make a pass through your extracted spec and remove implementation details.
**Before (too concrete):** **Before (too concrete):**
``` ```
entity Invitation { entity Invitation {
candidate_id: Integer candidate_id: Integer
@ -537,6 +554,7 @@ entity Invitation {
``` ```
**After (domain-level):** **After (domain-level):**
``` ```
entity Invitation { entity Invitation {
candidacy: Candidacy candidacy: Candidacy
@ -549,6 +567,7 @@ entity Invitation {
``` ```
Changes: Changes:
- `candidate_id: Integer` became `candidacy: Candidacy` (relationship, not FK) - `candidate_id: Integer` became `candidacy: Candidacy` (relationship, not FK)
- `token: String(32)` removed (implementation) - `token: String(32)` removed (implementation)
- `DateTime` became `Timestamp` (domain type) - `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. 3. **Look for gaps.** Code often has bugs or missing features; the spec might reveal them.
Common findings: Common findings:
- "Oh, that retry logic was a hack, we should remove it" - "Oh, that retry logic was a hack, we should remove it"
- "Actually we wanted X but never built it" - "Actually we wanted X but never built it"
- "These two code paths should be the same but aren't" - "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 ### Signals in the code
**Third-party integration modules:** **Third-party integration modules:**
```python ```python
# Finding code like this suggests a library spec # Finding code like this suggests a library spec
class StripeWebhookHandler: class StripeWebhookHandler:
@ -594,6 +615,7 @@ class GoogleOAuthProvider:
``` ```
**Generic patterns with specific providers:** **Generic patterns with specific providers:**
- OAuth flows (Google, Microsoft, GitHub) - OAuth flows (Google, Microsoft, GitHub)
- Payment processing (Stripe, PayPal) - Payment processing (Stripe, PayPal)
- Email delivery (SendGrid, Postmark, SES) - Email delivery (SendGrid, Postmark, SES)
@ -602,6 +624,7 @@ class GoogleOAuthProvider:
- File storage (S3, GCS) - File storage (S3, GCS)
**Configuration-driven integrations:** **Configuration-driven integrations:**
```python ```python
# Heavy configuration suggests the integration itself is separable # Heavy configuration suggests the integration itself is separable
OAUTH_CONFIG = { OAUTH_CONFIG = {
@ -627,6 +650,7 @@ OAUTH_CONFIG = {
**Option 1: Reference an existing library spec** **Option 1: Reference an existing library spec**
If a standard library spec exists for this integration: If a standard library spec exists for this integration:
``` ```
use "github.com/allium-specs/stripe-billing/abc123" as stripe use "github.com/allium-specs/stripe-billing/abc123" as stripe
@ -640,6 +664,7 @@ rule ActivateSubscription {
**Option 2: Create a separate library spec** **Option 2: Create a separate library spec**
If no standard spec exists but the integration is generic: If no standard spec exists but the integration is generic:
``` ```
-- greenhouse-ats.allium (library spec) -- greenhouse-ats.allium (library spec)
-- Specifies: Greenhouse webhook events, candidate sync, etc. -- Specifies: Greenhouse webhook events, candidate sync, etc.
@ -656,6 +681,7 @@ rule ImportCandidate {
**Option 3: Abstract and move on** **Option 3: Abstract and move on**
If the integration is minor, just abstract it: If the integration is minor, just abstract it:
``` ```
-- Don't specify Slack details, just: -- Don't specify Slack details, just:
ensures: Notification.created( ensures: Notification.created(
@ -683,6 +709,7 @@ rule ProcessStripeWebhook {
``` ```
Instead: Instead:
``` ```
-- Application responds to payment events (integration handled elsewhere) -- Application responds to payment events (integration handled elsewhere)
rule PaymentReceived { rule PaymentReceived {
@ -693,14 +720,14 @@ rule PaymentReceived {
### Common library spec extractions ### Common library spec extractions
| Code pattern found | Library spec candidate | | Code pattern found | Library spec candidate |
|-------------------|----------------------| | ------------------------------------------------- | ------------------------------------------- |
| OAuth token exchange, refresh, session management | `oauth2.allium` | | OAuth token exchange, refresh, session management | `oauth2.allium` |
| Stripe webhook handling, subscription lifecycle | `stripe-billing.allium` | | Stripe webhook handling, subscription lifecycle | `stripe-billing.allium` |
| Email sending with templates, bounce handling | `email-delivery.allium` | | Email sending with templates, bounce handling | `email-delivery.allium` |
| Calendar event sync, availability checking | `calendar-integration.allium` | | Calendar event sync, availability checking | `calendar-integration.allium` |
| ATS candidate import, status sync | `greenhouse-ats.allium`, `lever-ats.allium` | | ATS candidate import, status sync | `greenhouse-ats.allium`, `lever-ats.allium` |
| File upload, virus scanning, thumbnail generation | `file-storage.allium` | | File upload, virus scanning, thumbnail generation | `file-storage.allium` |
See patterns.md Pattern 8 for detailed examples of integrating library specs. 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. 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:** **What to do:**
- Choose one term. Cross-reference related specs before deciding. - Choose one term. Cross-reference related specs before deciding.
- Update all references. Do not leave the old term in comments or "see also" notes. - 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. - Note the rename in a changelog, not in the spec itself.
**Warning signs in code:** **Warning signs in code:**
- Two models representing the same concept (`Order` and `Purchase`) - Two models representing the same concept (`Order` and `Purchase`)
- Join tables for both (`order_items`, `purchase_items`) - Join tables for both (`order_items`, `purchase_items`)
- Comments like "equivalent to X" or "same as Y" - Comments like "equivalent to X" or "same as Y"
@ -745,11 +774,13 @@ class FeedbackRequest:
``` ```
The implicit states are: The implicit states are:
- `pending`: requested_at set, feedback_id null, reminded_at null - `pending`: requested_at set, feedback_id null, reminded_at null
- `reminded`: reminded_at set, feedback_id null - `reminded`: reminded_at set, feedback_id null
- `submitted`: feedback_id set - `submitted`: feedback_id set
Extract to explicit: Extract to explicit:
``` ```
entity FeedbackRequest { entity FeedbackRequest {
interview: Interview interview: Interview
@ -784,6 +815,7 @@ def process_acceptance(invitation, slot):
``` ```
Consolidate into one rule: Consolidate into one rule:
``` ```
rule CandidateAccepts { rule CandidateAccepts {
when: CandidateAccepts(invitation, slot) 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. 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: Do not include these in the spec. If you are unsure:
1. Check if the code is actually reachable 1. Check if the code is actually reachable
2. Ask developers if it is intentional 2. Ask developers if it is intentional
3. Check git history for context 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: The spec should capture the intended behaviour, not the bug:
``` ```
ensures: Notification.created(to: user, channel: slack) ensures: Notification.created(to: user, channel: slack)
``` ```

View File

@ -1,6 +1,6 @@
--- ---
name: elicit 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 disable-model-invocation: true
license: MIT license: MIT
metadata: 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?" For every detail, ask: "Why does the stakeholder care about this?"
| Detail | Why? | Include? | | Detail | Why? | Include? |
|--------|------|----------| | --------------------------------------- | --------------------------- | --------------------------------------------------- |
| "Users log in with Google OAuth" | They need to authenticate | Maybe not, "Users authenticate" might be sufficient | | "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 | | "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 expire after 24 hours" | Security/UX decision | Yes, affects user experience |
| "Sessions are stored in Redis" | Performance | No, implementation detail | | "Sessions are stored in Redis" | Performance | No, implementation detail |
| "Passwords must be 12+ characters" | Security policy | Yes, affects users | | "Passwords must be 12+ characters" | Security policy | Yes, affects users |
| "Passwords are hashed with bcrypt" | Security implementation | No, how not what | | "Passwords are hashed with bcrypt" | Security implementation | No, how not what |
### The "Could it be different?" test ### The "Could it be different?" test
@ -80,12 +80,12 @@ Examples:
Is this a category of thing, or a specific instance? Is this a category of thing, or a specific instance?
| Instance (implementation) | Template (domain-level) | | Instance (implementation) | Template (domain-level) |
|---------------------------|-------------------------| | ------------------------- | ----------------------------------- |
| Google OAuth | Authentication provider | | Google OAuth | Authentication provider |
| Slack | Notification channel | | Slack | Notification channel |
| 15 minutes | Link expiry duration (configurable) | | 15 minutes | Link expiry duration (configurable) |
| Greenhouse ATS | External candidate source | | 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. 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: When you hear implementation language, redirect:
| They say | You redirect | | They say | You redirect |
|----------|-------------| | ---------------------------- | ----------------------------------------- |
| "The API returns a 404" | "So the user is informed it's not found?" | | "The API returns a 404" | "So the user is informed it's not found?" |
| "We store it in Postgres" | "What information is captured?" | | "We store it in Postgres" | "What information is captured?" |
| "The frontend shows a modal" | "The user is prompted to confirm?" | | "The frontend shows a modal" | "The user is prompted to confirm?" |
| "We use a cron job" | "This happens on a schedule, how often?" | | "We use a cron job" | "This happens on a schedule, how often?" |
### Surface ambiguity explicitly ### Surface ambiguity explicitly

View File

@ -1,6 +1,6 @@
--- ---
name: propagate 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 disable-model-invocation: true
license: MIT license: MIT
metadata: metadata:
@ -76,25 +76,27 @@ For deterministic obligations: field presence, enum membership, transition valid
### 2. Property-based tests ### 2. Property-based tests
For invariants and rule properties. Each expression-bearing invariant becomes a PBT property: For invariants and rule properties. Each expression-bearing invariant becomes a PBT property:
- Generate a valid entity state using the generator spec - 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) - 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 - Check the invariant holds at every step
Use the project's PBT framework: Use the project's PBT framework:
| Language | Framework | Discovery | | Language | Framework | Discovery |
|----------|-----------|-----------| | ---------- | ---------- | ---------------- |
| TypeScript | fast-check | `package.json` | | TypeScript | fast-check | `package.json` |
| Python | Hypothesis | `pyproject.toml` | | Python | Hypothesis | `pyproject.toml` |
| Rust | proptest | `Cargo.toml` | | Rust | proptest | `Cargo.toml` |
| Go | rapid | `go.mod` | | Go | rapid | `go.mod` |
| Elixir | StreamData | `mix.exs` | | Elixir | StreamData | `mix.exs` |
Fall back to assertion-based tests if no PBT framework is present. Fall back to assertion-based tests if no PBT framework is present.
### 3. State machine tests ### 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. 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 transitions succeed via witnessing rules
- Verify rejected transitions fail - Verify rejected transitions fail
- Verify state-dependent fields are present or absent at each state per their `when` clauses - 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. 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: To build the action map:
1. For each edge in the transition graph, find the witnessing rule in the spec 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) 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 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 ### For surface tests
Map surfaces to their implementation: Map surfaces to their implementation:
- API surfaces map to endpoints (REST routes, GraphQL resolvers, gRPC services) - API surfaces map to endpoints (REST routes, GraphQL resolvers, gRPC services)
- UI surfaces map to components or pages - UI surfaces map to components or pages
- Integration surfaces map to message handlers or SDK methods - 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 internal tests
For each rule in the spec: For each rule in the spec:
1. Find the code implementing the rule (service method, event handler, state machine transition) 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) 2. Determine how to instantiate the entities involved (factories, builders, fixtures)
3. Determine how to invoke the rule (API call, method call, event dispatch) 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. 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: 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 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) 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 3. If a fixture exists, reuse it. Cross-module tests should compose existing wiring, not rebuild it

View File

@ -1,6 +1,6 @@
--- ---
name: tend 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 disable-model-invocation: true
license: MIT license: MIT
metadata: 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: **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. - _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. - _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"). 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").

View File

@ -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: Work through the user story using Example Mapping. For each rule, identify:
### Rules (yellow) — Domain concepts ### Rules (yellow) — Domain concepts
- Nouns and concepts in the domain - Nouns and concepts in the domain
- What the system manages - What the system manages
- Example: "Health", "Damage", "Character", "Level", "Faction" - Example: "Health", "Damage", "Character", "Level", "Faction"
### Examples (blue) — Concrete scenarios ### Examples (blue) — Concrete scenarios
- Specific instances of rules - Specific instances of rules
- Should be testable - Should be testable
- Example: "Character with 500 health takes 200 damage → 300 health" - Example: "Character with 500 health takes 200 damage → 300 health"
### Questions (pink) — Ambiguities ### Questions (pink) — Ambiguities
- Things we don't know or aren't sure about - Things we don't know or aren't sure about
- Example: "What happens when damage exceeds health?" - Example: "What happens when damage exceeds health?"
### Answers (green) — Resolved questions ### Answers (green) — Resolved questions
- Write directly on the pink sticky - Write directly on the pink sticky
- Example: "Health becomes 0, character dies" - Example: "Health becomes 0, character dies"
@ -82,18 +86,22 @@ Create a structured output like this:
## Example Map: [User Story Title] ## Example Map: [User Story Title]
### Rules ### Rules
1. [Rule description] 1. [Rule description]
2. [Rule description] 2. [Rule description]
### Examples ### Examples
- [Example 1] - [Example 1]
- [Example 2] - [Example 2]
### Questions ### Questions
- [Question 1] - [Question 1]
- [Question 2] - [Question 2]
### Answers ### Answers
- [Answer to Question 1] - [Answer to Question 1]
- [Answer to Question 2] - [Answer to Question 2]
``` ```
@ -171,13 +179,10 @@ Translate Allium invariants and rules into fast-check properties.
import fc from 'fast-check'; import fc from 'fast-check';
// "Health is never negative" // "Health is never negative"
fc.property( fc.property(fc.integer({ min: 0, max: 10000 }), (health) => {
fc.integer({ min: 0, max: 10000 }), const c = new Character({ health: Health.create(health) });
(health) => { return c.health.value >= 0;
const c = new Character({ health: Health.create(health) }); });
return c.health.value >= 0;
}
);
``` ```
#### Rule Properties (State Transitions) #### Rule Properties (State Transitions)
@ -191,7 +196,7 @@ fc.property(
const c = new Character({ health: Health.create(health) }); const c = new Character({ health: Health.create(health) });
c.takeDamage(Damage.create(damage)); c.takeDamage(Damage.create(damage));
return c.health.value === Math.max(0, health - damage); return c.health.value === Math.max(0, health - damage);
} },
); );
``` ```
@ -199,14 +204,11 @@ fc.property(
```typescript ```typescript
// "Zero damage changes nothing" // "Zero damage changes nothing"
fc.property( fc.property(fc.integer({ min: 0, max: 10000 }), (health) => {
fc.integer({ min: 0, max: 10000 }), const c = new Character({ health: Health.create(health) });
(health) => { c.takeDamage(Damage.create(0));
const c = new Character({ health: Health.create(health) }); return c.health.value === health;
c.takeDamage(Damage.create(0)); });
return c.health.value === health;
}
);
``` ```
### Property Naming ### Property Naming
@ -250,7 +252,9 @@ class Health {
return level >= 6 ? 1500 : 1000; return level >= 6 ? 1500 : 1000;
} }
get value(): number { return this.value; } get value(): number {
return this.value;
}
add(amount: number): Health { add(amount: number): Health {
return Health.create(Math.min(this.value + amount, Health.maxForLevel(this.level.value))); 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 ### Example Map
**Rules:** **Rules:**
1. Damage is subtracted from Health 1. Damage is subtracted from Health
2. When damage exceeds health, health becomes 0 and character dies 2. When damage exceeds health, health becomes 0 and character dies
3. A Character cannot Deal Damage to itself 3. A Character cannot Deal Damage to itself
**Examples:** **Examples:**
- Character with 1000 health takes 200 damage → 800 health - Character with 1000 health takes 200 damage → 800 health
- Character with 100 health takes 200 damage → 0 health, dead - Character with 100 health takes 200 damage → 0 health, dead
- Character tries to deal damage to self → no effect - Character tries to deal damage to self → no effect
**Questions:** **Questions:**
- Can a dead character take damage? - Can a dead character take damage?
- Does damage stack across multiple attacks? - Does damage stack across multiple attacks?
**Answers:** **Answers:**
- Dead characters cannot take damage (they're already dead) - Dead characters cannot take damage (they're already dead)
- Yes, damage stacks (each attack reduces health further) - Yes, damage stacks (each attack reduces health further)

View File

@ -1,6 +1,6 @@
--- ---
name: weed 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 disable-model-invocation: true
license: MIT license: MIT
metadata: metadata:

View File

@ -4,37 +4,42 @@
The domain has **four core entities**: The domain has **four core entities**:
| Entity | Key Properties | | Entity | Key Properties |
|---|---| | ---------------------- | ------------------------------------------------------------------------------------------------- |
| **Character** | Health (max 1000/1500), Level (110), Alive/Dead, Factions (many), damage survived, faction count | | **Character** | Health (max 1000/1500), Level (110), Alive/Dead, Factions (many), damage survived, faction count |
| **Magical Object** | Health, maxHealth, type (Healing or Weapon), destroyed flag | | **Magical Object** | Health, maxHealth, type (Healing or Weapon), destroyed flag |
| **Faction** | String/name, members | | **Faction** | String/name, members |
| **Damage/Heal events** | Source, target, amount, level-adjusted | | **Damage/Heal events** | Source, target, amount, level-adjusted |
--- ---
## User Story Groups ## User Story Groups
### 1. Damage & Health (3 rules) ### 1. Damage & Health (3 rules)
- Characters start at 1000 HP, Alive - Characters start at 1000 HP, Alive
- Damage subtracts from HP; death at 0 - Damage subtracts from HP; death at 0
- Self-damage forbidden - Self-damage forbidden
- Healing: self only, dead can't heal - Healing: self only, dead can't heal
### 2. Levels (2 rules) ### 2. Levels (2 rules)
- Base level 1; max HP scales at level 6 (→ 1500) - Base level 1; max HP scales at level 6 (→ 1500)
- Level gap ≥ 5: damage ±50% (target higher = -50%, target lower = +50%) - Level gap ≥ 5: damage ±50% (target higher = -50%, target lower = +50%)
### 3. Factions (3 rules) ### 3. Factions (3 rules)
- Characters can join/leave multiple factions - Characters can join/leave multiple factions
- Same faction = Allies: no damage between allies, ally-only healing - Same faction = Allies: no damage between allies, ally-only healing
### 4. Magical Objects (3 rules) ### 4. Magical Objects (3 rules)
- **Healing objects**: give HP to characters, can't deal damage, can't be healed - **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 - **Weapons**: deal fixed damage, lose 1 HP per use, can't heal
- Objects are faction-neutral - Objects are faction-neutral
### 5. Leveling Up (3 rules) ### 5. Leveling Up (3 rules)
- **Survived damage**: Level 1 → 1000, then +2000 per level (cumulative) - **Survived damage**: Level 1 → 1000, then +2000 per level (cumulative)
- **Faction diversity**: Level 1 → 3 factions, then +3 per level (cumulative) - **Faction diversity**: Level 1 → 3 factions, then +3 per level (cumulative)
- Max level 10, no level loss - Max level 10, no level loss

14
package-lock.json generated
View File

@ -16,6 +16,7 @@
"@types/node": "^25.9.1", "@types/node": "^25.9.1",
"eslint": "^10.4.1", "eslint": "^10.4.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-no-null": "^1.0.2",
"prettier": "^3.8.3", "prettier": "^3.8.3",
"typescript": "^6.0.3", "typescript": "^6.0.3",
"typescript-eslint": "^8.60.0", "typescript-eslint": "^8.60.0",
@ -1192,6 +1193,19 @@
"eslint": ">=7.0.0" "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": { "node_modules/eslint-scope": {
"version": "9.1.2", "version": "9.1.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz",