From dbb306121420de769ef3ad248c9e61e17f81537f Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 13:46:03 -0400 Subject: [PATCH 01/27] =?UTF-8?q?feat(discovery):=20complete=20session=201?= =?UTF-8?q?=20=E2=80=94=20CLI=20entrypoint=20chosen=20as=20demonstration?= =?UTF-8?q?=20feature?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- WORK.md | 14 ++- docs/discovery.md | 30 ++--- docs/glossary.md | 282 ++++++++++++++++++++++++++++++++++++++++-- docs/scope_journal.md | 15 +-- 4 files changed, 298 insertions(+), 43 deletions(-) diff --git a/WORK.md b/WORK.md index 8a1cfbe..b0780e6 100644 --- a/WORK.md +++ b/WORK.md @@ -19,7 +19,15 @@ Each item carries exactly the variables defined by `FLOW.md`: ## Session Log - +### 2026-04-22 — Session 1 (Discovery) + +- Resumed interrupted Stage 1 discovery session (Q1–Q8 were pre-captured). +- Completed Block B cross-cutting questions (Q9–Q11): confirmed scope is one demonstration feature. +- Completed Block C feature discovery: stakeholder chose CLI entrypoint (`--help` + `--version`) as the demonstration feature. +- Created `docs/features/backlog/cli-entrypoint.feature` — Status: BASELINED (2026-04-22). +- Created `docs/features/in-progress/` and `docs/features/completed/` directories. +- Created `docs/glossary.md`, `docs/discovery.md`. +- Marked `docs/scope_journal.md` Session 1 as COMPLETE. + +**Next:** Run `@system-architect` — begin Step 2 (ARCH) for `cli-entrypoint`. PO must first move `docs/features/backlog/cli-entrypoint.feature` → `docs/features/in-progress/cli-entrypoint.feature`. -2026-04-22 00:00 | @system-architect | [none] | [IDLE] | Read all 14 backlog features; wrote docs/domain-model.md, docs/system.md, docs/adr/ (5 ADRs); recommended config-reading as first feature; awaiting PO to move feature to in-progress/ and confirm branch creation -2026-04-22 00:01 | @system-architect | [none] | [IDLE] | Correction: docs/domain-model.md, docs/context.md, and docs/container.md are not separate files — Context and Container diagrams and the Domain Model are all sections inside docs/system.md (SA-owned). docs/domain-model.md was never created; docs/context.md deleted and consolidated into system.md. diff --git a/docs/discovery.md b/docs/discovery.md index 5a4ea04..b4a3b22 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -1,24 +1,18 @@ # Discovery: temple8 ---- - -## Session: 2026-04-22 - -### Context +> Append-only session synthesis log. +> Written by the product-owner at the end of each discovery session. +> Each block records one session: a summary paragraph and a table of features whose behavior changed. +> A row appears only when a `.feature` file would be updated as a result of the session. +> Confirmations of existing behavior are not recorded here — see `docs/scope_journal.md` for the full Q&A. +> Never edit past blocks — later blocks extend or supersede earlier ones. -temple8 is a Python project template used by Python engineers who want to start a new project with production-ready tooling already in place. The product eliminates setup boilerplate — quality tooling, CI, test infrastructure, and an AI-assisted delivery workflow are all preconfigured. It exists because the cost of setting up a rigorous environment from scratch discourages engineers from applying good practices from day one. Success means an engineer can clone the template and ship a meaningful first feature within a single session. Failure means the template introduces more friction than it removes, or that it locks engineers into choices they cannot override. - -Out of scope: runtime infrastructure (databases, message queues, cloud deployment), UI frameworks, and any domain-specific business logic. - -### Feature List +--- -- `display-version` — The application reads its own version from `pyproject.toml` at runtime and logs it; log output is gated by a verbosity parameter. +## Session 2026-04-22 -### Domain Model +**Summary**: First discovery session for temple8. Established that the product serves Python engineers who want a production-ready project skeleton without setup cost. Confirmed the template ships with exactly one demonstration feature — a CLI entrypoint (`python -m app --help` and `python -m app --version`) implemented entirely in `app/__main__.py` using stdlib only. The feature was chosen for its genuine utility, minimal footprint (zero new dependencies, ~15 lines), and its ability to showcase the full five-step delivery workflow end-to-end. -| Type | Name | Description | In Scope | -|------|------|-------------|----------| -| Noun | `Version` | Semver string read from `pyproject.toml` via `tomllib` | Yes | -| Noun | `ValidVerbosity` | Closed set of five standard Python log level names | Yes | -| Verb | `version()` | Reads version and emits INFO log | Yes | -| Verb | `main(verbosity)` | Configures logging and calls `version()` | Yes | +| Feature | Change | Source questions | Reason | +|---------|--------|-----------------|--------| +| `cli-entrypoint` | created | Q8: "ship with one working demo feature" → one end-to-end example; Q9: "simple useful command, no bloat" → single CLI command; Q11: Option C chosen → `--help` + `--version` combined | New feature: CLI entrypoint with `--help` (prints name, tagline, options, exits 0), `--version` (prints `temple8 ` from package metadata, exits 0), and unknown-flag handling (exits 2). All code in `app/__main__.py`, zero new dependencies. | diff --git a/docs/glossary.md b/docs/glossary.md index 8e4a4bb..97b71a6 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -1,22 +1,278 @@ -# Glossary — temple8 +# Glossary: temple8 -Living glossary of domain terms. PO updates after each discovery session. -SA reads before Step 2. SE reads before Step 3. +> Living glossary of domain terms used in this project. +> Written and maintained by the product-owner during Step 1 discovery. +> Append-only: never edit or remove past entries. If a term changes, mark it superseded and write a new entry. +> Code and tests take precedence over this glossary — if they diverge, refactor the code, not this file. -All terms are reconciled against `docs/system.md` (Domain Model section) and `docs/discovery.md`. +--- + +## Entry Format + +``` +## + +**Definition:** + +**Aliases:** + +**Example:** + +**Source:** +``` + +Entries are sorted alphabetically. + +--- + +## Acceptance Criteria + +**Definition:** A set of conditions that a feature must satisfy before the product-owner considers it complete. + +**Aliases:** Definition of Done (different concept — do not conflate), exit criteria + +**Example:** "The CLI entrypoint acceptance criterion states: given the package is installed, when the user runs `python -m app --version`, then the output contains the version string from package metadata." + +**Source:** template — BDD practice (Gherkin `Example:` blocks with `@id` tags) + +--- + +## ADR (Architecture Decision Record) + +**Definition:** A short document that records a single significant architectural decision, the context that forced it, the alternatives considered, and the consequences. + +**Aliases:** decision log entry, design decision record + +**Example:** "ADR-2026-04-22-cli-entrypoint records why the team chose argparse over click for the CLI skeleton." + +**Source:** template — Nygard (2011), MADR format + +--- + +## Agent + +**Definition:** An AI assistant assigned a specific role in the development workflow, operating within defined boundaries and producing defined outputs. + +**Aliases:** AI agent, LLM agent, assistant + +**Example:** "The product-owner agent interviews the stakeholder and writes `.feature` files; the software-engineer agent implements the tests and production code." + +**Source:** template — this project's workflow + +--- + +## BDD (Behaviour-Driven Development) + +**Definition:** A collaborative software development practice in which acceptance criteria are written as concrete examples of system behaviour, expressed in a structured natural language understood by both stakeholders and developers. + +**Aliases:** Behaviour-Driven Development, Behavior-Driven Development (US spelling) + +**Example:** "The team uses BDD to write Gherkin `Example:` blocks that become the executable specification for each feature." + +**Source:** template — North (2006) BDD origin paper + +--- + +## Backlog + +**Definition:** The ordered collection of features that have been discovered and baselined but not yet started. + +**Aliases:** feature backlog, product backlog + +**Example:** "The product-owner moves `cli-entrypoint.feature` from `backlog/` to `in-progress/` when the team is ready to begin implementation." + +**Source:** template — this project's workflow + +--- + +## Bounded Context + +**Definition:** A boundary within a domain model inside which a particular ubiquitous language is internally consistent and unambiguous. + +**Aliases:** context boundary, model boundary + +**Example:** "In a retail system, 'Product' means a catalogue entry in the browsing context but means a fulfilment line item in the shipping context — they are different concepts in different bounded contexts." + +**Source:** template — Evans (2003) DDD; Fowler (2014) BoundedContext bliki + +--- + +## CLI Entrypoint + +**Definition:** The `app/__main__.py` module that wires the application's command-line interface, exposing `--help` and `--version` flags via Python's stdlib `argparse`. + +**Aliases:** entry point, main module, CLI entry + +**Example:** "Running `python -m app --version` invokes the CLI entrypoint and prints `temple8 7.1.20260422`." + +**Source:** 2026-04-22 — Session 1; feature `cli-entrypoint` + +--- + +## DDD (Domain-Driven Design) + +**Definition:** A software design approach that centres the codebase around an explicit model of the business domain, using the same language in code, tests, and stakeholder conversations. + +**Aliases:** Domain-Driven Design + +**Example:** "Following DDD, the team names the Python class `Invoice` because the accountant calls it an invoice — not `BillingDocument` or `PaymentRecord`." + +**Source:** template — Evans (2003) Domain-Driven Design; Evans (2015) DDD Reference + +--- + +## Demonstration Feature + +**Definition:** The single working feature that ships with the template to show engineers the full five-step delivery workflow end-to-end before they build their own features. + +**Aliases:** demo feature, starter feature + +**Example:** "The `cli-entrypoint` feature is the demonstration feature — it implements `--help` and `--version` flags and is delivered through all five workflow steps." + +**Source:** 2026-04-22 — Session 1 (Q8, Q9) + +--- + +## Domain Event + +**Definition:** A record of something that happened in the domain that domain experts care about, expressed as a past-tense verb phrase. + +**Aliases:** event, business event + +**Example:** "`OrderPlaced`, `VersionDisplayed`, and `ReportGenerated` are domain events — they record facts that occurred, not commands to be executed." + +**Source:** template — Vernon (2013) Implementing DDD + +--- + +## Feature + +**Definition:** A unit of user-visible functionality described by a `.feature` file containing a title, narrative, rules, and acceptance criteria examples. + +**Aliases:** story, user story (broader concept — a feature here is a Gherkin file) + +**Example:** "The `cli-entrypoint` feature covers all behaviour related to the application's command-line interface." + +**Source:** template — this project's workflow + +--- + +## Gherkin + +**Definition:** A structured plain-English syntax for writing acceptance criteria using `Feature`, `Rule`, `Example`, `Given`, `When`, and `Then` keywords. + +**Aliases:** Cucumber syntax, BDD syntax + +**Example:** "`Given the application package is installed`, `When the user runs python -m app --version`, `Then the output contains the version string` is a Gherkin example." + +**Source:** template — Cucumber project; North (2006) BDD origin + +--- + +## Package Metadata + +**Definition:** The runtime-accessible project information (name, version, description, author) stored in `pyproject.toml` and read at runtime via Python's `importlib.metadata` stdlib module. + +**Aliases:** project metadata, distribution metadata + +**Example:** "`importlib.metadata.version('temple8')` returns `7.1.20260422` at runtime, matching the `version` field in `pyproject.toml`." + +**Source:** 2026-04-22 — Session 1 (Q10, Q11); feature `cli-entrypoint` --- -## Terms +## Product Owner (PO) -| Term | Type | Definition | First seen | -|------|------|------------|------------| -| `Version` | Noun | The semver string (`MAJOR.MINOR.YYYYMMDD`) stored under `[project] version` in `pyproject.toml` and read at runtime via `tomllib`. Never duplicated as a source-code constant. | 2026-04-22 | -| `ValidVerbosity` | Noun | A string value drawn from the closed set `{DEBUG, INFO, WARNING, ERROR, CRITICAL}` — the five standard Python log level names. Any other value is invalid and raises `ValueError`. | 2026-04-22 | -| `version()` | Verb | The function in `app/version.py` that reads `pyproject.toml`, emits an INFO log message in the format `"Version: "`, and returns the version string. | 2026-04-22 | -| `main(verbosity)` | Verb | The CLI entry point in `app/__main__.py`. Accepts a `ValidVerbosity` string, configures the root logger, then calls `version()`. Raises `ValueError` on invalid verbosity. | 2026-04-22 | -| `feature-stem` | Noun | The kebab-case filename (without `.feature` extension) used to identify a feature across `docs/features/`, branch names (`feat/`), and test directories (`tests/features//`). | 2026-04-22 | +**Definition:** The agent responsible for discovering requirements, writing acceptance criteria, and deciding whether delivered features meet stakeholder needs. + +**Aliases:** PO + +**Example:** "The product-owner interviews the stakeholder, writes `.feature` files, and either accepts or rejects delivered features at Step 5." + +**Source:** template — this project's workflow (adapted from Scrum PO role) --- -> Entries are append-only. To correct a definition, add a new row with the corrected text and mark the old one *(superseded by )*. +## Skill + +**Definition:** A markdown file loaded on demand that provides an agent with specialised instructions for a specific task. + +**Aliases:** prompt skill, agent skill + +**Example:** "The software-engineer loads the `implement` skill at the start of Step 3 to receive TDD loop instructions." + +**Source:** template — this project's workflow + +--- + +## Software Engineer (SE) + +**Definition:** The agent responsible for writing tests, implementing production code, and maintaining the git history during the TDD loop. + +**Aliases:** SE, developer, implementer + +**Example:** "The software-engineer runs `uv run task test-fast` after every code change to verify the test suite stays green." + +**Source:** template — this project's workflow + +--- + +## Stakeholder + +**Definition:** The human who owns the problem being solved, provides domain knowledge, and has final authority over whether delivered features meet their needs. + +**Aliases:** user, domain expert, customer, product manager + +**Example:** "The stakeholder answered Q11 by choosing Option C — `--help` + `--version` combined — as the demonstration feature." + +**Source:** template — requirements-elicitation practice + +--- + +## System Architect (SA) + +**Definition:** The agent responsible for translating accepted requirements into an architectural design, writing domain stubs, recording architectural decisions, and verifying implementation against the design. + +**Aliases:** SA, architect, technical lead + +**Example:** "The system-architect reads `cli-entrypoint.feature`, writes domain stubs in `app/__main__.py`, and records the argparse decision as an ADR." + +**Source:** template — this project's workflow + +--- + +## TDD (Test-Driven Development) + +**Definition:** A development practice in which a failing test is written before any production code, the minimum code needed to pass that test is written, and then the code is refactored while keeping the test green. + +**Aliases:** Test-Driven Development, test-first development + +**Example:** "Following TDD, the software-engineer writes a failing `test_cli_entrypoint_c1a2b3d4` test, then writes only enough production code to make it pass." + +**Source:** template — Beck (2002) Test-Driven Development by Example + +--- + +## Ubiquitous Language + +**Definition:** A shared vocabulary built from domain-expert terms that is used consistently in all conversation, documentation, and code within a bounded context. + +**Aliases:** domain language, shared language, common language + +**Example:** "Because the stakeholder says 'help flag', the code uses `--help` as the argument name — the ubiquitous language ensures no translation layer exists between domain expert and code." + +**Source:** template — Evans (2003) DDD; Evans (2015) DDD Reference + +--- + +## WIP (Work In Progress) + +**Definition:** The count of features currently being actively developed; this project enforces a WIP limit of one feature at a time. + +**Aliases:** work in progress, in-flight work + +**Example:** "If `docs/features/in-progress/` already contains a `.feature` file, the WIP limit is reached and no new feature may start until that one is accepted." + +**Source:** template — Kanban WIP limit principle + +--- diff --git a/docs/scope_journal.md b/docs/scope_journal.md index 24d7f0b..b41a456 100644 --- a/docs/scope_journal.md +++ b/docs/scope_journal.md @@ -4,7 +4,7 @@ ## 2026-04-22 — Session 1 -Status: IN-PROGRESS +Status: COMPLETE ### General @@ -23,16 +23,13 @@ Status: IN-PROGRESS | ID | Question | Answer | |----|----------|--------| | Q8 | Should the template ship with any working feature, or be purely empty? | It should ship with exactly one working demonstration feature so engineers see the full workflow end-to-end. | -| Q9 | What is the simplest useful feature for that demonstration? | Displaying the application version read from `pyproject.toml` — it exercises the full stack with no external dependencies. | -### Feature: display-version +### Feature: cli-entrypoint | ID | Question | Answer | |----|----------|--------| -| Q10 | Where is the authoritative version stored? | In `pyproject.toml` under `[project] version`. No other copy should exist. | -| Q11 | How should verbosity be controlled? | Via a string parameter to `main()` matching Python's standard log level names. Invalid values should raise a `ValueError`. | -| Q12 | At what log level should the version be emitted? | INFO — visible by default in most environments, suppressible by raising to WARNING. | -| Q13 | Is the version needed at import time, or only when `main()` runs? | Only when `main()` runs; no module-level side effects. | -| Q14 | What should happen with an unrecognised verbosity string? | Raise `ValueError` naming the invalid value and listing the valid options. | +| Q9 | Which behavioral areas are in scope for the template's own feature backlog? | Just one simple command in the base package — useful for any starting project, simple enough not to bloat the app, and showcasing the template's capabilities end-to-end. | +| Q10 | What kind of command would be "useful for any starting project"? Candidate options presented: version, hello/greet, info/about, config show, health. | Stakeholder asked: "if I choose version, what will it add to my app/ folder?" — confirmed interest in version-style command after seeing the footprint (one file, ~10 lines, zero new dependencies). | +| Q11 | Three options presented: (A) `--help` only, (B) `--version` only, (C) `--help` + `--version` combined. Stakeholder also asked how a help/usage command would look in code and terminal. Full code sketches and tradeoff table provided. Which option for the demonstration feature? | Option C — `--help` + `--version` combined. `python -m app --help` shows app name, tagline, and available options. `python -m app --version` shows `temple8 ` read from package metadata. Zero new dependencies, all code in `app/__main__.py`. | + -Status: COMPLETE From 1807b3d505be36979f87c8ef8eb96d21cf8140fd Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 13:46:11 -0400 Subject: [PATCH 02/27] feat(criteria): write acceptance criteria for cli-entrypoint --- docs/features/backlog/cli-entrypoint.feature | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 docs/features/backlog/cli-entrypoint.feature diff --git a/docs/features/backlog/cli-entrypoint.feature b/docs/features/backlog/cli-entrypoint.feature new file mode 100644 index 0000000..ff0603d --- /dev/null +++ b/docs/features/backlog/cli-entrypoint.feature @@ -0,0 +1,79 @@ +Feature: CLI Entrypoint + + The application exposes a command-line interface via `python -m app`. + Running with `--help` prints the application name, tagline, and available + options then exits with code 0. Running with `--version` prints the + application name and its current version (read from package metadata, which + is sourced from `pyproject.toml`) then exits with code 0. Running with an + unrecognised flag exits with code 2 and prints a usage error. The entire + implementation lives in `app/__main__.py` with no new dependencies — both + `argparse` and `importlib.metadata` are Python stdlib. + + Status: BASELINED (2026-04-22) + + Rules (Business): + - The version string is always read from package metadata at runtime; it is never hardcoded. + - The help description must match the project tagline from `pyproject.toml`. + - Both `--help` and `--version` exit with code 0. + - Unrecognised arguments exit with code 2. + + Constraints: + - Zero new dependencies (argparse and importlib.metadata are stdlib). + - All production code lives in `app/__main__.py` only — no new files. + - Version format follows the project's calver scheme (e.g. `7.1.20260422`); tests must not assume semver. + + Rule: Help output + As a developer using the template + I want to run `python -m app --help` and see the app name, tagline, and available options + So that I know the CLI is wired up correctly and understand what the entry point offers + + @id:c1a2b3d4 + Example: Help flag prints description and exits successfully + Given the application package is installed + When the user runs `python -m app --help` + Then the output contains the application name "temple8" + And the output contains the tagline + And the process exits with code 0 + + @id:e5f6a7b8 + Example: Help flag lists available options + Given the application package is installed + When the user runs `python -m app --help` + Then the output contains "--help" + And the output contains "--version" + + Rule: Version output + As a developer using the template + I want to run `python -m app --version` and see the current version + So that I can verify the installed package version matches what I expect + + @id:c9d0e1f2 + Example: Version flag prints name and version string then exits successfully + Given the application package is installed + When the user runs `python -m app --version` + Then the output contains "temple8" + And the output contains the version string from package metadata + And the process exits with code 0 + + @id:a3b4c5d6 + Example: Version string matches package metadata at runtime + Given the application package is installed + When the user runs `python -m app --version` + Then the version in the output matches `importlib.metadata.version("temple8")` + + Rule: Unrecognised arguments + As a developer using the template + I want unrecognised flags to produce a clear error + So that I know immediately when I have mistyped a command + + @id:e7f8a9b0 + Example: Unknown flag exits with error code + Given the application package is installed + When the user runs `python -m app --unknown-flag` + Then the process exits with code 2 + + @id:b1c2d3e4 + Example: No arguments runs without error + Given the application package is installed + When the user runs `python -m app` with no arguments + Then the process exits with code 0 From 4f6018816280e17cdabc1dc8bd86e06fd3239283 Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 13:50:11 -0400 Subject: [PATCH 03/27] chore: move cli-entrypoint to in-progress, ready for Step 2 --- WORK.md | 12 +++++++++++- .../{backlog => in-progress}/cli-entrypoint.feature | 0 2 files changed, 11 insertions(+), 1 deletion(-) rename docs/features/{backlog => in-progress}/cli-entrypoint.feature (100%) diff --git a/WORK.md b/WORK.md index b0780e6..69ed5ca 100644 --- a/WORK.md +++ b/WORK.md @@ -13,7 +13,9 @@ Each item carries exactly the variables defined by `FLOW.md`: -*(no active items — waiting for PO to move chosen feature to in-progress/)* +- @id: cli-entrypoint + @state: STEP-2-READY + @branch: [NONE — SA creates feat/cli-entrypoint at Step 2 start] --- @@ -31,3 +33,11 @@ Each item carries exactly the variables defined by `FLOW.md`: **Next:** Run `@system-architect` — begin Step 2 (ARCH) for `cli-entrypoint`. PO must first move `docs/features/backlog/cli-entrypoint.feature` → `docs/features/in-progress/cli-entrypoint.feature`. +### 2026-04-22 — PO: Move cli-entrypoint to in-progress + +- Stakeholder confirmed: move `cli-entrypoint.feature` to `in-progress/`. +- Moved `docs/features/backlog/cli-entrypoint.feature` → `docs/features/in-progress/cli-entrypoint.feature`. +- Updated WORK.md: `@state: STEP-2-READY`. + +**Next:** Run `@system-architect` — load skill `architect` and begin Step 2 (Architecture) for `cli-entrypoint`. + diff --git a/docs/features/backlog/cli-entrypoint.feature b/docs/features/in-progress/cli-entrypoint.feature similarity index 100% rename from docs/features/backlog/cli-entrypoint.feature rename to docs/features/in-progress/cli-entrypoint.feature From 42addfd1fda8b3c2eb074e459e1e6a4fd3708fcb Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:03:48 -0400 Subject: [PATCH 04/27] fix(workflow): remove Session Log and Next: from WORK.md and all skill/config files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - WORK.md: stripped Session Log section and Next: blocks; now contains only @id/@state/@branch - AGENTS.md: removed 'session log' from WORK.md description and Chain of Responsibility section - run-session/SKILL.md: removed Next: line format examples, Rule 5 about Next:, and 'Always close with Next:' from Output Style - flow/SKILL.md: removed Rule 4 about Next: line in session output - flow/flow.md.template: replaced 'Every session must close with Next:' with neutral output style guidance - FLOW.md: removed 'Every agent session must close with Next:' from Output Style - architect/SKILL.md: fix erroneous 'update FLOW.md Next:' instruction → update WORK.md @state - verify/SKILL.md: fix erroneous 'update FLOW.md Next:' instruction → update WORK.md @state - implement/SKILL.md: fix 'Note gap in FLOW.md under ## Next' → document gap in handoff message --- .github/workflows/tag-release.yml | 19 ++ .opencode/agents/product-owner.md | 6 +- .opencode/agents/software-engineer.md | 4 +- .opencode/agents/system-architect.md | 6 +- .opencode/skills/architect/SKILL.md | 8 +- .../system.md.template | 0 .opencode/skills/check-quality/SKILL.md | 4 +- .opencode/skills/create-pr/SKILL.md | 2 +- .opencode/skills/define-scope/SKILL.md | 11 +- .../skills/define-scope/discovery-template.md | 9 - .../skills/define-scope/feature.md.template | 2 +- .../skills/define-scope/glossary.md.template | 255 +++++++++++++++- .opencode/skills/flow/SKILL.md | 23 +- .opencode/skills/flow/flow.md.template | 17 +- .opencode/skills/flow/work.md.template | 5 - .opencode/skills/git-release/SKILL.md | 23 +- .opencode/skills/implement/SKILL.md | 19 +- .opencode/skills/implement/adr.md.template | 36 --- .opencode/skills/run-session/SKILL.md | 34 +-- .opencode/skills/select-feature/SKILL.md | 10 +- .opencode/skills/update-docs/SKILL.md | 45 +-- .../skills/update-docs/glossary.md.template | 18 -- .opencode/skills/verify/SKILL.md | 6 +- AGENTS.md | 10 +- FLOW.md | 105 +++---- README.md | 2 +- WORK.md | 26 +- app/__main__.py | 24 -- .../ADR-2026-04-22-verbosity-validation.md | 26 -- docs/adr/ADR-2026-04-22-version-source.md | 26 -- docs/branding.md | 13 +- docs/features/backlog/.gitkeep | 0 .../completed/display-version.feature | 60 ---- docs/research/domain-modeling.md | 17 +- docs/system.md | 151 ---------- pyproject.toml | 10 +- scripts/check_adrs.py | 62 ++++ scripts/check_commit_messages.py | 127 ++++++++ scripts/check_feature_file.py | 160 ++++++++++ scripts/check_oc.py | 204 +++++++++++++ scripts/check_stubs.py | 113 +++++++ scripts/check_version.py | 65 ++++ scripts/check_work_md.py | 116 ++++++++ scripts/detect_state.py | 278 ++++++++++++++++++ scripts/score_features.py | 78 +++++ tests/unit/app_test.py | 18 -- uv.lock | 4 - 47 files changed, 1628 insertions(+), 629 deletions(-) rename .opencode/skills/{implement => architect}/system.md.template (100%) delete mode 100644 .opencode/skills/define-scope/discovery-template.md delete mode 100644 .opencode/skills/implement/adr.md.template delete mode 100644 .opencode/skills/update-docs/glossary.md.template delete mode 100644 docs/adr/ADR-2026-04-22-verbosity-validation.md delete mode 100644 docs/adr/ADR-2026-04-22-version-source.md delete mode 100644 docs/features/backlog/.gitkeep delete mode 100644 docs/features/completed/display-version.feature delete mode 100644 docs/system.md create mode 100644 scripts/check_adrs.py create mode 100644 scripts/check_commit_messages.py create mode 100644 scripts/check_feature_file.py create mode 100644 scripts/check_oc.py create mode 100644 scripts/check_stubs.py create mode 100644 scripts/check_version.py create mode 100644 scripts/check_work_md.py create mode 100644 scripts/detect_state.py create mode 100644 scripts/score_features.py delete mode 100644 tests/unit/app_test.py diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 4fbc251..e7c4325 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -35,6 +35,25 @@ jobs: echo "exists=false" >> "$GITHUB_OUTPUT" fi + - name: Install uv + if: steps.check.outputs.exists == 'false' + uses: astral-sh/setup-uv@cdfb2ee6dde255817c739680168ad81e184c4bfb # v4.0.0 + with: + enable-cache: true + cache-dependency-glob: "uv.lock" + + - name: Set up Python 3.13 + if: steps.check.outputs.exists == 'false' + run: uv python install 3.13 + + - name: Install dependencies + if: steps.check.outputs.exists == 'false' + run: uv sync --locked --all-extras --dev + + - name: Run release-check + if: steps.check.outputs.exists == 'false' + run: uv run task release-check + - name: Create and push tag if: steps.check.outputs.exists == 'false' env: diff --git a/.opencode/agents/product-owner.md b/.opencode/agents/product-owner.md index a5ec440..3993111 100644 --- a/.opencode/agents/product-owner.md +++ b/.opencode/agents/product-owner.md @@ -45,8 +45,8 @@ After the system-architect approves (Step 4): 1. Run or observe the feature yourself. If user interaction is involved, interact with it. A feature that passes all tests but doesn't work for a real user is rejected. 2. Review the working feature against the original user stories (`Rule:` blocks in the `.feature` file). -3. **If accepted**: move `docs/features/in-progress/.feature` → `docs/features/completed/.feature`; update `WORK.md` (`@state: STEP-5-MERGE`, append to Session Log); notify stakeholder. The stakeholder decides when to trigger PR and release. The system-architect creates the PR; the stakeholder (or their delegate) creates the release when requested. -4. **If rejected**: write specific feedback in `WORK.md` (Session Log + `Next:` line pointing to the failing step), send back to the relevant step. +3. **If accepted**: move `docs/features/in-progress/.feature` → `docs/features/completed/.feature`; update `WORK.md` (`@state: STEP-5-MERGE`); notify stakeholder. The stakeholder decides when to trigger PR and release. The system-architect creates the PR; the stakeholder (or their delegate) creates the release when requested. +4. **If rejected**: write specific feedback in `WORK.md` pointing to the failing step, then send back to the relevant step. ## Handling Gaps @@ -64,7 +64,7 @@ When a gap is reported (by software-engineer or system-architect): When a defect is reported against any feature: 1. Add a `@bug` Example to the relevant `Rule:` block in the `.feature` file using the standard `Given/When/Then` format describing the correct behavior. -2. Update `WORK.md` Session Log to note the new bug Example; set `Next:` to `Run @software-engineer — implement @bug Example in `. +2. Update `WORK.md` `@state` to reflect the bug work and notify the software-engineer. 3. SE implements the test in `tests/features/` **and** a `@given` Hypothesis property test in `tests/unit/`. Both are required. ## Available Skills diff --git a/.opencode/agents/software-engineer.md b/.opencode/agents/software-engineer.md index a58a148..a4227f0 100644 --- a/.opencode/agents/software-engineer.md +++ b/.opencode/agents/software-engineer.md @@ -53,14 +53,14 @@ Load `skill run-session` first — it reads FLOW.md, orients you to the current If `docs/features/in-progress/` contains only `.gitkeep` (no `.feature` file): 1. Do not pick a feature from backlog yourself. -2. Update `WORK.md` `Next:` line: `Run @product-owner — load skill select-feature and pick the next BASELINED feature from backlog.` +2. Update `WORK.md` `@state` to `[IDLE]` if it is not already. 3. Stop. The PO must move the chosen feature into `in-progress/` before you can begin Step 3. ## Spec Gaps If during implementation you discover behavior not covered by existing acceptance criteria: - Do not extend criteria yourself — escalate to the PO -- Note the gap in `WORK.md` `Next:` line and Session Log +- Note the gap in `WORK.md` and escalate to PO ## Available Skills diff --git a/.opencode/agents/system-architect.md b/.opencode/agents/system-architect.md index 1457029..5e2a8bb 100644 --- a/.opencode/agents/system-architect.md +++ b/.opencode/agents/system-architect.md @@ -47,13 +47,13 @@ Load `skill run-session` first — it reads FLOW.md, orients you to the current - You own `docs/system.md` (including the `## Domain Model` section) and `docs/adr/ADR-*.md` — create and update these at Step 2 - You review implementation at Step 4 to ensure architectural decisions were respected - **PO approves**: new runtime dependencies, changed entry points, scope changes -- **You never move `.feature` files.** The PO is the sole owner of all feature file moves. If you find no `.feature` file in `docs/features/in-progress/`, **STOP** — do not self-select a feature. Update `WORK.md` `Next:` and escalate to PO. +- **You never move `.feature` files.** The PO is the sole owner of all feature file moves. If you find no `.feature` file in `docs/features/in-progress/`, **STOP** — do not self-select a feature. Update `WORK.md` `@state` to `[IDLE]` and escalate to PO. ## Step 2 → Step 3 Handoff After architecture is complete and test stubs are generated: 1. Commit all changes on `feat/` -2. Update `WORK.md`: set `@state: STEP-3-WORKING`, append to Session Log, set `Next: Run @software-engineer — Step 3 TDD Loop` +2. Update `WORK.md`: set `@state: STEP-3-WORKING` 3. Stop. The SE takes over for implementation. ## Step 4 Review Stance @@ -67,7 +67,7 @@ Your default hypothesis is that the code is broken despite passing automated che If during Step 2 or Step 4 you discover behavior not covered by existing acceptance criteria: - Do not extend criteria yourself — escalate to the PO -- Note the gap in `WORK.md` `Next:` line and Session Log +- Note the gap in `WORK.md` and escalate to PO ## Available Skills diff --git a/.opencode/skills/architect/SKILL.md b/.opencode/skills/architect/SKILL.md index 55f005b..9bc2b1b 100644 --- a/.opencode/skills/architect/SKILL.md +++ b/.opencode/skills/architect/SKILL.md @@ -31,7 +31,7 @@ Design correctness is far more important than lint/pyright/coverage compliance. ### Prerequisites (stop if any fail — escalate to PO) -1. `docs/features/in-progress/` contains exactly one `.feature` file (not just `.gitkeep`). If none exists, **STOP** — update FLOW.md `Next:` to `Run @product-owner — move the chosen feature to in-progress/` and stop. Never self-select or move a feature yourself. +1. `docs/features/in-progress/` contains exactly one `.feature` file (not just `.gitkeep`). If none exists, **STOP** — update `WORK.md` `@state` to `[IDLE]` and stop. Never self-select or move a feature yourself. 2. The feature file's discovery section has `Status: BASELINED`. If not, escalate to PO — Step 1 is incomplete. 3. The feature file contains `Rule:` blocks with `Example:` blocks and `@id` tags. If not, escalate to PO — criteria have not been written. 4. Package name confirmed: read `pyproject.toml` → locate `[tool.setuptools]` → confirm directory exists on disk. @@ -196,7 +196,7 @@ Commit: `feat(): add architecture and test stubs` ### Hand off to Step 3 (TDD Loop) -1. Update FLOW.md: `Next: Run @software-engineer — Step 3 TDD Loop` +1. Update `WORK.md` `@state: STEP-3-WORKING` 2. Provide the SE with: - Feature file path - Summary of stubs created @@ -210,7 +210,7 @@ Commit: `feat(): add architecture and test stubs` If during architecture you discover behavior not covered by existing acceptance criteria: - **Do not extend criteria yourself** — escalate to PO -- Note the gap in `WORK.md` Next: line and Session Log +- Note the gap in `WORK.md` and escalate to PO - The PO will decide whether to add a new Example to the `.feature` file --- @@ -222,6 +222,6 @@ Templates for files written by this skill live in this skill's directory (`archi - `system.md.template` — `docs/system.md` structure (domain model + Context + Container sections included) - `adr.md.template` — individual ADR file structure (includes `## Context` section) -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/architect +Base directory for this skill: `.opencode/skills/architect/` Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. Note: file list is sampled. diff --git a/.opencode/skills/implement/system.md.template b/.opencode/skills/architect/system.md.template similarity index 100% rename from .opencode/skills/implement/system.md.template rename to .opencode/skills/architect/system.md.template diff --git a/.opencode/skills/check-quality/SKILL.md b/.opencode/skills/check-quality/SKILL.md index 6de9f77..6f6b472 100644 --- a/.opencode/skills/check-quality/SKILL.md +++ b/.opencode/skills/check-quality/SKILL.md @@ -22,7 +22,7 @@ Load this skill when completing Step 3 and preparing to hand off to the system-a ```bash uv run task lint # ruff check + ruff format — must exit 0 uv run task static-check # pyright — must exit 0, 0 errors -uv run task test # pytest with coverage — must exit 0, 100% coverage +uv run task test-coverage # pytest with coverage — must exit 0, coverage passes timeout 10s uv run task run # app starts — must exit non-124 ``` @@ -32,6 +32,6 @@ All four must pass. Do not hand off broken work. - [ ] `lint` exits 0 (ruff check + ruff format) - [ ] `static-check` exits 0, 0 pyright errors -- [ ] `test` exits 0, 100% coverage +- [ ] `test-coverage` exits 0, coverage passes - [ ] `run` exits non-124 (not hung) - [ ] No `noqa` or `type: ignore` — fix the underlying issue diff --git a/.opencode/skills/create-pr/SKILL.md b/.opencode/skills/create-pr/SKILL.md index 72bfea0..61ba48b 100644 --- a/.opencode/skills/create-pr/SKILL.md +++ b/.opencode/skills/create-pr/SKILL.md @@ -77,7 +77,7 @@ EOF - [ ] All commits follow conventional commit format - [ ] `task lint` exits 0 - [ ] `task static-check` exits 0 -- [ ] `task test` exits 0, coverage 100% +- [ ] `task test` exits 0, coverage passes - [ ] `timeout 10s task run` exits with code ≠ 124 - [ ] PR description includes all `@id` acceptance criteria diff --git a/.opencode/skills/define-scope/SKILL.md b/.opencode/skills/define-scope/SKILL.md index 58f0d3b..61a9f27 100644 --- a/.opencode/skills/define-scope/SKILL.md +++ b/.opencode/skills/define-scope/SKILL.md @@ -428,7 +428,7 @@ Stakeholder reports a feature is wrong after PO acceptance attempt. ``` 4. **PO scans `docs/post-mortem/`**, selects relevant files by `` or `` in filename. 5. **PO reads selected post-mortems** for context before handoff. -6. **PO updates `WORK.md`**: set `@state: STEP-2-ARCH`, `@branch: fix/`; append to Session Log; set `Next: Run @system-architect — restart Step 2 for on fix/ with post-mortem context`. +6. **PO updates `WORK.md`**: set `@state: STEP-2-ARCH`, `@branch: fix/`. 7. **SA begins Step 2** on `fix/`, reading relevant post-mortems as input. ### Document Format @@ -452,13 +452,14 @@ All templates for files written by this skill live in this skill's directory: - `discovery.md.template` — `docs/discovery.md` per-session block - `feature.md.template` — `.feature` file structure - `post-mortem.md.template` — `docs/post-mortem/YYYY-MM-DD--.md` structure +- `glossary.md.template` — `docs/glossary.md` initial file (pre-filled with common jargon; PO appends project-specific entries) -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/define-scope +Base directory for this skill: `.opencode/skills/define-scope/` Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. Note: file list is sampled. -/home/user/Documents/projects/python-project-template/.opencode/skills/define-scope/discovery.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/define-scope/feature.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/define-scope/scope-journal.md.template +.opencode/skills/define-scope/discovery.md.template +.opencode/skills/define-scope/feature.md.template +.opencode/skills/define-scope/scope-journal.md.template diff --git a/.opencode/skills/define-scope/discovery-template.md b/.opencode/skills/define-scope/discovery-template.md deleted file mode 100644 index aa4cc5c..0000000 --- a/.opencode/skills/define-scope/discovery-template.md +++ /dev/null @@ -1,9 +0,0 @@ -Feature: - - <2–4 sentence description of what this feature does and why it exists.> - - Status: ELICITING - - Rules (Business): - - Constraints: diff --git a/.opencode/skills/define-scope/feature.md.template b/.opencode/skills/define-scope/feature.md.template index 7a6b9e0..40f9226 100644 --- a/.opencode/skills/define-scope/feature.md.template +++ b/.opencode/skills/define-scope/feature.md.template @@ -23,7 +23,7 @@ Feature: Then @deprecated @id:b5c6d7e8 - Example: + Example: Given ... When ... Then ... diff --git a/.opencode/skills/define-scope/glossary.md.template b/.opencode/skills/define-scope/glossary.md.template index c7cdc7a..dbb511f 100644 --- a/.opencode/skills/define-scope/glossary.md.template +++ b/.opencode/skills/define-scope/glossary.md.template @@ -1,19 +1,260 @@ # Glossary: -> Living glossary of domain terms. -> Written and maintained by the product-owner. -> Terms are added after each discovery session, updated when meaning changes. -> If code or tests diverge from a term here, refactor the code — not the glossary. +> Living glossary of domain terms used in this project. +> Written and maintained by the product-owner during Step 1 discovery. +> Append-only: never edit or remove past entries. If a term changes, mark it superseded and write a new entry. +> Code and tests take precedence over this glossary — if they diverge, refactor the code, not this file. --- +## Entry Format + +``` ## - +**Definition:** + +**Aliases:** + +**Example:** + +**Source:** +``` + +Entries are sorted alphabetically. + +--- + +## Common Jargon + +These entries cover methodology and tooling terms used throughout this project. They are pre-filled so that no domain expertise is required to read the documentation. + +--- + +## Acceptance Criteria + +**Definition:** A set of conditions that a feature must satisfy before the product-owner considers it complete. + +**Aliases:** Definition of Done (different concept — do not conflate), exit criteria + +**Example:** "The version command acceptance criterion states: given any valid invocation, when the user runs the command, then the output contains a semantic version string." + +**Source:** template — BDD practice (Gherkin `Example:` blocks with `@id` tags) + +--- + +## ADR (Architecture Decision Record) + +**Definition:** A short document that records a single significant architectural decision, the context that forced it, the alternatives considered, and the consequences. + +**Aliases:** decision log entry, design decision record + +**Example:** "ADR-2025-01-15-hexagonal-architecture records why the team chose hexagonal architecture over layered architecture." + +**Source:** template — Nygard (2011), MADR format + +--- + +## Agent + +**Definition:** An AI assistant assigned a specific role in the development workflow, operating within defined boundaries and producing defined outputs. + +**Aliases:** AI agent, LLM agent, assistant + +**Example:** "The product-owner agent interviews the stakeholder and writes `.feature` files; the software-engineer agent implements the tests and production code." + +**Source:** template — this project's workflow + +--- + +## BDD (Behaviour-Driven Development) + +**Definition:** A collaborative software development practice in which acceptance criteria are written as concrete examples of system behaviour, expressed in a structured natural language understood by both stakeholders and developers. + +**Aliases:** Behaviour-Driven Development, Behavior-Driven Development (US spelling) + +**Example:** "The team uses BDD to write Gherkin `Scenario:` blocks that become the executable specification for each feature." + +**Source:** template — North (2006) BDD origin paper + +--- + +## Backlog + +**Definition:** The ordered collection of features that have been discovered and baselined but not yet started. + +**Aliases:** feature backlog, product backlog + +**Example:** "The product-owner moves a `.feature` file from `backlog/` to `in-progress/` when the team is ready to begin implementation." + +**Source:** template — this project's workflow + +--- + +## Bounded Context + +**Definition:** A boundary within a domain model inside which a particular ubiquitous language is internally consistent and unambiguous. + +**Aliases:** context boundary, model boundary + +**Example:** "In a retail system, 'Product' means a catalogue entry in the browsing context but means a fulfilment line item in the shipping context — they are different concepts in different bounded contexts." + +**Source:** template — Evans (2003) DDD; Fowler (2014) BoundedContext bliki + +--- + +## DDD (Domain-Driven Design) + +**Definition:** A software design approach that centres the codebase around an explicit model of the business domain, using the same language in code, tests, and stakeholder conversations. + +**Aliases:** Domain-Driven Design + +**Example:** "Following DDD, the team names the Python class `Invoice` because the accountant calls it an invoice — not `BillingDocument` or `PaymentRecord`." + +**Source:** template — Evans (2003) Domain-Driven Design; Evans (2015) DDD Reference + +--- + +## Domain Event + +**Definition:** A record of something that happened in the domain that domain experts care about, expressed as a past-tense verb phrase. + +**Aliases:** event, business event + +**Example:** "`OrderPlaced`, `VersionDisplayed`, and `ReportGenerated` are domain events — they record facts that occurred, not commands to be executed." + +**Source:** template — Vernon (2013) Implementing DDD + +--- + +## Feature + +**Definition:** A unit of user-visible functionality described by a `.feature` file containing a title, narrative, rules, and acceptance criteria examples. + +**Aliases:** story, user story (broader concept — a feature here is a Gherkin file) + +**Example:** "The `display-version` feature covers all behaviour related to showing the application version to the user." + +**Source:** template — this project's workflow + +--- + +## Gherkin + +**Definition:** A structured plain-English syntax for writing acceptance criteria using `Feature`, `Rule`, `Scenario`, `Given`, `When`, and `Then` keywords. + +**Aliases:** Cucumber syntax, BDD syntax + +**Example:** "`Given the application is installed`, `When the user runs version`, `Then the output contains a semantic version` is a Gherkin scenario." + +**Source:** template — Cucumber project; North (2006) BDD origin + +--- + +## Product Owner (PO) + +**Definition:** The agent responsible for discovering requirements, writing acceptance criteria, and deciding whether delivered features meet stakeholder needs. + +**Aliases:** PO + +**Example:** "The product-owner interviews the stakeholder, writes `.feature` files, and either accepts or rejects delivered features at Step 5." + +**Source:** template — this project's workflow (adapted from Scrum PO role) + +--- + +## Skill + +**Definition:** A markdown file loaded on demand that provides an agent with specialised instructions for a specific task. + +**Aliases:** prompt skill, agent skill + +**Example:** "The software-engineer loads the `implement` skill at the start of Step 3 to receive TDD loop instructions." + +**Source:** template — this project's workflow + +--- + +## Software Engineer (SE) + +**Definition:** The agent responsible for writing tests, implementing production code, and maintaining the git history during the TDD loop. + +**Aliases:** SE, developer, implementer + +**Example:** "The software-engineer runs `uv run task test-fast` after every code change to verify the test suite stays green." + +**Source:** template — this project's workflow + +--- + +## Stakeholder + +**Definition:** The human who owns the problem being solved, provides domain knowledge, and has final authority over whether delivered features meet their needs. + +**Aliases:** user, domain expert, customer, product manager + +**Example:** "The stakeholder answers the product-owner's questions during discovery sessions and approves the feature synthesis at Stage 1 close." + +**Source:** template — requirements-elicitation practice + +--- + +## System Architect (SA) + +**Definition:** The agent responsible for translating accepted requirements into an architectural design, writing domain stubs, recording architectural decisions, and verifying implementation against the design. + +**Aliases:** SA, architect, technical lead + +**Example:** "The system-architect reads the in-progress `.feature` file, writes domain stubs, and records the hexagonal architecture decision as an ADR before handing off to the software-engineer." + +**Source:** template — this project's workflow + +--- + +## TDD (Test-Driven Development) + +**Definition:** A development practice in which a failing test is written before any production code, the minimum code needed to pass that test is written, and then the code is refactored while keeping the test green. + +**Aliases:** Test-Driven Development, test-first development + +**Example:** "Following TDD, the software-engineer writes a failing `test_display_version_shows_semver` test, then writes only enough production code to make it pass." + +**Source:** template — Beck (2002) Test-Driven Development by Example + +--- + +## Ubiquitous Language + +**Definition:** A shared vocabulary built from domain-expert terms that is used consistently in all conversation, documentation, and code within a bounded context. + +**Aliases:** domain language, shared language, common language + +**Example:** "Because the accountant says 'invoice', the code uses `Invoice` as the class name — the ubiquitous language ensures no translation layer exists between domain expert and code." + +**Source:** template — Evans (2003) DDD; Evans (2015) DDD Reference (entry #63 in research) + +--- + +## WIP (Work In Progress) + +**Definition:** The count of features currently being actively developed; this project enforces a WIP limit of one feature at a time. + +**Aliases:** work in progress, in-flight work + +**Example:** "If `docs/features/in-progress/` already contains a `.feature` file, the WIP limit is reached and no new feature may start until that one is accepted." + +**Source:** template — Kanban WIP limit principle --- ## - +**Definition:** + +**Aliases:** + +**Example:** + +**Source:** + +--- diff --git a/.opencode/skills/flow/SKILL.md b/.opencode/skills/flow/SKILL.md index 566a609..7b3fb1a 100644 --- a/.opencode/skills/flow/SKILL.md +++ b/.opencode/skills/flow/SKILL.md @@ -45,9 +45,10 @@ A finite state machine (FSM) consists of: ### The Two-File Pattern -``` -FLOW.md ── "What are the rules?" (static, versioned with the project) -WORK.md ── "What is happening now?" (dynamic, updated by agents) +```mermaid +flowchart LR + FLOW["FLOW.md
What are the rules?
(static, versioned with the project)"] + WORK["WORK.md
What is happening now?
(dynamic, updated by agents)"] ``` Agents read `FLOW.md` to know **what to do**. They read `WORK.md` to know **what is active and where it is**. They write only to `WORK.md` during normal operation. @@ -121,7 +122,7 @@ List each role with its agent file path. Every state owner must appear in this t ### Step 6 — Draw the transition diagram -Include an ASCII or Mermaid diagram. It must show every state and every valid transition including failure routes. This is the primary human-readable artifact. +Include a Mermaid `stateDiagram-v2`. It must show every state and every valid transition including failure routes. This is the primary human-readable artifact. --- @@ -141,7 +142,6 @@ Include an ASCII or Mermaid diagram. It must show every state and every valid tr 1. Update `WORK.md`: - Set `@state` to the new state - - Append to Session Log 2. Commit WORK.md update before any further work: ```bash git add WORK.md && git commit -m "chore: @id transition to @state" @@ -181,11 +181,6 @@ Each item carries exactly the variables defined by `FLOW.md`: @state: @branch: ---- - -## Session Log - - ``` Multiple active items are allowed when the workflow permits parallel work. Each is a separate bullet entry under `## Active Items`. @@ -214,8 +209,6 @@ Steps: 1. Never skip reading `FLOW.md` and `WORK.md` at session start 2. Never end a session without updating `WORK.md` and committing 3. Never commit directly to `main` -4. The `Next:` line in every session output must be actionable for a fresh agent -5. `WORK.md` Session Log is append-only — never delete entries -6. If `WORK.md` is missing, create it from `work.md.template` before any other work -7. If detected state differs from `WORK.md`, trust the filesystem and update `WORK.md` -8. One step per session where possible; do not start Step N+1 in the same session as Step N +4. If `WORK.md` is missing, create it from `work.md.template` before any other work +5. If detected state differs from `WORK.md`, trust the filesystem and update `WORK.md` +6. One step per session where possible; do not start Step N+1 in the same session as Step N diff --git a/.opencode/skills/flow/flow.md.template b/.opencode/skills/flow/flow.md.template index 63424a5..af0b99f 100644 --- a/.opencode/skills/flow/flow.md.template +++ b/.opencode/skills/flow/flow.md.template @@ -46,11 +46,14 @@ All must be satisfied before starting any session. If any are missing, stop and States are checked **in order**. The first matching condition is the current state. -``` -[INITIAL] ──► [STATE-A] ──► [STATE-B] ──► [TERMINAL] - │ - ▼ - [FAILURE] +```mermaid +stateDiagram-v2 + [*] --> INITIAL + INITIAL --> STATE-A + STATE-A --> STATE-B + STATE-B --> TERMINAL + STATE-B --> FAILURE + TERMINAL --> [*] ``` ### Detection Rules (evaluated in order) @@ -115,7 +118,7 @@ States are checked **in order**. The first matching condition is the current sta 4. Check prerequisites — if any missing, stop and report ### Session End -1. Update `WORK.md`: set `@state` to the new state; append to Session Log +1. Update `WORK.md`: set `@state` to the new state 2. Commit `WORK.md` update before any further work: ```bash git add WORK.md && git commit -m "chore: @id transition to @state" @@ -137,4 +140,4 @@ States are checked **in order**. The first matching condition is the current sta ## Output Style -Every session must close with a `Next:` line — one concrete action for a fresh agent. +Report results, not process. No narration, no summaries. Output only what the next agent or stakeholder needs: findings, status, decisions, blockers. diff --git a/.opencode/skills/flow/work.md.template b/.opencode/skills/flow/work.md.template index 54579c7..22a2f34 100644 --- a/.opencode/skills/flow/work.md.template +++ b/.opencode/skills/flow/work.md.template @@ -22,8 +22,3 @@ Format: *(no active items)* ---- - -## Session Log - - diff --git a/.opencode/skills/git-release/SKILL.md b/.opencode/skills/git-release/SKILL.md index 6f3794d..66fd600 100644 --- a/.opencode/skills/git-release/SKILL.md +++ b/.opencode/skills/git-release/SKILL.md @@ -109,7 +109,17 @@ Load and execute the full `update-docs` skill now: The `update-docs` commit step is **skipped** here — all changed files are staged together with the version bump in step 6. -### 6. Regenerate lockfile and commit version bump +### 6. Run release-check + +Run the automated pre-release checklist before committing: + +```bash +uv run task release-check +``` + +If this fails, fix the issues and rerun. Do not commit until it passes. + +### 7. Regenerate lockfile and commit version bump After updating `pyproject.toml`, regenerate the lockfile — CI runs `uv sync --locked` and will fail if it is stale: @@ -121,7 +131,7 @@ git commit -m "chore(release): bump version to v{version}[ - {Release Name}]" # Include " - {Release Name}" only if a release name was generated in Step 0; omit otherwise. ``` -### 7. Create GitHub release +### 8. Create GitHub release Assign the SHA first so it expands correctly inside the notes string: @@ -153,7 +163,7 @@ gh release create "v{version}" \ # Replace [ - {Release Name}] with the actual name, or omit the bracketed portion entirely if Step 0 produced no name. ``` -### 8. If a hotfix commit follows the release tag +### 9. If a hotfix commit follows the release tag If CI fails after the release (e.g. a stale lockfile) and a hotfix commit is pushed, reassign the tag and GitHub release to that commit: @@ -174,12 +184,9 @@ The release notes and title do not need to change — only the target commit mov ## Quality Checklist -- [ ] `task test` passes -- [ ] `task lint` passes -- [ ] `task static-check` passes +- [ ] `task release-check` passes (runs version alignment, changelog entry, lint, static-check, tests, doc-build) - [ ] `pyproject.toml` version updated -- [ ] `uv lock` run after version bump — lockfile must be up to date -- [ ] `/__version__` matches `pyproject.toml` version +- [ ] `/__version__` matches `pyproject.toml` version (if present) - [ ] CHANGELOG.md updated - [ ] `update-docs` skill run — Context, Container sections, and glossary reflect the new feature - [ ] Release name not used before diff --git a/.opencode/skills/implement/SKILL.md b/.opencode/skills/implement/SKILL.md index 793ad01..51aa0ff 100644 --- a/.opencode/skills/implement/SKILL.md +++ b/.opencode/skills/implement/SKILL.md @@ -75,7 +75,7 @@ INNER LOOP ├── uv run task test-fast after each individual change └── EXIT: test-fast passes; no smells remain -Mark @id completed in WORK.md Session Log +Commit when a meaningful increment is green Commit when a meaningful increment is green ``` @@ -84,11 +84,11 @@ Commit when a meaningful increment is green ```bash uv run task lint uv run task static-check -uv run task test # coverage must be 100% +uv run task test-coverage # coverage must pass timeout 10s uv run task run ``` -If coverage < 100%: add test in `tests/unit/` for uncovered branch (do NOT add @id tests for coverage). +If coverage is below the threshold: add test in `tests/unit/` for uncovered branch (do NOT add @id tests for coverage). All must pass before Self-Declaration. @@ -183,7 +183,7 @@ def test__<@id>() -> None: ### Markers - `@pytest.mark.slow` — takes > 50ms (Hypothesis, DB, network, terminal I/O) -- `@pytest.mark.deprecated` — auto-skipped by pytest-beehave; used for superseded Examples +- `@pytest.mark.deprecated` — auto-skipped by pytest-beehave; used for replaced Examples ```python @pytest.mark.deprecated @@ -254,7 +254,7 @@ If testing through the real entry point is infeasible, escalate to PO to adjust If during implementation you discover a behavior not covered by existing acceptance criteria: - **Do not extend criteria yourself** — escalate to PO -- Note the gap in FLOW.md under `## Next` +- Document the gap in your handoff message to the PO - The PO will decide whether to add a new Example to the `.feature` file Extra tests in `tests/unit/` are allowed freely (coverage, edge cases, etc.) — these do not need `@id` traceability. @@ -291,13 +291,6 @@ class UserRepository(Protocol): --- -## Templates - -Templates for architecture files live in the `architect` skill's directory: - -- `system.md.template` — `docs/system.md` structure (includes `## Domain Model`, `## Context`, `## Container` sections) -- `adr.md.template` — individual ADR file structure (includes `## Context` section) - -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/implement +Base directory for this skill: `.opencode/skills/implement/` Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. Note: file list is sampled. diff --git a/.opencode/skills/implement/adr.md.template b/.opencode/skills/implement/adr.md.template deleted file mode 100644 index 80b0bb6..0000000 --- a/.opencode/skills/implement/adr.md.template +++ /dev/null @@ -1,36 +0,0 @@ -# ADR: - -> Architectural Decision Record -> Written by the system-architect during Step 2 for non-obvious decisions with meaningful trade-offs. -> Routine YAGNI choices do not need a record. - -| Field | Value | -|-------|-------| -| **Date** | YYYY-MM-DD | -| **Feature** | | -| **Status** | Proposed \| Accepted \| Superseded | - -## Context - -**Question ():** - -<1–3 sentences: what was known, what was asked, what constraints or stakeholder input produced the decision> - ---- - -## Decision - - - -## Reason - - - -## Alternatives Considered - -- ****: rejected — - -## Consequences - -- (+) -- (-) diff --git a/.opencode/skills/run-session/SKILL.md b/.opencode/skills/run-session/SKILL.md index 6c2b578..552e906 100644 --- a/.opencode/skills/run-session/SKILL.md +++ b/.opencode/skills/run-session/SKILL.md @@ -11,7 +11,7 @@ workflow: session-management Every session starts by reading state. Every session ends by writing state. This makes any agent able to continue from where the last session stopped. -State is tracked across two files: `FLOW.md` (static state machine — never modified by agents) and `WORK.md` (dynamic tracker — updated every session). Agents read `FLOW.md` to understand the workflow; they read and update `WORK.md` to track the active feature and session log. +State is tracked across two files: `FLOW.md` (static state machine — never modified by agents) and `WORK.md` (dynamic tracker — updated every session). Agents read `FLOW.md` to understand the workflow; they read and update `WORK.md` to track the active feature. ## Read Policy @@ -26,7 +26,7 @@ Each agent reads only what is operationally necessary for their current step. Do ## Session Start -1. **Read `WORK.md`** — find the active item: `@id`, `@state`, `@branch`, and `Next:` line. +1. **Read `WORK.md`** — find the active item: `@id`, `@state`, `@branch`. - If `WORK.md` does not exist, create it from `.opencode/skills/flow/work.md.template` 2. **Read `FLOW.md`** — understand the static workflow (roles, states, detection rules, transitions). - If `FLOW.md` does not exist, create it from `.opencode/skills/flow/flow.md.template` @@ -48,14 +48,12 @@ Each agent reads only what is operationally necessary for their current step. Do **If `WORK.md` `@state` is [IDLE] or no active item exists:** - **PO**: Load `skill select-feature` — it guides you through scoring and selecting the next BASELINED backlog feature. You must verify the feature has `Status: BASELINED` before moving it to `in-progress/`. Only you may move it. -- **Software-engineer or system-architect**: Update `WORK.md` `Next:` line to `Run @product-owner — load skill select-feature and pick the next BASELINED feature from backlog.` Then **stop**. Never self-select a feature. Never create, edit, or move a `.feature` file. +- **Software-engineer or system-architect**: Update `WORK.md` `@state` to `[IDLE]` if it is not already, then **stop**. Never self-select a feature. Never create, edit, or move a `.feature` file. ## Session End 1. Update `WORK.md`: - Set `@state` to the detected state - - Append to Session Log with timestamp, agent, state, and action - - Update the `Next:` line with one concrete action 2. Commit any uncommitted work (even WIP): ```bash git add -A @@ -83,41 +81,25 @@ When a step completes within a session: @id: @state: @branch: | [NONE] - -## Session Log -**YYYY-MM-DD HH:MM** — - -## Next -Run @ ``` -**"Next" line format**: Always prefix with `Run @` so the human knows exactly which agent to invoke. Agent names are defined in `AGENTS.md` — use the name exactly as listed there. Examples: -- `Run @software-engineer — implement @id:a1b2c3d4 (Step 3 RED)` -- `Run @system-architect — load skill architect and begin Step 2 (Architecture) for ` -- `Run @system-architect — verify feature at Step 4` -- `Run @product-owner — pick next BASELINED feature from backlog` -- `Run @product-owner — accept feature at Step 5` - ## Rules 1. Never skip reading `WORK.md` and `FLOW.md` at session start 2. Never end a session without updating `WORK.md` 3. Never leave uncommitted changes — commit as WIP if needed 4. One step per session where possible; do not start Step N+1 in the same session as Step N -5. The "Next" line must be actionable enough that a fresh AI can execute it without asking questions -6. When a step completes, update `WORK.md` and commit **before** any further work -7. The Session Log is append-only — never delete old entries -8. If `FLOW.md` is missing, create it from `.opencode/skills/flow/flow.md.template` before doing any other work -9. If detected state differs from `WORK.md` `@state`, trust the detected state and update `WORK.md`. **Never modify `FLOW.md`.** -10. Output is minimal-signal: findings, status, decisions, blockers, Next: line only. Use the fewest, least verbose tool calls necessary. Report results, not process. No redundant prose. +5. When a step completes, update `WORK.md` and commit **before** any further work +6. If `FLOW.md` is missing, create it from `.opencode/skills/flow/flow.md.template` before doing any other work +7. If detected state differs from `WORK.md` `@state`, trust the detected state and update `WORK.md`. **Never modify `FLOW.md`.** +8. Output is minimal-signal: findings, status, decisions, blockers only. Use the fewest, least verbose tool calls necessary. Report results, not process. No redundant prose. ## Output Style -Use minimal output. Every message must contain only what the next agent or stakeholder needs to continue — findings, status, decisions, blockers, and the Next: line. +Use minimal output. Every message must contain only what the next agent or stakeholder needs to continue — findings, status, decisions, blockers. - Use the fewest, least verbose tool calls necessary to achieve the step's goal - Report results, not process ("3 files changed" not "I ran git status and it showed...") - No narration before or after tool calls - No restating tool output in prose - No summaries of what was just done -- Always close with Next: diff --git a/.opencode/skills/select-feature/SKILL.md b/.opencode/skills/select-feature/SKILL.md index 432849c..872312a 100644 --- a/.opencode/skills/select-feature/SKILL.md +++ b/.opencode/skills/select-feature/SKILL.md @@ -86,7 +86,7 @@ If all BASELINED features have Dependency=1: stop and resolve the blocking depen mv docs/features/backlog/.feature docs/features/in-progress/.feature ``` -Update `WORK.md` — add (or replace) the active item block and append to Session Log: +Update `WORK.md` — add (or replace) the active item block: ```markdown ## Active Items @@ -94,12 +94,6 @@ Update `WORK.md` — add (or replace) the active item block and append to Sessio @id: @state: [STEP-1-DISCOVERY] or [STEP-2-READY] — whichever is next @branch: [NONE] - -## Session Log -**YYYY-MM-DD HH:MM** — product-owner — [IDLE] → [] — selected from backlog - -## Next -Run @ ``` - If the feature has no `Rule:` blocks yet → `@state: STEP-1-DISCOVERY`; `Run @product-owner — load skill define-scope and write stories` @@ -121,5 +115,5 @@ git commit -m "chore: select as next feature" - [ ] WSJF scores filled for all candidates - [ ] Selected feature has highest WSJF among Dependency=0 candidates - [ ] Feature moved to `in-progress/` -- [ ] `WORK.md` updated with correct `@state` and `Next:` line +- [ ] `WORK.md` updated with correct `@state` - [ ] Changes committed diff --git a/.opencode/skills/update-docs/SKILL.md b/.opencode/skills/update-docs/SKILL.md index c768978..afa786c 100644 --- a/.opencode/skills/update-docs/SKILL.md +++ b/.opencode/skills/update-docs/SKILL.md @@ -23,7 +23,7 @@ The glossary is a secondary artifact derived from the code, the domain entities | Document | Created/Updated by | Inputs read | |---|---|---| | `docs/system.md` (Context + Container sections) | SA at Step 2; `update-docs` skill (SA) updates these sections post-acceptance | `docs/discovery.md`, `docs/adr/ADR-*.md`, `docs/features/completed/` | -| `docs/glossary.md` | `update-docs` skill (SA) | `docs/system.md` (Domain Model section), `docs/glossary.md` (existing), `docs/adr/ADR-*.md`, `docs/features/completed/` | +| `docs/glossary.md` | PO only (Step 1, via `define-scope` skill) | — | | `docs/discovery.md` | PO only (Step 1) | — | **Never edit `docs/adr/ADR-*.md` or `docs/discovery.md` in this skill.** Those files are owned by their respective agents. This skill reads them; it never writes to them. @@ -63,7 +63,7 @@ Rules: - One `System_Ext(...)` per external dependency identified in ADR files - Relationships (`Rel`) use verb phrases from feature `When` clauses or architecture decision labels - If no external systems are identified in ADRs, omit `System_Ext` entries -- If the section already exists: update only — add new actors/systems, update relationship labels. Never remove an existing entry unless the feature it came from has been explicitly superseded +- If the section already exists: update only — add new actors/systems, update relationship labels. Never remove an existing entry unless the feature it came from has been explicitly replaced --- @@ -84,32 +84,18 @@ Rules: --- -## Step 4 — Update Living Glossary +## Step 4 — Read Glossary for Diagram Accuracy -File: `docs/glossary.md` +File: `docs/glossary.md` — **read-only in this skill** -The glossary answers: **what does each domain term mean in this project's context?** - -Use the template in `glossary.md.template` in this skill's directory. +The glossary is owned by the PO (via `define-scope` skill). This skill reads it to verify that diagram labels and entity names in the Context and Container sections match the canonical domain terms. ### Rules -- Extract all entities and verbs from the Domain Model section of `docs/system.md` -- Extract all roles from `As a ` clauses in completed `.feature` files -- Extract all external system names from ADR decisions -- Extract any term defined or clarified in architectural decision `Reason:` fields -- **Do not remove existing glossary entries** — if a term's meaning has changed, add a `**Superseded by:**` line pointing to the new entry and write a new entry -- **Every term must have a traceable source** — completed feature files or ADR decisions. If a term appears in sources but is never defined, write `Definition: Term appears in [source] but has not been explicitly defined.` Do not invent a definition. -- Terms are sorted alphabetically within the file - -### Merge with existing glossary - -If `docs/glossary.md` already exists: -1. Read all existing entries -2. For each new term found in sources: check if it already exists in the glossary - - Exists, definition unchanged → skip - - Exists, definition changed → append `**Superseded by:** ` to old entry; write new entry - - Does not exist → append new entry in alphabetical order +- Read `docs/glossary.md` if it exists +- Verify every actor name, container name, and relationship label in the Context and Container diagrams matches a term in the glossary or in the Domain Model section of `system.md` +- If a diagram label uses a term not found in either source, flag it as a potential inconsistency — do **not** add the term to the glossary; escalate to PO +- **Never write to `docs/glossary.md` in this skill** — if you identify a missing or incorrect glossary term, note it in the commit message or in a comment to the stakeholder and stop --- @@ -137,9 +123,8 @@ docs(update-docs): refresh context, container, and glossary - [ ] Context section in `system.md` reflects all actors from completed feature files - [ ] Context section in `system.md` reflects all external systems from ADR files - [ ] Container section in `system.md` present only if multi-container architecture confirmed in ADR files -- [ ] Glossary contains all entities and verbs from the Domain Model section of `docs/system.md` -- [ ] No existing glossary entry removed -- [ ] Every new term has a traceable source in completed feature files or ADRs; no term is invented +- [ ] Glossary read; all diagram labels verified against glossary and Domain Model section of `system.md` +- [ ] Any unrecognized diagram term flagged to PO — not added to glossary unilaterally - [ ] No edits made to ADR files or `docs/discovery.md` - [ ] If standalone: committed with `docs(update-docs): ...` message - [ ] If called from release: files staged but not committed (release process commits) @@ -152,14 +137,12 @@ All templates for diagrams written by this skill live in this skill's directory: - `context.md.template` — Context diagram body (inserted into `## Context` section of `system.md`) - `container.md.template` — Container diagram body (inserted into `## Container` section of `system.md`) -- `glossary.md.template` — `docs/glossary.md` entry format -Base directory for this skill: file:///home/user/Documents/projects/python-project-template/.opencode/skills/update-docs +Base directory for this skill: `.opencode/skills/update-docs/` Relative paths in this skill (e.g., scripts/, reference/) are relative to this base directory. Note: file list is sampled. -/home/user/Documents/projects/python-project-template/.opencode/skills/update-docs/container.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/update-docs/context.md.template -/home/user/Documents/projects/python-project-template/.opencode/skills/update-docs/glossary.md.template +.opencode/skills/update-docs/container.md.template +.opencode/skills/update-docs/context.md.template diff --git a/.opencode/skills/update-docs/glossary.md.template b/.opencode/skills/update-docs/glossary.md.template deleted file mode 100644 index 32676e5..0000000 --- a/.opencode/skills/update-docs/glossary.md.template +++ /dev/null @@ -1,18 +0,0 @@ -# Glossary — - -> Living document. Updated after each completed feature by the `update-docs` skill. -> Source: docs/discovery.md, docs/features/completed/, docs/adr/ADR-*.md - ---- - -## - -**Type:** Noun | Verb | Domain Event | Concept | Role | External System - -**Definition:** - -**Bounded context:** - -**First appeared:** - ---- diff --git a/.opencode/skills/verify/SKILL.md b/.opencode/skills/verify/SKILL.md index d5b7de7..c95ad47 100644 --- a/.opencode/skills/verify/SKILL.md +++ b/.opencode/skills/verify/SKILL.md @@ -15,7 +15,7 @@ This skill guides the system-architect through Step 4: adversarial verification **Every PASS/FAIL cell must have evidence.** Empty evidence = UNCHECKED = REJECTED. -**You never move, create, or edit `.feature` files.** After producing an APPROVED report: update FLOW.md `Next:` to `Run @product-owner — accept feature at Step 5.` then stop. The PO accepts the feature and moves the file. +**You never move, create, or edit `.feature` files.** After producing an APPROVED report: update `WORK.md` `@state` to `STEP-5-READY` then stop. The PO accepts the feature and moves the file. The system-architect produces one written report (see template below) that includes: all gate results, the SE Self-Declaration Audit, the **Architect Review Stance Declaration**, and the final APPROVED/REJECTED verdict. Do not start until the software-engineer has committed all work and communicated the Self-Declaration verbally in the handoff message. @@ -88,7 +88,7 @@ Run before semantic review. If any row is FAIL, stop immediately with REJECTED. ### 6. Self-Declaration Audit -**Completeness check (hard gate — REJECT if failed)**: Count the numbered items in the SE's Self-Declaration. The template in `implement/SKILL.md` has exactly 25 items numbered 1–25. If the count is not 25, or any number in the sequence 1–25 is missing, REJECT immediately — do not proceed to item-level audit. +**Completeness check (hard gate — REJECT if failed)**: Verify that every claim in the SE's Self-Declaration is present and numbered. If any claim is missing, or the declaration is empty, REJECT immediately — do not proceed to item-level audit. Read the software-engineer's Self-Declaration from the handoff message. @@ -227,7 +227,7 @@ Record what input was given and what output was observed. |---------|--------|-------| | uv run task lint | PASS / FAIL | | | uv run task static-check | PASS / FAIL | | -| uv run task test | PASS / FAIL | | +| uv run task test-coverage | PASS / FAIL | | ### Naming Consistency | Check | Result | Notes | diff --git a/AGENTS.md b/AGENTS.md index 7229c46..0bb6402 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -61,7 +61,7 @@ All feature work happens on branches. `main` is the single source of truth and r | `backlog/` → `in-progress/` | PO only | Before Step 2 begins; only if `Status: BASELINED` | | `in-progress/` → `completed/` | PO only | After Step 5 acceptance | -**If an agent (SE or SA) finds no `.feature` in `in-progress/`**: update `WORK.md` with the correct `Next:` escalation line and stop. Never self-select a backlog feature. +**If an agent (SE or SA) finds no `.feature` in `in-progress/`**: update `WORK.md` `@state` to `[IDLE]` and stop. Never self-select a backlog feature. ## Agents @@ -180,7 +180,7 @@ tests/ _test.py ← software-engineer-authored extras (no @id traceability) FLOW.md ← static workflow state machine (roles, prerequisites, states, transitions) -WORK.md ← dynamic work tracker (active items with @id, @state, @branch + session log) +WORK.md ← dynamic work tracker (active items with @id, @state, @branch) ``` Tests in `tests/unit/` are software-engineer-authored extras not covered by any `@id` criterion. Any test style is valid — plain `assert` or Hypothesis `@given`. Use Hypothesis when the test covers a **property** that holds across many inputs (mathematical invariants, parsing contracts, value object constraints). Use plain pytest for specific behaviors or single edge cases discovered during refactoring. @@ -209,7 +209,7 @@ def test__<@id>() -> None: ### Markers - `@pytest.mark.slow` — takes > 50ms; applied to Hypothesis tests and any test with I/O, network, or DB -- `@pytest.mark.deprecated` — auto-skipped by pytest-beehave; used for superseded Examples +- `@pytest.mark.deprecated` — auto-skipped by pytest-beehave; used for replaced Examples ## Development Commands @@ -247,7 +247,7 @@ uv run task doc-build - **Principles (in priority order)**: YAGNI > KISS > DRY > SOLID > Object Calisthenics > appropriate design patterns > complex code > complicate code > failing code > no code - **Linting**: ruff format, ruff check, Google docstring convention, `noqa` forbidden - **Type checking**: pyright, 0 errors required -- **Coverage**: 100% (measured against your actual package) +- **Coverage**: enforced by `test-coverage` - **Function length**: ≤ 20 lines (code lines only, excluding docstrings) - **Class length**: ≤ 50 lines (code lines only, excluding docstrings) - **Max nesting**: 2 levels @@ -288,7 +288,7 @@ The stakeholder initiates the release process. When the stakeholder requests a r Every session: load `skill run-session`. Read `FLOW.md` and `WORK.md` at session start; update `WORK.md` at the end. - `FLOW.md` — static state machine: roles, prerequisites, states, transitions, detection rules. **Agents never modify this file.** Only the stakeholder (human) may change it, using `skill flow` as the design protocol. -- `WORK.md` — dynamic tracker: active `@id` items with `@state` and `@branch`. Updated by the state owner at every transition. Session Log is append-only. +- `WORK.md` — dynamic tracker: active `@id` items with `@state` and `@branch`. Updated by the state owner at every transition. See `.opencode/skills/flow/SKILL.md` for the generic flow protocol, state machine design principles, and templates for creating new workflows. diff --git a/FLOW.md b/FLOW.md index e5ca1fa..84cd3ea 100644 --- a/FLOW.md +++ b/FLOW.md @@ -57,37 +57,37 @@ All must be satisfied before starting any session. If any are missing, stop and States are checked **in order**. The first matching condition is the current state. -``` -[IDLE] ──► [STEP-1-BACKLOG-CRITERIA] (Stage 2 on backlog files — no WIP slot needed) - -[IDLE] ──► [STEP-1-DISCOVERY] ──► [STEP-1-STORIES] ──► [STEP-1-CRITERIA] - │ - ▼ -[POST-MORTEM] ◄──────────────────────────────────── [STEP-2-READY] - │ │ - │ ▼ - └──────────────────────────────────────────────► [STEP-2-ARCH] - │ - ▼ - [STEP-3-WORKING] - │ - ▼ - [STEP-3-RED] - │ - ▼ - [STEP-4-READY] - │ - ▼ - [STEP-5-READY] - │ - ▼ - [STEP-5-MERGE] - │ - ▼ - [STEP-5-COMPLETE] - │ - ▼ - [IDLE] +```mermaid +stateDiagram-v2 + [*] --> IDLE + + IDLE --> STEP-1-BACKLOG-CRITERIA + IDLE --> STEP-1-DISCOVERY + + STEP-1-BACKLOG-CRITERIA --> IDLE + STEP-1-DISCOVERY --> STEP-1-STORIES + STEP-1-STORIES --> STEP-1-CRITERIA + STEP-1-CRITERIA --> STEP-2-READY + + STEP-2-READY --> STEP-2-ARCH + STEP-2-ARCH --> STEP-3-WORKING + STEP-2-ARCH --> STEP-1-CRITERIA : failure + + STEP-3-WORKING --> STEP-3-RED + STEP-3-RED --> STEP-3-WORKING + STEP-3-WORKING --> STEP-4-READY + + STEP-4-READY --> STEP-5-READY + STEP-4-READY --> STEP-3-WORKING : failure + + STEP-5-READY --> STEP-5-MERGE + STEP-5-READY --> POST-MORTEM : failure + + POST-MORTEM --> STEP-2-ARCH + + STEP-5-MERGE --> STEP-5-COMPLETE + STEP-5-COMPLETE --> IDLE + STEP-5-COMPLETE --> [*] ``` ### Detection Rules (evaluated in order) @@ -171,7 +171,7 @@ States are checked **in order**. The first matching condition is the current sta **Entry condition**: On `@branch`, no test stubs in `tests/features//` **Action**: Read feature; design domain stubs; write ADRs; update `system.md` (domain model + Context + Container sections); run `uv run task test-fast` to generate stubs **Exit**: Stubs generated → update `@state: STEP-3-WORKING` in `WORK.md` -**Failure**: Spec unclear → escalate to `product-owner`; update `@state: STEP-1-CRITERIA` in `WORK.md`; document the gap in `WORK.md` `Next:` line +**Failure**: Spec unclear → escalate to `product-owner`; update `@state: STEP-1-CRITERIA` in `WORK.md`; document the gap for the PO **Commit**: `feat(arch): design @id architecture` --- @@ -204,7 +204,7 @@ States are checked **in order**. The first matching condition is the current sta **Entry condition**: All tests implemented (no `@skip`) and passing **Action**: Run all quality checks; semantic review against acceptance criteria **Exit**: All checks pass → update `@state: STEP-5-READY` in `WORK.md` -**Failure**: Issues found → update `@state: STEP-3-WORKING` in `WORK.md`; document issues in `WORK.md` `Next:` line +**Failure**: Issues found → update `@state: STEP-3-WORKING` in `WORK.md`; document issues for the SE --- @@ -252,7 +252,7 @@ States are checked **in order**. The first matching condition is the current sta 6. Run `git status` and `git branch --show-current` to confirm workspace matches `@branch` ### Session End -1. Update `WORK.md`: set `@state` to the new state; append to Session Log +1. Update `WORK.md`: set `@state` to the new state 2. Commit any uncommitted work: ```bash git add -A && git commit -m "WIP(@id): " @@ -271,48 +271,25 @@ git add WORK.md && git commit -m "chore: @id transition to @state" ## Auto-Detection Commands -Run in order; first matching condition determines the state. +Run these three checks to verify workspace consistency. Finer-grained state +verification happens during normal agent session work (reading the `.feature` +file, running tests, etc.). ```bash -# 0. Check for STEP-1-BACKLOG-CRITERIA: no in-progress file AND backlog has BASELINED features without @id -NO_INPROGRESS=$(ls docs/features/in-progress/*.feature 2>/dev/null | grep -v ".gitkeep" | wc -l) -HAS_BASELINED_WITHOUT_IDS=$(grep -rl "Status: BASELINED" docs/features/backlog/ 2>/dev/null | xargs grep -L "@id:" 2>/dev/null | wc -l) -# If NO_INPROGRESS=0 AND HAS_BASELINED_WITHOUT_IDS>0 → [STEP-1-BACKLOG-CRITERIA] - -# 1. Check for in-progress feature +# 1. Is there a feature in progress? ls docs/features/in-progress/*.feature 2>/dev/null | grep -v ".gitkeep" -# 2. Check feature baselined -grep -q "Status: BASELINED" docs/features/in-progress/*.feature - -# 3. Check for Rule blocks -grep -q "^ Rule:" docs/features/in-progress/*.feature - -# 4. Check for Example blocks with @id -grep -q "@id:" docs/features/in-progress/*.feature - -# 5. Check for feature branch -git branch --show-current | grep -E "^feat/|^fix/" +# 2. Are we on the correct branch? +git branch --show-current -# 6. Check for test stubs +# 3. Do test stubs exist when they should? ls tests/features/*/ 2>/dev/null | head -1 - -# 7. Check for skipped tests -grep -r "@pytest.mark.skip" tests/features/*/ - -# 8. Check test failures -uv run task test-fast 2>&1 | grep -E "FAILED|ERROR" - -# 9. Check WORK.md @state for STEP-5-READY (must evaluate before rule 12 / test-pass check) -grep "@state:" WORK.md | grep -q "STEP-5-READY" ``` --- ## Output Style -Every agent session must close with a `Next:` line — one concrete action, enough for a fresh agent to continue without questions. - - Report results, not process - No narration around tool calls - No restating tool output in prose diff --git a/README.md b/README.md index b3ea66f..8720d4a 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ Every artifact is version-controlled alongside the code that implements it. **Behavioral tests only** — Tests describe observable contracts, not implementation internals. A test that survives a complete internal rewrite is a good test. A test that breaks on refactoring is a liability. -**100% coverage** — Measured against your package. No untested paths ship. Coverage is a floor, not a goal. +**Coverage enforced** — Measured against your package. Threshold is defined in `pyproject.toml`. No untested paths ship. Coverage is a floor, not a goal. **Design principles enforced** — YAGNI, KISS, DRY, SOLID, and Object Calisthenics are not guidelines — they are review gates. Every principle is checked with file and line evidence before a feature is approved. diff --git a/WORK.md b/WORK.md index 69ed5ca..31a661a 100644 --- a/WORK.md +++ b/WORK.md @@ -15,29 +15,5 @@ Each item carries exactly the variables defined by `FLOW.md`: - @id: cli-entrypoint @state: STEP-2-READY - @branch: [NONE — SA creates feat/cli-entrypoint at Step 2 start] - ---- - -## Session Log - -### 2026-04-22 — Session 1 (Discovery) - -- Resumed interrupted Stage 1 discovery session (Q1–Q8 were pre-captured). -- Completed Block B cross-cutting questions (Q9–Q11): confirmed scope is one demonstration feature. -- Completed Block C feature discovery: stakeholder chose CLI entrypoint (`--help` + `--version`) as the demonstration feature. -- Created `docs/features/backlog/cli-entrypoint.feature` — Status: BASELINED (2026-04-22). -- Created `docs/features/in-progress/` and `docs/features/completed/` directories. -- Created `docs/glossary.md`, `docs/discovery.md`. -- Marked `docs/scope_journal.md` Session 1 as COMPLETE. - -**Next:** Run `@system-architect` — begin Step 2 (ARCH) for `cli-entrypoint`. PO must first move `docs/features/backlog/cli-entrypoint.feature` → `docs/features/in-progress/cli-entrypoint.feature`. - -### 2026-04-22 — PO: Move cli-entrypoint to in-progress - -- Stakeholder confirmed: move `cli-entrypoint.feature` to `in-progress/`. -- Moved `docs/features/backlog/cli-entrypoint.feature` → `docs/features/in-progress/cli-entrypoint.feature`. -- Updated WORK.md: `@state: STEP-2-READY`. - -**Next:** Run `@system-architect` — load skill `architect` and begin Step 2 (Architecture) for `cli-entrypoint`. + @branch: [NONE] diff --git a/app/__main__.py b/app/__main__.py index a200610..e69de29 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -1,24 +0,0 @@ -"""Entry point for running the application as a module.""" - -import logging - -import fire - -logger = logging.getLogger(__name__) - - -def main(verbosity: str = "INFO") -> None: - """Run the application. - - Args: - verbosity: Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL). - """ - logging.basicConfig( - level=getattr(logging, verbosity.upper(), logging.INFO), - format="%(levelname)s - %(name)s: %(message)s", - ) - logger.info("Ready.") - - -if __name__ == "__main__": - fire.Fire(main) diff --git a/docs/adr/ADR-2026-04-22-verbosity-validation.md b/docs/adr/ADR-2026-04-22-verbosity-validation.md deleted file mode 100644 index 9a881b9..0000000 --- a/docs/adr/ADR-2026-04-22-verbosity-validation.md +++ /dev/null @@ -1,26 +0,0 @@ -# ADR-2026-04-22 — verbosity-validation - -**Status:** Accepted -**Date:** 2026-04-22 -**Author:** system-architect -**Feature:** display-version - ---- - -## Context - -`main()` accepts a `verbosity` string from the CLI. Python's `logging.basicConfig` silently falls back to `WARNING` when given an unrecognised level name, producing no error and potentially confusing users who mistype a level. The caller needs a fast, descriptive failure rather than silent misconfiguration. - -## Decision - -We will validate `verbosity` against the closed set `{DEBUG, INFO, WARNING, ERROR, CRITICAL}` before calling `logging.basicConfig`. An invalid value raises `ValueError` with a message that names the invalid input and lists all valid options. The type alias `ValidVerbosity` documents this constraint in the type system. - -## Consequences - -- **Positive:** Misconfigured verbosity fails loudly at startup; the error message is self-documenting. -- **Negative:** Custom log level names (registered via `logging.addLevelName`) are not accepted; this is intentional — the template targets standard usage. -- **Neutral:** The validation logic is a single guard clause; it adds no meaningful complexity. - ---- - -> ADRs are append-only. To revise a decision, create a new ADR and set the Status of this one to "Superseded by ADR-YYYY-MM-DD-". diff --git a/docs/adr/ADR-2026-04-22-version-source.md b/docs/adr/ADR-2026-04-22-version-source.md deleted file mode 100644 index eaa2d50..0000000 --- a/docs/adr/ADR-2026-04-22-version-source.md +++ /dev/null @@ -1,26 +0,0 @@ -# ADR-2026-04-22 — version-source - -**Status:** Accepted -**Date:** 2026-04-22 -**Author:** system-architect -**Feature:** display-version - ---- - -## Context - -The application needs to expose its own version at runtime. Two options exist: hardcode a `__version__` constant in the source tree, or read the version from `pyproject.toml` — the file that already serves as the project's authoritative metadata record. Duplicating the version introduces drift risk; a single source of truth eliminates it. Python 3.11+ ships `tomllib` in the standard library, so no additional dependency is required. - -## Decision - -We will read the version from `pyproject.toml` at runtime using `tomllib`. No `__version__` constant will be defined anywhere in the package. The `pyproject.toml` `[project] version` field is the single source of truth. - -## Consequences - -- **Positive:** Version is never out of sync between the package and its metadata; no extra release step to update a constant. -- **Negative:** `version()` performs a file read on every call; this is acceptable for a CLI tool but would be a concern in a hot path. -- **Neutral:** Requires Python ≥ 3.11 (tomllib). This matches the project's `requires-python = ">=3.13"` constraint. - ---- - -> ADRs are append-only. To revise a decision, create a new ADR and set the Status of this one to "Superseded by ADR-YYYY-MM-DD-". diff --git a/docs/branding.md b/docs/branding.md index 94ddc03..63afde0 100644 --- a/docs/branding.md +++ b/docs/branding.md @@ -47,15 +47,6 @@ Warm parchment `#faf7f2` background. Left zone: temple mark (same geometry as lo Every word carries weight. The Greeks had a name for ornament that obscures meaning: *kenophonia* — empty noise. -- **Avoid:** `easy`, `simple`, `just`, `quick`, `scaffold` — these words undermine engineer credibility or imply the work is trivial. A temple is not a scaffold. -- **Prefer:** `minimal`, `precise`, `production-ready`, `zero-boilerplate`, `rigorous`, `from zero to hero` +- **Avoid:** `easy`, `simple`, `just`, `quick`, `scaffold`, `superseded` — these words undermine engineer credibility or imply the work is trivial. A temple is not a scaffold. +- **Prefer:** `minimal`, `precise`, `production-ready`, `zero-boilerplate`, `rigorous`, `replaces`, `from zero to hero` -## Project Summary - -A Python project template with a production-ready AI-assisted delivery workflow. -Ships with quality tooling (ruff, pyright, pytest, hypothesis), Gherkin-driven -acceptance criteria, and five specialised AI agents covering scope through release. -Built on the premise that rigorous method, applied from the beginning, produces -something worth building on. Use this summary in banners, release notes, and document headers. - -(End of file - total 59 lines) \ No newline at end of file diff --git a/docs/features/backlog/.gitkeep b/docs/features/backlog/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/docs/features/completed/display-version.feature b/docs/features/completed/display-version.feature deleted file mode 100644 index 0dfc3dd..0000000 --- a/docs/features/completed/display-version.feature +++ /dev/null @@ -1,60 +0,0 @@ -Feature: Display version - - Reads the application version from pyproject.toml at runtime and logs it at INFO - level. Log output is controlled by a verbosity parameter; the version is visible - at DEBUG and INFO but suppressed at WARNING and above. An invalid verbosity value - raises a descriptive error. - - Status: COMPLETED - - Rules (Business): - - Version is read from pyproject.toml at runtime using tomllib - - Log verbosity is controlled by a ValidVerbosity parameter passed to main() - - Valid verbosity levels are: DEBUG, INFO, WARNING, ERROR, CRITICAL - - An invalid verbosity value raises a ValueError with the invalid value and valid options - - The version string is logged at INFO level; visible at DEBUG and INFO, not at WARNING+ - - Constraints: - - No hardcoded __version__ constant — pyproject.toml is the single source of truth - - Entry point: app/__main__.py (main(verbosity) function) - - Version logic: app/version.py (version() function) - - Rule: Version retrieval - As a software-engineer - I want to retrieve the application version programmatically - So that I can display or log it at runtime - - @id:3f2a1b4c - Example: Version string is read from pyproject.toml - Given pyproject.toml exists with a version field - When version() is called - Then the returned string matches the version in pyproject.toml - - @id:7a8b9c0d - Example: Version call emits an INFO log message - Given pyproject.toml exists with a version field - When version() is called - Then an INFO log message in the format "Version: " is emitted - - Rule: Verbosity control - As a software-engineer - I want to control log verbosity via a parameter - So that I can tune output for different environments - - @id:a1b2c3d4 - Example: Version appears in logs at DEBUG and INFO verbosity - Given a verbosity level of DEBUG or INFO is passed to main() - When main() is called - Then the version string appears in the log output - - @id:b2c3d4e5 - Example: Version is absent from logs at WARNING and above - Given a verbosity level of WARNING, ERROR, or CRITICAL is passed to main() - When main() is called - Then the version string does not appear in the log output - - @id:e5f6a7b8 - Example: Invalid verbosity raises a descriptive error - Given an invalid verbosity string is passed to main() - When main() is called - Then a ValueError is raised with the invalid value and valid options listed diff --git a/docs/research/domain-modeling.md b/docs/research/domain-modeling.md index 2b550e7..d632497 100644 --- a/docs/research/domain-modeling.md +++ b/docs/research/domain-modeling.md @@ -28,7 +28,7 @@ Foundations for bounded context identification, ubiquitous language, and feature | **Status** | Confirmed — freely available CC-BY canonical summary; maintained by Evans personally | | **Core finding** | The open-access pattern summary of all DDD patterns from the 2003 book. More precisely citable than the book for specific pattern definitions. Key patterns: Ubiquitous Language ("Use the model as the backbone of a language. Commit the team to exercising that language relentlessly in all communication within the team and in the code."), Bounded Context, Context Map, Domain Events, Aggregates, Repositories. | | **Mechanism** | Each pattern is described with: intent, prescription, and "therefore" consequences. The Ubiquitous Language pattern prescribes: use the same terms in diagrams, writing, and especially speech. Refactor the code when the language changes. Resolve confusion over terms in conversation, the way confusion over ordinary words is resolved — by agreement and precision. | -| **Where used** | Primary reference for `docs/domain-model.md` structure and the ubiquitous language practice. `update-docs` skill glossary entries derive from this: terms must match code identifiers (Evans' "use the same language in code" prescription). `docs/research/domain-modeling.md`. | +| **Where used** | Primary reference for `docs/domain-model.md` structure and the ubiquitous language practice. `update-docs` skill glossary entries derive from this: terms must match code identifiers (Evans' "use the same language in code" prescription). `docs/research/domain-modeling.md`. `define-scope/glossary.md.template` — the living glossary format and entry structure. | | **Note** | Supersedes entry #31 as the citable source for specific pattern quotes. Entry #31 remains as the book reference. Use this entry when citing a specific Evans pattern definition. | --- @@ -103,6 +103,20 @@ Foundations for bounded context identification, ubiquitous language, and feature --- +### 69. ISO 704 — Terminology Work Principles and Methods + +| | | +|---|---| +| **Source** | ISO. (2022). *ISO 704:2022 — Terminology work — Principles and methods*. International Organization for Standardization. https://www.iso.org/standard/79077.html | +| **Date** | 2022 (first edition 1987; current edition 2022) | +| **Alternative** | ISO 1087:2019 — Terminology work and terminology science: vocabulary (the companion vocabulary standard) | +| **Status** | Not directly verified — paywalled standard (~CHF 158). Content described here is drawn from the publicly available ISO preview, ISO TC 37 documentation, and widely cited secondary sources in terminology science literature. Core principles (genus + differentia, monosemy, consistency) are uncontested across secondary sources and have been stable since the 1987 first edition. | +| **Core finding** | A definition should identify the concept by stating (1) a **genus** — the broader category the concept belongs to — and (2) a **differentia** — the features that distinguish it from all other concepts in the same genus. This produces a one-sentence definition that is internally consistent, non-circular, and sufficient. Definitions should avoid negation ("what it is not") and synonymy ("same as X") as primary definition strategies. | +| **Mechanism** | Genus + differentia: "A [genus] that [differentia]." Example: "A Bounded Context is a [boundary within a domain model] that [enforces consistency of a single Ubiquitous Language]." The genus locates the term in a category the reader already knows; the differentia narrows it to this specific concept. | +| **Where used** | `define-scope/glossary.md.template` — the `**Definition:**` field format. The genus + differentia pattern is the prescribed sentence structure for all glossary entries. Not cited by name in the template — Evans DDD is the headline practice; ISO 704 is the definition *format* underlying it. `docs/research/domain-modeling.md`. | + +--- + ## Bibliography 1. Context Mapper. (2025). Rapid Object-Oriented Analysis and Design. https://contextmapper.org/docs/rapid-ooad @@ -113,3 +127,4 @@ Foundations for bounded context identification, ubiquitous language, and feature 6. Fowler, M. (2014). BoundedContext. martinfowler.com. https://martinfowler.com/bliki/BoundedContext.html 7. Vernon, V. (2013). *Implementing Domain-Driven Design*. Addison-Wesley. 8. Verraes, M. (2013). Ubiquitous Language Is Not a Glossary. verraes.net (archived). https://web.archive.org/web/20131004/https://verraes.net/2013/04/ubiquitous-language-is-not-a-glossary/ +9. ISO. (2022). *ISO 704:2022 — Terminology work — Principles and methods*. International Organization for Standardization. https://www.iso.org/standard/79077.html diff --git a/docs/system.md b/docs/system.md deleted file mode 100644 index 3b9eebd..0000000 --- a/docs/system.md +++ /dev/null @@ -1,151 +0,0 @@ -# System Overview: temple8 - -> Last updated: 2026-04-22 — display-version - -**Purpose:** Provide a production-ready Python project template that eliminates setup boilerplate so engineers can ship features immediately. - ---- - -## Summary - -temple8 is a Python project template. Engineers clone it and run a five-step AI-assisted delivery workflow — Scope → Arch → TDD Loop → Verify → Accept — to ship features with quality gates from day one. The template ships with one working demonstration feature (`display-version`) that exercises the full stack end-to-end: it reads the application version from `pyproject.toml` at runtime via `tomllib`, logs it at INFO level, and gates visibility on a `ValidVerbosity` parameter. Quality tooling (ruff, pyright, pytest, hypothesis) and CI are preconfigured; no setup required beyond cloning. - ---- - -## Actors - -| Actor | Needs | -|-------|-------| -| Engineer | Clones the template; runs `python -m app` to verify the installed version and control log verbosity; ships features using the built-in workflow | -| CI Pipeline | Imports the package; runs the full test suite, lint, and type-check on every push | - ---- - -## Structure - -| Module | Responsibility | -|--------|----------------| -| `app/__main__.py` | CLI entry point; accepts `--verbosity` flag; validates it and delegates to `version()` | -| `app/version.py` | Reads `pyproject.toml` via `tomllib`; logs and returns the version string | - ---- - -## Key Decisions - -- Version is read from `pyproject.toml` at runtime via `tomllib`; no hardcoded `__version__` constant. (see `ADR-2026-04-22-version-source`) -- Log verbosity is validated against the five standard Python log levels before use; invalid values raise `ValueError`. (see `ADR-2026-04-22-verbosity-validation`) - ---- - -## External Dependencies - -| Dependency | What it provides | Why not replaced | -|------------|------------------|-----------------| -| `fire` | CLI argument parsing from function signatures | Zero boilerplate; consistent with template philosophy | -| `tomllib` (stdlib, Python ≥ 3.11) | TOML parsing for `pyproject.toml` | Standard library; no extra dependency needed | - ---- - -## Active Constraints - -- `pyproject.toml` is the single source of truth for the version string; never duplicate it. -- `main()` must accept `verbosity` as its only parameter; no global state. -- All new modules must achieve 100% test coverage before merging. - ---- - -## Domain Model - -### Bounded Contexts - -| Context | Responsibility | Key Modules | -|---------|----------------|-------------| -| **Version** | Read the project version and emit a log message | `app/version.py` | -| **CLI** | Parse CLI arguments; validate verbosity; compose entry point | `app/__main__.py` | - -### Entities - -| Name | Type | Description | Bounded Context | -|------|------|-------------|-----------------| -| `Version` | Value Object | The semver string (`MAJOR.MINOR.YYYYMMDD`) read from `pyproject.toml` at runtime via `tomllib`. Never duplicated as a source-code constant. | Version | -| `ValidVerbosity` | Value Object | A string drawn from the closed set `{DEBUG, INFO, WARNING, ERROR, CRITICAL}`. Any other value is invalid and raises `ValueError`. | CLI | - -### Actions - -| Name | Actor | Object | Description | -|------|-------|--------|-------------| -| `version()` | version module | `pyproject.toml` → `Version` | Reads `pyproject.toml`, emits an INFO log in the format `"Version: "`, and returns the version string | -| `main(verbosity)` | CLI entry point | `ValidVerbosity` → None | Validates verbosity, configures the root logger, then calls `version()`. Raises `ValueError` on invalid verbosity | - -### Relationships - -| Subject | Relation | Object | Cardinality | Notes | -|---------|----------|--------|-------------|-------| -| `main()` | validates-and-calls | `version()` | 1:1 | Verbosity guard runs before version read | -| `version()` | reads | `pyproject.toml` | 1:1 | Single file read per call; no caching | -| `ValidVerbosity` | constrains | `main()` | 1:1 | Only valid level names accepted | - ---- - -## Context - -```mermaid -C4Context - title System Context — temple8 - - Person(engineer, "Engineer", "Clones the template; runs app to verify version; ships features via workflow") - Person(ci, "CI Pipeline", "Imports the package; runs full test suite, lint, type-check on every push") - - System(temple8, "temple8", "Production-ready Python project template with AI-assisted five-step delivery workflow") - - System_Ext(pyproject, "pyproject.toml", "Single source of truth for project version and metadata") - System_Ext(github, "GitHub Actions", "Runs quality gates on every push via .github/workflows/ci.yml") - - Rel(engineer, temple8, "Runs", "CLI: python -m app [--verbosity LEVEL]") - Rel(ci, temple8, "Imports and tests", "pytest / pyright / ruff") - Rel(temple8, pyproject, "Reads version at runtime", "tomllib (stdlib)") - Rel(github, temple8, "Executes quality gates", "uv run task lint / test / static-check") -``` - ---- - -## Container - -```mermaid -C4Container - title Container Diagram — temple8 - - Person(engineer, "Engineer", "") - Person(ci, "CI Pipeline", "") - - System_Boundary(temple8_sys, "temple8") { - Container(cli, "CLI Entry Point", "Python / fire", "app/__main__.py — accepts --verbosity, validates it against ValidVerbosity, calls version().") - Container(version_mod, "Version Module", "Python / tomllib", "app/version.py — reads pyproject.toml, emits INFO log, returns version string.") - } - - System_Ext(pyproject, "pyproject.toml", "Project version and metadata") - System_Ext(github, "GitHub Actions", "CI pipeline") - - Rel(engineer, cli, "runs", "CLI: python -m app [--verbosity LEVEL]") - Rel(ci, cli, "imports and tests", "pytest / pyright / ruff") - Rel(cli, version_mod, "calls version()") - Rel(version_mod, pyproject, "reads [project] version", "tomllib / filesystem") - Rel(github, cli, "executes quality gates", "uv run task lint / test / static-check") -``` - ---- - -## ADR Index - -| ADR | Decision | -|-----|----------| -| [ADR-2026-04-22-version-source](adr/ADR-2026-04-22-version-source.md) | Read version from `pyproject.toml` via `tomllib` at runtime; no hardcoded constant | -| [ADR-2026-04-22-verbosity-validation](adr/ADR-2026-04-22-verbosity-validation.md) | Validate verbosity against a closed set; raise `ValueError` on invalid input | - ---- - -## Completed Features - -| Feature | Description | -|---------|-------------| -| `display-version` | Reads version from `pyproject.toml` at runtime and logs it; verbosity controls log visibility | diff --git a/pyproject.toml b/pyproject.toml index 3cb1797..53d5c6a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,9 +11,7 @@ authors = [ maintainers = [ { name = "eol", email = "nullhack@users.noreply.github.com" } ] -dependencies = [ - "fire>=0.7.1", -] +dependencies = [] [project.urls] Repository = "https://github.com/nullhack/temple8" @@ -74,7 +72,7 @@ pydocstyle.convention = "google" [tool.ruff.lint.per-file-ignores] "tests/**" = ["S101", "ANN", "D205", "D212", "D415", "D100", "D103"] -".opencode/skills/**/scripts/*.py" = ["T20"] +"scripts/*.py" = ["T20"] [tool.pytest.ini_options] minversion = "6.0" @@ -95,6 +93,7 @@ python_functions = ["test_*"] render_collapsed = "all" [tool.coverage.report] +fail_under = 100 exclude_lines = [ "pragma: no cover", "def __repr__", @@ -111,7 +110,6 @@ test-coverage = """\ pytest \ --cov-config=pyproject.toml \ --cov=app \ - --cov-fail-under=100 \ --tb=no """ test-build = """\ @@ -122,7 +120,6 @@ pytest \ --cov-report html:docs/coverage \ --cov-report term:skip-covered \ --cov=app \ - --cov-fail-under=100 \ --hypothesis-show-statistics \ --html=docs/tests/report.html \ --self-contained-html \ @@ -147,6 +144,7 @@ pytest \ """ doc-publish = "task doc-build && ghp-import -n -p -f docs" static-check = "pyright" +release-check = "python scripts/check_version.py && task lint && task static-check && task test && task doc-build" [dependency-groups] dev = [ diff --git a/scripts/check_adrs.py b/scripts/check_adrs.py new file mode 100644 index 0000000..c0c164f --- /dev/null +++ b/scripts/check_adrs.py @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +"""Validate ADR files: naming convention and required sections.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +REQUIRED_SECTIONS = { + "## Context", + "## Decision", + "## Reason", + "## Alternatives Considered", + "## Consequences", +} + + +def _adr_files(project_root: Path) -> list[Path]: + """List all .md files in docs/adr/.""" + adr_dir = project_root / "docs" / "adr" + if not adr_dir.exists(): + return [] + return sorted(f for f in adr_dir.iterdir() if f.suffix == ".md") + + +def validate_adrs(project_root: Path) -> tuple[bool, list[str]]: + """Validate all ADR files; return (ok, errors).""" + files = _adr_files(project_root) + if not files: + return True, ["no ADR files found (skipping)"] + + errors = [] + for f in files: + if not re.match( + r"ADR-\d{4}-\d{2}-\d{2}-[a-z0-9-]+\.md$", f.name + ): + errors.append( + f"{f.name}: invalid naming (expected ADR-YYYY-MM-DD-.md)" + ) + + text = f.read_text() + for section in REQUIRED_SECTIONS: + if section not in text: + errors.append(f"{f.name}: missing {section}") + + return not errors, errors + + +def main() -> int: + """Run ADR validation.""" + project_root = Path(__file__).resolve().parent.parent + ok, errors = validate_adrs(project_root) + for err in errors: + print(f"ERROR: {err}") + if ok and not any("no ADR files" in e for e in errors): + print("OK: all ADR files are valid") + return 0 if ok else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_commit_messages.py b/scripts/check_commit_messages.py new file mode 100644 index 0000000..8edd392 --- /dev/null +++ b/scripts/check_commit_messages.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +"""Validate commit messages on the current branch against conventional commits.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +CONVENTIONAL_RE = re.compile( + r"^(feat|fix|test|refactor|chore|docs|perf|ci)" + r"(\([^)]+\))?: .+" +) + +FORBIDDEN = re.compile( + r"^(wip|temp|fix tests|oops|asdf|xxx|qwerty|test)", + re.IGNORECASE, +) + + +def _current_branch(project_root: Path) -> str: + """Return the current git branch name by reading .git/HEAD.""" + git_head = project_root / ".git" / "HEAD" + if not git_head.exists(): + return "" + content = git_head.read_text().strip() + if content.startswith("ref: refs/heads/"): + return content[len("ref: refs/heads/"):] + return "" + + +def _main_sha(project_root: Path) -> str: + """Return the current SHA of main by reading .git/refs/heads/main or packed-refs.""" + ref_file = project_root / ".git" / "refs" / "heads" / "main" + if ref_file.exists(): + return ref_file.read_text().strip() + # Fall back to packed-refs + packed = project_root / ".git" / "packed-refs" + if packed.exists(): + for line in packed.read_text().splitlines(): + if line.endswith(" refs/heads/main"): + return line.split()[0] + return "" + + +def _branch_sha(project_root: Path, branch: str) -> str: + """Return the current SHA of a branch.""" + ref_file = project_root / ".git" / "refs" / "heads" / branch + if ref_file.exists(): + return ref_file.read_text().strip() + packed = project_root / ".git" / "packed-refs" + if packed.exists(): + for line in packed.read_text().splitlines(): + if line.endswith(f" refs/heads/{branch}"): + return line.split()[0] + return "" + + +def _reflog_commits(project_root: Path, branch: str) -> list[tuple[str, str]]: + """Return (sha, subject) pairs from .git/logs/refs/heads/.""" + log_file = project_root / ".git" / "logs" / "refs" / "heads" / branch + if not log_file.exists(): + return [] + commits = [] + for line in log_file.read_text().splitlines(): + # Format: \t: + parts = line.split("\t", 1) + if len(parts) < 2: + continue + new_sha = parts[0].split()[1] + action_msg = parts[1] + # Only commit entries (not checkout/merge/etc) + if action_msg.startswith("commit: "): + subject = action_msg[len("commit: "):] + commits.append((new_sha, subject)) + return commits + + +def _git_commits(project_root: Path) -> list[str]: + """Return commit subjects for all commits on the current branch ahead of main.""" + branch = _current_branch(project_root) + if not branch or branch == "main": + return [] + + main_sha = _main_sha(project_root) + commits = _reflog_commits(project_root, branch) + + # Walk from tip back until we hit a SHA that main also has + subjects = [] + for sha, subject in reversed(commits): + if sha == main_sha: + break + subjects.append(subject) + + return list(reversed(subjects)) + + +def validate_commits(project_root: Path) -> tuple[bool, list[str]]: + """Check all branch commits; return (ok, errors).""" + subjects = _git_commits(project_root) + if not subjects: + return True, [] + + errors = [] + for subject in subjects: + if FORBIDDEN.search(subject): + errors.append(f"forbidden pattern: '{subject}'") + elif not CONVENTIONAL_RE.match(subject): + errors.append(f"non-conventional: '{subject}'") + + return not errors, errors + + +def main() -> int: + """Run commit message validation.""" + project_root = Path(__file__).resolve().parent.parent + ok, errors = validate_commits(project_root) + if ok: + print("OK: all commits follow conventional format") + return 0 + for err in errors: + print(f"ERROR: {err}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_feature_file.py b/scripts/check_feature_file.py new file mode 100644 index 0000000..2471748 --- /dev/null +++ b/scripts/check_feature_file.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +"""Validate the in-progress .feature file structure.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +def _in_progress_feature(project_root: Path) -> Path | None: + """Return the single .feature file in in-progress/, or None.""" + ip = project_root / "docs" / "features" / "in-progress" + if not ip.exists(): + return None + files = [f for f in ip.iterdir() if f.suffix == ".feature"] + return files[0] if len(files) == 1 else None + + +def _flush_example( + current_example: dict | None, + current_rule: dict | None, +) -> dict | None: + """Append current_example to current_rule and return None.""" + if current_example is not None and current_rule is not None: + current_rule["examples"].append(current_example) + return None + + +def _flush_rule( + current_rule: dict | None, data: dict +) -> dict | None: + """Append current_rule to data["rules"] and return None.""" + if current_rule is not None: + data["rules"].append(current_rule) + return None + + +def _handle_rule_line(line: str, current_example: dict | None, + current_rule: dict | None, + data: dict) -> tuple[dict | None, dict | None]: + """Process a 'Rule:' line and return updated state.""" + current_example = _flush_example(current_example, current_rule) + current_rule = _flush_rule(current_rule, data) + return {"name": line, "examples": []}, current_example + + +def _handle_tag_line(line: str, current_example: dict | None, + current_rule: dict | None) -> tuple[dict | None, dict | None]: + """Process an @id tag line and return updated state.""" + current_example = _flush_example(current_example, current_rule) + return {"id": line[1:], "steps": []}, current_example + + +def _handle_keyword_line(line: str, current_example: dict | None, + data: dict) -> dict | None: + """Process Example:/Scenario: or step lines.""" + if current_example is None: + current_example = {"id": None, "steps": []} + if line.startswith("Scenario:"): + data["errors"].append("uses 'Scenario:' instead of 'Example:'") + return current_example + + +def parse_feature(text: str) -> dict: + """Parse a .feature file into structured data via regex.""" + data = { + "has_baselined": False, + "rules": [], + "errors": [], + } + current_rule: dict | None = None + current_example: dict | None = None + + for raw_line in text.splitlines(): + line = raw_line.strip() + if not line: + continue + if "Status: BASELINED" in line: + data["has_baselined"] = True + if line.startswith("Rule:"): + current_rule, current_example = _handle_rule_line( + line, current_example, current_rule, data + ) + if re.match(r"^@\w+$", line): + current_example, _ = _handle_tag_line( + line, current_example, current_rule + ) + if line.startswith(("Example:", "Scenario:")): + current_example = _handle_keyword_line( + line, current_example, data + ) + step_keywords = ("Given ", "When ", "Then ", "And ", "But ") + if line.startswith(step_keywords) and current_example is not None: + current_example["steps"].append(line) + + current_example = _flush_example(current_example, current_rule) + _flush_rule(current_rule, data) + + return data + + +def _validate_examples(rule: dict, errors: list[str]) -> int: + """Validate all examples in a rule; return count.""" + count = 0 + if not rule["examples"]: + errors.append(f"{rule['name']} has no Example: blocks") + for ex in rule["examples"]: + count += 1 + if not ex["id"]: + errors.append(f"Example in {rule['name']} missing @id tag") + steps = [s.split()[0] for s in ex["steps"]] + for keyword in ("Given", "When", "Then"): + if keyword not in steps: + errors.append( + f"@{ex['id'] or '?'} missing {keyword} step" + ) + return count + + +def validate_feature(project_root: Path) -> tuple[bool, list[str]]: + """Validate the in-progress feature file; return (ok, errors).""" + feature = _in_progress_feature(project_root) + if feature is None: + return False, ["no feature file in docs/features/in-progress/"] + + data = parse_feature(feature.read_text()) + errors = list(data["errors"]) + + if not data["has_baselined"]: + errors.append("missing 'Status: BASELINED'") + if not data["rules"]: + errors.append("no 'Rule:' blocks found") + + total_examples = sum( + _validate_examples(rule, errors) for rule in data["rules"] + ) + + if total_examples > 8: + errors.append( + f"decomposition check failed: {total_examples} examples (>8)" + ) + + return not errors, errors + + +def main() -> int: + """Run validation and print results.""" + project_root = Path(__file__).resolve().parent.parent + ok, errors = validate_feature(project_root) + if ok: + print("OK: feature file is valid") + return 0 + for err in errors: + print(f"ERROR: {err}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_oc.py b/scripts/check_oc.py new file mode 100644 index 0000000..5b52324 --- /dev/null +++ b/scripts/check_oc.py @@ -0,0 +1,204 @@ +#!/usr/bin/env python3 +"""Check mechanically-verifiable Object Calisthenics rules. Warn-only.""" + +from __future__ import annotations + +import ast +import sys +from pathlib import Path + + +class _OCVisitor(ast.NodeVisitor): + """AST visitor that collects OC violations per file.""" + + def __init__(self) -> None: + self.violations: list[str] = [] + + def _line_ref(self, node: ast.AST) -> str: + """Return 'L{line}' for a node.""" + return f"L{node.lineno}" + + def visit_ClassDef(self, node: ast.ClassDef) -> None: + """Check class length (OC-7).""" + code_lines = set() + for child in ast.walk(node): + if child is node: + continue + if hasattr(child, "lineno"): + code_lines.add(child.lineno) + if len(code_lines) > 50: + self.violations.append( + f"{node.name} {self._line_ref(node)}: class >50 lines " + f"({len(code_lines)} code lines)" + ) + self.generic_visit(node) + + def visit_FunctionDef(self, node: ast.FunctionDef) -> None: + """Check function-level OC rules.""" + self._check_function_length(node) + self._check_nesting(node) + self._check_else_after_return(node) + self._check_getter_setter(node) + self.generic_visit(node) + + visit_AsyncFunctionDef = visit_FunctionDef # noqa: N815 + + def _check_function_length(self, node: ast.FunctionDef) -> None: + """OC-7: function body <=20 code lines (excluding docstring).""" + doc_end = 0 + if ( + node.body + and isinstance(node.body[0], ast.Expr) + and isinstance(node.body[0].value, ast.Constant) + and isinstance(node.body[0].value.value, str) + ): + doc_end = node.body[0].end_lineno or node.body[0].lineno + + code_lines = { + child.lineno + for child in ast.walk(node) + if hasattr(child, "lineno") + and child.lineno > doc_end + and child is not node + } + if len(code_lines) > 20: + self.violations.append( + f"{node.name} {self._line_ref(node)}: function >20 lines " + f"({len(code_lines)} code lines)" + ) + + def _check_nesting(self, node: ast.FunctionDef) -> None: + """OC-1: nesting depth <=2.""" + max_depth = 0 + stack: list[tuple[ast.AST, int]] = [(node, 0)] + while stack: + current, depth = stack.pop() + if isinstance( + current, (ast.For, ast.While, ast.If, ast.With, ast.Try) + ): + depth += 1 + max_depth = max(max_depth, depth) + for child in ast.iter_child_nodes(current): + stack.append((child, depth)) + if max_depth > 2: + self.violations.append( + f"{node.name} {self._line_ref(node)}: " + f"nesting depth {max_depth} >2" + ) + + def _check_else_after_return(self, node: ast.FunctionDef) -> None: + """OC-2: no else after return.""" + for child in ast.walk(node): + if ( + isinstance(child, ast.If) + and self._ends_with_return(child.body) + and child.orelse + ): + self.violations.append( + f"{node.name} {self._line_ref(child)}: " + "else after return" + ) + + def _ends_with_return(self, body: list[ast.stmt]) -> bool: + """True if the last statement in body is a return or raise.""" + if not body: + return False + last = body[-1] + return isinstance(last, (ast.Return, ast.Raise)) + + def _check_getter_setter(self, node: ast.FunctionDef) -> None: + """OC-9: no get_ / set_ method names.""" + if node.name.startswith(("get_", "set_")): + self.violations.append( + f"{node.name} {self._line_ref(node)}: getter/setter name" + ) + + +def _is_dataclass_like(text: str, class_node: ast.ClassDef) -> bool: + """Heuristic: class has @dataclass, @dataclass(), @pydantic decorator.""" + for dec in class_node.decorator_list: + name = "" + if isinstance(dec, ast.Name): + name = dec.id + elif isinstance(dec, ast.Call) and isinstance(dec.func, ast.Name): + name = dec.func.id + if name in {"dataclass", "pydantic"}: + return True + return "@dataclass" in text or "BaseModel" in text + + +def _scan_forbidden(text: str, rel_path: Path) -> list[str]: + """Scan file text for noqa and type: ignore lines.""" + forbidden: list[str] = [] + for i, line in enumerate(text.splitlines(), 1): + if "noqa" in line: + forbidden.append(f"{rel_path} L{i}: noqa") + if "type: ignore" in line: + forbidden.append(f"{rel_path} L{i}: type: ignore") + return forbidden + + +def _check_file( + f: Path, project_root: Path +) -> tuple[list[str], list[str]]: + """Run OC checks on a single file; return (violations, forbidden).""" + text = f.read_text() + rel = f.relative_to(project_root) + forbidden = _scan_forbidden(text, rel) + + try: + tree = ast.parse(text, filename=str(f)) + except SyntaxError: + return [], forbidden + + violations: list[str] = [] + for node in ast.walk(tree): + if isinstance(node, ast.ClassDef) and _is_dataclass_like(text, node): + continue + if isinstance(node, ast.FunctionDef): + visitor = _OCVisitor() + visitor.visit(node) + for v in visitor.violations: + violations.append(f"{rel} {v}") + + return violations, forbidden + + +def check_oc(project_root: Path, package: str = "app") -> tuple[list[str], list[str]]: + """Run OC checks on the package; return (violations, forbidden).""" + pkg_dir = project_root / package + all_violations: list[str] = [] + all_forbidden: list[str] = [] + + if not pkg_dir.exists(): + return all_violations, all_forbidden + + for f in pkg_dir.rglob("*.py"): + v, fb = _check_file(f, project_root) + all_violations.extend(v) + all_forbidden.extend(fb) + + return all_violations, all_forbidden + + +def main() -> int: + """Run OC checks and print violations as warnings. Always exits 0.""" + project_root = Path(__file__).resolve().parent.parent + violations, forbidden = check_oc(project_root) + + if forbidden: + for f in forbidden: + print(f"FORBIDDEN: {f}") + if violations: + for v in violations: + print(f"OC: {v}") + if not violations and not forbidden: + print("OK: no OC violations or forbidden patterns") + else: + print(f"\nWarnings: {len(forbidden)} forbidden, {len(violations)} OC") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_stubs.py b/scripts/check_stubs.py new file mode 100644 index 0000000..6bd8231 --- /dev/null +++ b/scripts/check_stubs.py @@ -0,0 +1,113 @@ +#!/usr/bin/env python3 +"""Check test stubs against @id tags in the in-progress .feature file.""" + +from __future__ import annotations + +import ast +import re +import sys +from pathlib import Path + + +def _in_progress_feature(project_root: Path) -> Path | None: + """Return the single .feature file in in-progress/, or None.""" + ip = project_root / "docs" / "features" / "in-progress" + if not ip.exists(): + return None + files = [f for f in ip.iterdir() if f.suffix == ".feature"] + return files[0] if len(files) == 1 else None + + +def _extract_ids(text: str) -> set[str]: + """Extract all @id tags that precede Example: blocks.""" + return set(re.findall(r"@(\w+)\n\s*Example:", text)) + + +def _scan_test_dir(stub_dir: Path) -> dict[str, dict]: + """Parse all *_test.py files and map test functions to metadata.""" + mapping: dict[str, dict] = {} + for f in stub_dir.glob("*_test.py"): + tree = ast.parse(f.read_text(), filename=str(f)) + for node in ast.walk(tree): + if not isinstance(node, ast.FunctionDef): + continue + mapping[node.name] = { + "file": f.name, + "skipped": any( + isinstance(dec, ast.Attribute) + and dec.attr == "skip" + for dec in node.decorator_list + ), + } + return mapping + + +def check_stubs(project_root: Path) -> tuple[bool, list[str], dict]: + """Check stubs; return (ok, errors, stats).""" + feature = _in_progress_feature(project_root) + if feature is None: + return False, ["no feature file in in-progress/"], {} + + stem = feature.stem + ids = _extract_ids(feature.read_text()) + stub_dir = project_root / "tests" / "features" / stem + stats = {"total_ids": len(ids), "stubs_found": 0, "skipped": 0} + + if not stub_dir.exists(): + return ( + False, + [f"tests/features/{stem}/ does not exist"], + stats, + ) + + mapping = _scan_test_dir(stub_dir) + errors = [] + found_ids: set[str] = set() + + for id_tag in ids: + func_name = f"test_{stem}_{id_tag}" + if func_name not in mapping: + errors.append(f"missing stub for @id {id_tag} ({func_name})") + else: + found_ids.add(id_tag) + stats["stubs_found"] += 1 + if mapping[func_name]["skipped"]: + stats["skipped"] += 1 + + for func_name, meta in mapping.items(): + if func_name.startswith(f"test_{stem}_"): + derived_id = func_name[len(f"test_{stem}_") :] + if derived_id not in ids: + errors.append( + f"orphan stub {func_name} in {meta['file']}" + ) + + if not ids: + errors.append("no @id tags found in .feature file") + + return not errors, errors, stats + + +def main() -> int: + """Run stub check and print results.""" + project_root = Path(__file__).resolve().parent.parent + ok, errors, stats = check_stubs(project_root) + if not stats: + for err in errors: + print(f"ERROR: {err}") + return 1 + print( + f"ids: {stats['total_ids']}, " + f"stubs: {stats['stubs_found']}, " + f"skipped: {stats['skipped']}" + ) + if ok: + print("OK: all @id tags have matching stubs") + return 0 + for err in errors: + print(f"ERROR: {err}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_version.py b/scripts/check_version.py new file mode 100644 index 0000000..9acdb1a --- /dev/null +++ b/scripts/check_version.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Verify version consistency before release. + +Checks: +1. Version is present in pyproject.toml. +2. CHANGELOG.md has an entry for the current version. +3. app/__init__.py __version__ matches pyproject.toml (if present). +""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +def main() -> int: + """Run version consistency checks.""" + project_root = Path(__file__).resolve().parent.parent + pyproject = project_root / "pyproject.toml" + changelog = project_root / "CHANGELOG.md" + init_py = project_root / "app" / "__init__.py" + + # 1. Extract version from pyproject.toml + version = None + for line in pyproject.read_text().splitlines(): + if line.startswith("version"): + version = line.split("=")[-1].strip().strip('"') + break + if not version: + print("ERROR: Could not extract version from pyproject.toml") + return 1 + print(f"Checking release v{version}") + + # 2. Check CHANGELOG.md has entry for this version + if f"## [v{version}]" not in changelog.read_text(): + print(f"ERROR: CHANGELOG.md has no entry for v{version}") + return 1 + print(" OK: CHANGELOG.md entry found") + + # 3. Check app/__init__.py __version__ matches (if present) + init_text = init_py.read_text() if init_py.exists() else "" + if "__version__" in init_text: + match = re.search( + r'^__version__\s*=\s*["\']([^"\']+)["\']', + init_text, + re.MULTILINE, + ) + pkg_version = match.group(1) if match else None + if pkg_version != version: + print( + f"ERROR: app/__init__.py __version__ ({pkg_version!r}) " + f"does not match pyproject.toml ({version!r})" + ) + return 1 + print(" OK: app/__init__.py __version__ matches") + else: + print(" NOTE: app/__init__.py has no __version__ (skipping)") + + print("Version checks passed") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/check_work_md.py b/scripts/check_work_md.py new file mode 100644 index 0000000..befd408 --- /dev/null +++ b/scripts/check_work_md.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +"""Validate WORK.md structure and consistency.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + +VALID_STATES = { + "IDLE", + "STEP-1-BACKLOG-CRITERIA", + "STEP-1-DISCOVERY", + "STEP-1-STORIES", + "STEP-1-CRITERIA", + "STEP-2-READY", + "STEP-2-ARCH", + "STEP-3-WORKING", + "STEP-3-RED", + "STEP-4-READY", + "STEP-5-READY", + "STEP-5-MERGE", + "STEP-5-COMPLETE", + "POST-MORTEM", +} + + +def _current_branch() -> str: + """Return the current git branch name by reading .git/HEAD.""" + git_head = Path(__file__).resolve().parent.parent / ".git" / "HEAD" + if not git_head.exists(): + return "" + content = git_head.read_text().strip() + # Format: "ref: refs/heads/" when on a branch + if content.startswith("ref: refs/heads/"): + return content[len("ref: refs/heads/"):] + return "" + + +def parse_work_md(project_root: Path) -> tuple[list[dict], list[str]]: + """Parse WORK.md active items; return (items, errors).""" + work_md = project_root / "WORK.md" + if not work_md.exists(): + return [], ["WORK.md does not exist"] + + text = work_md.read_text() + in_active = False + items: list[dict] = [] + errors: list[str] = [] + + for line in text.splitlines(): + stripped = line.strip() + if stripped.startswith("## Active Items"): + in_active = True + continue + if in_active and stripped.startswith("##"): + break + if not in_active: + continue + if stripped.startswith("-") and "@id:" in stripped: + item: dict = {"raw": stripped, "id": None, "state": None, "branch": None} + id_match = re.search(r"@id:\s*(\S+)", stripped) + state_match = re.search(r"@state:\s*(\S+)", stripped) + branch_match = re.search(r"@branch:\s*(\S+)", stripped) + if id_match: + item["id"] = id_match.group(1) + if state_match: + item["state"] = state_match.group(1) + if branch_match: + item["branch"] = branch_match.group(1) + items.append(item) + + return items, errors + + +def validate_work_md(project_root: Path) -> tuple[bool, list[str]]: + """Validate WORK.md; return (ok, errors).""" + items, errors = parse_work_md(project_root) + branch = _current_branch() + + if not items: + return True, errors + + for item in items: + if item["id"] is None: + errors.append(f"missing @id in: {item['raw']}") + if item["state"] is None: + errors.append(f"missing @state in: {item['raw']}") + elif item["state"] not in VALID_STATES: + errors.append( + f"invalid @state '{item['state']}' in: {item['raw']}" + ) + if item["branch"] is None: + errors.append(f"missing @branch in: {item['raw']}") + elif item["branch"] != branch: + errors.append( + f"@branch '{item['branch']}' != current branch '{branch}'" + ) + + return not errors, errors + + +def main() -> int: + """Run WORK.md validation.""" + project_root = Path(__file__).resolve().parent.parent + ok, errors = validate_work_md(project_root) + if ok: + print("OK: WORK.md is valid and consistent") + return 0 + for err in errors: + print(f"ERROR: {err}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/detect_state.py b/scripts/detect_state.py new file mode 100644 index 0000000..22138d7 --- /dev/null +++ b/scripts/detect_state.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python3 +"""Evaluate all 14 FLOW.md detection rules and output the current workflow state.""" + +from __future__ import annotations + +import io +import re +import sys +from pathlib import Path + +import pytest + + +def _current_branch(project_root: Path) -> str: + """Return the current git branch name by reading .git/HEAD.""" + git_head = project_root / ".git" / "HEAD" + if not git_head.exists(): + return "" + content = git_head.read_text().strip() + if content.startswith("ref: refs/heads/"): + return content[len("ref: refs/heads/"):] + return "" + + +def _in_progress_feature(project_root: Path) -> Path | None: + """Return the single .feature file in in-progress/, or None.""" + ip = project_root / "docs" / "features" / "in-progress" + if not ip.exists(): + return None + files = [f for f in ip.iterdir() if f.suffix == ".feature"] + return files[0] if len(files) == 1 else None + + +def _has_status_baselined(text: str) -> bool: + """True if the feature text contains 'Status: BASELINED'.""" + return "Status: BASELINED" in text + + +def _has_rule_blocks(text: str) -> bool: + """True if any 'Rule:' block exists.""" + return "Rule:" in text + + +def _has_example_with_id(text: str) -> bool: + """True if any 'Example:' block carries an @id tag.""" + return bool(re.search(r"@\w+\n\s*Example:", text)) + + +def _backlog_features_with_baselined_no_id( + project_root: Path, +) -> bool: + """Check if any backlog feature is BASELINED but lacks @id Examples.""" + backlog = project_root / "docs" / "features" / "backlog" + if not backlog.exists(): + return False + for f in backlog.iterdir(): + if f.suffix != ".feature": + continue + text = f.read_text() + if _has_status_baselined(text) and not _has_example_with_id(text): + return True + return False + + +def _branch_exists(project_root: Path, branch_name: str) -> bool: + """Check if a local branch exists by reading .git/refs or packed-refs.""" + ref_file = project_root / ".git" / "refs" / "heads" / branch_name + if ref_file.exists(): + return True + packed = project_root / ".git" / "packed-refs" + if packed.exists(): + marker = f" refs/heads/{branch_name}" + return any(line.endswith(marker) for line in packed.read_text().splitlines()) + return False + + +def _feature_branch_exists(project_root: Path, feature_stem: str) -> bool: + """Check if a feat/ or fix/ branch exists for this feature.""" + return _branch_exists(project_root, f"feat/{feature_stem}") or _branch_exists( + project_root, f"fix/{feature_stem}" + ) + + +def _stubs_exist(project_root: Path, feature_stem: str) -> bool: + """True if tests/features// exists and has .py files.""" + stub_dir = project_root / "tests" / "features" / feature_stem + return stub_dir.exists() and any(stub_dir.glob("*_test.py")) + + +def _count_skipped_stubs(project_root: Path, feature_stem: str) -> int: + """Count @pytest.mark.skip decorators in test stubs.""" + stub_dir = project_root / "tests" / "features" / feature_stem + if not stub_dir.exists(): + return 0 + return sum( + f.read_text().count("@pytest.mark.skip") + for f in stub_dir.glob("*_test.py") + ) + + +def _pytest_result(project_root: Path, feature_stem: str) -> int: + """Run pytest on feature tests and return exit code.""" + stub_dir = project_root / "tests" / "features" / feature_stem + if not stub_dir.exists(): + return 0 + # Suppress pytest output; we only care about the exit code + buf = io.StringIO() + old_stdout, old_stderr = sys.stdout, sys.stderr + sys.stdout = sys.stderr = buf + try: + code = pytest.main([str(stub_dir), "-q", "--tb=no"]) + finally: + sys.stdout, sys.stderr = old_stdout, old_stderr + return int(code) + + +def _work_md_state(project_root: Path) -> str | None: + """Parse WORK.md for the first @state value in ## Active Items.""" + work_md = project_root / "WORK.md" + if not work_md.exists(): + return None + text = work_md.read_text() + in_active = False + for line in text.splitlines(): + if line.strip().startswith("## Active Items"): + in_active = True + continue + if in_active and line.strip().startswith("##"): + break + if in_active and "@state:" in line: + return line.split("@state:")[1].strip().split()[0] + return None + + +def _postmortem_exists(project_root: Path, feature_stem: str) -> bool: + """Check if any post-mortem file references this feature stem.""" + pm_dir = project_root / "docs" / "post-mortem" + if not pm_dir.exists(): + return False + return any( + feature_stem in f.name for f in pm_dir.iterdir() if f.is_file() + ) + + +VALID_STATES = { + "IDLE", + "STEP-1-BACKLOG-CRITERIA", + "STEP-1-DISCOVERY", + "STEP-1-STORIES", + "STEP-1-CRITERIA", + "STEP-2-READY", + "STEP-2-ARCH", + "STEP-3-WORKING", + "STEP-3-RED", + "STEP-4-READY", + "STEP-5-READY", + "STEP-5-MERGE", + "STEP-5-COMPLETE", + "POST-MORTEM", +} + + +def _detect_idle(project_root: Path) -> tuple[str, str] | None: + """Rules 1-2: no feature in progress.""" + if _backlog_features_with_baselined_no_id(project_root): + return "STEP-1-BACKLOG-CRITERIA", "backlog BASELINED but no @id" + return "IDLE", "no feature in in-progress/" + + +def _detect_from_content( + project_root: Path, text: str, stem: str +) -> tuple[str, str] | None: + """Rules 3-6: content-based detection.""" + if not _has_status_baselined(text): + return "STEP-1-DISCOVERY", "feature in-progress but not BASELINED" + if not _has_rule_blocks(text): + return "STEP-1-STORIES", "BASELINED but no Rule: blocks" + if not _has_example_with_id(text): + return "STEP-1-CRITERIA", "Rule: blocks present but no @id Examples" + if not _feature_branch_exists(project_root, stem): + return ( + "STEP-2-READY", + f"@id present but no feat/{stem} or fix/{stem} branch", + ) + return None + + +def _detect_from_tests( + project_root: Path, stem: str, branch: str +) -> tuple[str, str] | None: + """Rules 7-11: branch and test state.""" + stubs = _stubs_exist(project_root, stem) + if branch.startswith((f"feat/{stem}", f"fix/{stem}")) and not stubs: + return ( + "STEP-2-ARCH", + f"on {branch} but no test stubs in tests/features/{stem}/", + ) + + skipped = _count_skipped_stubs(project_root, stem) + if stubs and skipped == 0: + if _pytest_result(project_root, stem) != 0: + return "STEP-3-RED", "unskipped test exists that fails" + return "STEP-4-READY", "all tests pass, no skipped tests" + + work_state = _work_md_state(project_root) + if work_state == "STEP-5-READY": + return "STEP-5-READY", "WORK.md @state = STEP-5-READY" + + if stubs and skipped > 0: + return ( + "STEP-3-WORKING", + "test stubs exist with @pytest.mark.skip remaining", + ) + + return None + + +def _detect_final( + project_root: Path, stem: str, branch: str +) -> tuple[str, str] | None: + """Rules 12-14: final state detection.""" + work_state = _work_md_state(project_root) + if branch == "main" and work_state == "STEP-5-COMPLETE": + return "STEP-5-COMPLETE", "on main, WORK.md @state = STEP-5-COMPLETE" + if branch.startswith((f"feat/{stem}", f"fix/{stem}")): + return "STEP-5-MERGE", f"on {branch}, feature still in in-progress/" + if _postmortem_exists(project_root, stem): + return "POST-MORTEM", f"post-mortem exists for {stem}" + return None + + +def detect_state(project_root: Path) -> tuple[str, str]: + """Evaluate all 14 detection rules and return (detected_state, reason).""" + feature = _in_progress_feature(project_root) + branch = _current_branch(project_root) + + if feature is None: + return _detect_idle(project_root) + + text = feature.read_text() + stem = feature.stem + + result = _detect_from_content(project_root, text, stem) + if result is not None: + return result + + result = _detect_from_tests(project_root, stem, branch) + if result is not None: + return result + + result = _detect_final(project_root, stem, branch) + if result is not None: + return result + + return "UNKNOWN", "no detection rule matched" + + +def main() -> int: + """Run detection and report state, exiting non-zero on WORK.md mismatch.""" + project_root = Path(__file__).resolve().parent.parent + state, reason = detect_state(project_root) + work_state = _work_md_state(project_root) + + print(f"Detected state: {state}") + print(f"Reason: {reason}") + if work_state: + print(f"WORK.md @state: {work_state}") + if work_state != state and state in VALID_STATES: + print("WARNING: detected state differs from WORK.md @state") + return 1 + else: + print("WORK.md @state: (none)") + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/score_features.py b/scripts/score_features.py new file mode 100644 index 0000000..fa95eeb --- /dev/null +++ b/scripts/score_features.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Score backlog features for WSJF selection.""" + +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +def _backlog_features(project_root: Path) -> list[Path]: + """List all .feature files in docs/features/backlog/.""" + backlog = project_root / "docs" / "features" / "backlog" + if not backlog.exists(): + return [] + return sorted(f for f in backlog.iterdir() if f.suffix == ".feature") + + +def _count_ids(text: str) -> int: + """Count @id tags that precede Example: blocks.""" + return len(re.findall(r"@(\w+)\n\s*Example:", text)) + + +def _count_must(text: str) -> int: + """Count 'Must' markers (case-insensitive, whole word).""" + return len(re.findall(r"\bMust\b", text)) + + +def _effort_from_ids(count: int) -> str: + """Map @id count to effort tier.""" + if count <= 2: + return "trivial" + if count <= 5: + return "small" + if count <= 8: + return "medium" + return "large" + + +def score_features(project_root: Path) -> list[dict]: + """Score each backlog feature; return list of result dicts.""" + results = [] + for f in _backlog_features(project_root): + text = f.read_text() + ids = _count_ids(text) + must = _count_must(text) + results.append( + { + "stem": f.stem, + "ids": ids, + "must": must, + "effort": _effort_from_ids(ids), + } + ) + return results + + +def main() -> int: + """Print a WSJF scoring table for backlog features.""" + project_root = Path(__file__).resolve().parent.parent + results = score_features(project_root) + + if not results: + print("No backlog features found.") + return 0 + + print("| Feature | @ids | Effort | Must count |") + print("|---------|------|--------|------------|") + for r in results: + print( + f"| {r['stem']} | {r['ids']} | {r['effort']} | {r['must']} |" + ) + + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tests/unit/app_test.py b/tests/unit/app_test.py deleted file mode 100644 index 206c51f..0000000 --- a/tests/unit/app_test.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Unit tests for the application entry point.""" - -import pytest -from hypothesis import example, given -from hypothesis import strategies as st - -from app.__main__ import main - - -@given(verbosity=st.sampled_from(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])) -@example(verbosity="INFO") -def test_app_main_runs_with_valid_verbosity(verbosity: str) -> None: - """ - Given: A valid verbosity level string - When: main() is called with that verbosity - Then: It completes without raising an exception - """ - main(verbosity) diff --git a/uv.lock b/uv.lock index 24ab3de..e1b5da2 100644 --- a/uv.lock +++ b/uv.lock @@ -1052,9 +1052,6 @@ wheels = [ name = "temple8" version = "7.1.20260422" source = { virtual = "." } -dependencies = [ - { name = "fire" }, -] [package.optional-dependencies] dev = [ @@ -1078,7 +1075,6 @@ dev = [ [package.metadata] requires-dist = [ - { name = "fire", specifier = ">=0.7.1" }, { name = "ghp-import", marker = "extra == 'dev'", specifier = ">=2.1.0" }, { name = "hypothesis", marker = "extra == 'dev'", specifier = ">=6.148.4" }, { name = "pdoc", marker = "extra == 'dev'", specifier = ">=14.0" }, From 7df5886df53b0ba8c0d93a30d0bb030652ef7f07 Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:31:48 -0400 Subject: [PATCH 05/27] fix(workflow): replace Verbs/Nouns with Actions/Entities in SA-owned docs and skills MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - architect/SKILL.md: domain analysis rule now reads 'Nouns in feature/glossary language → candidate Entities, Value Objects, or Aggregates' and 'Verbs in feature/glossary language → candidate Actions (operations on an Entity, standalone function, or Domain Service)'; lines 129-130 updated to use 'actions' when writing to system.md - architect/system.md.template: '### Verbs' section renamed to '### Actions'; row placeholder updated from verb to action - update-docs/SKILL.md: 'nouns and verbs from the Domain Model' → 'entities and actions from the Domain Model' - define-scope/SKILL.md line 143 intentionally unchanged: PO reads sources using business-language framing (nouns/verbs is correct cognitive framing for the PO role) --- .../skills/architect/.system.md.template.swp | Bin 0 -> 12288 bytes .opencode/skills/architect/SKILL.md | 8 ++++---- .opencode/skills/architect/system.md.template | 4 ++-- .opencode/skills/update-docs/SKILL.md | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 .opencode/skills/architect/.system.md.template.swp diff --git a/.opencode/skills/architect/.system.md.template.swp b/.opencode/skills/architect/.system.md.template.swp new file mode 100644 index 0000000000000000000000000000000000000000..66c03795fbfc693dd3565c44ce73ee36e4abf6f4 GIT binary patch literal 12288 zcmeI2&yN&E6vqoikYD~lj2<8^W~ZR1d(uC4 znYbV^5x5wP8pS9^O^ne4Bw~W`0O3#YrUyL`e*g*5i$>$c@2l#botd2#<4I#r_-yZV zS5?3Ft@mElhAMBE7#ZbTavK;vw=nk8dza+Pe=KBMh8D0m6an47)@N|!S}pYRLo{H% z=-Y8kcu|aKV&pY!X&vwcV8!~N2A!@L#PRT-g8K5T?&m8&^9)!2%} z-+O1)DpMd+AXQ)m+qAJ?rFw7AUHrC{yV3{QHB%r{AX6YyAX6YyAX6YyAX6Yy;D4Y% zRA0befS=bTz8+7m3p%dJ*X)uhkSUNUkSUNUkSUNUkSUNUkSUNUkSUNUkSXvVRKRi= z`w)Np|Gp5$R5p7J;9a;d}%544eU{!71=AI0=q{qhJr{2fr?5>=$qe{0Pp1qhJ?U16G5T z;8t*X31b()5%3ZSK>$p!2%Nu>v2)-9@IE*R_JIlzzyc+(3+x0VU@N#CtO8dS;~n5U zI0s$<$3Y3~0ndWnU;>PQ$H2p&A1nmdgC7=Q?!X1`A~*yNg6&`o=&)yku5{6~%!t=x9-)#4jFxotwTV z3|D%BJ0i3LS&yXe6&UO7G@G(Y5jbhO-LpPqp zh%+tYg>kKB1&soqVKeAv82YFF6=Prsv@Z%s;Y6GT04lV6>%KI za$*~f(dg;GIps90O5H4#N{sUz!tFCFfnO`E=Vs*NXOHPxW#JZj)-gOER|mEB4j+j6 zOzEh{V^&QRdZ=Nmrn>n-bm5RznJj`XeQ^A%>iMoe+2}e(^Nrj5*mEFo#}MoI^JLbO zA_#r2;~7lSRzxy##jF*)D6PrBs_BSGj;R?Qxxt3+ZyL*>NKFJaOFBt_Bwi$OaZSNU zl2-TriGW(~xsG+wdOi)hG}VIWv6U`}p!wCn!){v8ZJi;cgjqw7xuU=?zJ8egz3~O_ za;wioFA;c_TUbY9lCZseRCPsi;C!HiBZ3Y7XkgW=D)r1kooH1n6V|xPPD@_xlIN@N;xvjGmdE9|>xeBxGk&s?=;x)Ru4P#mD0^onXXRNDE74*9_mD z-W#$4N5T^sHPB*=UPft64yDb*s;ncw$j&Jdl(nhTRbQDmjogG>M_%P-Ai~(i7Z}}| zL$ju*#U#D9Ru?iFub?!<0E2dfI?`fhakkK<=1sts#4MG(RLLeRHx{$bb=bj8N(;=L zk`wMU`we>tCKBK;duXi_>zhufyY1iTR*N?~8+CH3%?GYZ0m@loQtFkmM2f_*)L!z9 z^sW0|D9e&&i*zoTb<5swO^QN34O5ihc|xVldi_Ni*)#wX-ueQJ|F~!*VciUh4zpn= zBBkh~zIfDBxJfn{+|K%Fc{{^*(~2Y2qB8~a;XQ6O zIC7B(qV8I>4yi2R!$~EW?XM;egKAs%i-xilZb)wu2C*wfpKDwn^cFa+_sJ6@y?KZI4;AO1l0&<9;x%BFJA|ul6 zKsqeNWl~4G$L(w>YxgRdiKLKHy=sp~<;{2$#5V1SS^^^QQM2b4b9Ng;x}~*cOb90l zYke=Z-`sTZ@N1i~6+x}=?6yr7_bZ$#p4x=y_~v#T1Ze6uxZ{g(J)Zlz!pmqqw>$@N z6k_)xX=BC9dINwM$!P({^^PlXrjV}hNDdDp&P@-sz#$H+6iq6X$|wQYc~CY<&%j1t z5``knu>$m)l42U6T5c$V^%g2J{Amvp2`|PYQRka=)9FA)@HcInByaO@d$ajQwZ#*( zkxNpR+YQ?lG|G^c0((`QeQ({Il5VtbODv!ro_+ys;F9ea-;ULV3?ATS`*!9}5*X>h literal 0 HcmV?d00001 diff --git a/.opencode/skills/architect/SKILL.md b/.opencode/skills/architect/SKILL.md index 9bc2b1b..f7c1436 100644 --- a/.opencode/skills/architect/SKILL.md +++ b/.opencode/skills/architect/SKILL.md @@ -116,8 +116,8 @@ Only create an ADR for non-obvious decisions with meaningful trade-offs. Routine ## Domain Analysis From `docs/glossary.md` + Rules (Business) in the `.feature` file: -- **Nouns** → candidate classes, value objects, aggregates -- **Verbs** → method names with typed signatures +- **Nouns** in feature/glossary language → candidate Entities, Value Objects, or Aggregates in the domain model +- **Verbs** in feature/glossary language → candidate Actions (operations with typed signatures on an Entity, a standalone function, or a Domain Service) - **Datasets** → named types (not bare dict/list) - **Bounded Context check**: same word, different meaning across features? → module boundary - **Cross-feature entities** → candidate shared domain layer @@ -126,8 +126,8 @@ From `docs/glossary.md` + Rules (Business) in the `.feature` file: Update the `## Domain Model` section of `docs/system.md`: -- **New feature, first entities**: add bounded contexts, entities, verbs, and relationships to the Domain Model section. -- **Existing feature**: append new entities and verbs. Deprecate old entries if superseded — move them to a `### Deprecated` subsection. Never edit existing live entries — code depends on them. +- **New feature, first entities**: add bounded contexts, entities, actions, and relationships to the Domain Model section. +- **Existing feature**: append new entities and actions. Deprecate old entries if superseded — move them to a `### Deprecated` subsection. Never edit existing live entries — code depends on them. - Update the `## Context` and `## Container` sections if new actors, external systems, or containers are identified. The PO reads `docs/system.md` but never writes to it. diff --git a/.opencode/skills/architect/system.md.template b/.opencode/skills/architect/system.md.template index a492697..5112f3a 100644 --- a/.opencode/skills/architect/system.md.template +++ b/.opencode/skills/architect/system.md.template @@ -72,11 +72,11 @@ | `` | Entity | | | | `` | Value Object | | | -### Verbs +### Actions | Name | Actor | Object | Description | |------|-------|--------|-------------| -| `` | | | | +| `` | | | | ### Relationships diff --git a/.opencode/skills/update-docs/SKILL.md b/.opencode/skills/update-docs/SKILL.md index afa786c..9168321 100644 --- a/.opencode/skills/update-docs/SKILL.md +++ b/.opencode/skills/update-docs/SKILL.md @@ -46,7 +46,7 @@ Identify from the read phase: - **Actors** — named human roles from feature `As a ` clauses and discovery Scope section - **External systems** — any system outside the package boundary named in features or architecture decisions - **Containers** — deployable/runnable units identified in ADR files (Hexagonal adapters, CLIs, services) -- **Key domain terms** — all nouns and verbs from the Domain Model section of `system.md`, plus any terms defined in ADR decisions +- **Key domain terms** — all entities and actions from the Domain Model section of `system.md`, plus any terms defined in ADR decisions --- From f724cab6a143c14a401def33e44b8cbf3c614a13 Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:35:10 -0400 Subject: [PATCH 06/27] feat(cli-entrypoint): add architecture stubs and ADRs --- app/__main__.py | 14 ++ docs/adr/ADR-2026-04-22-cli-parser-library.md | 41 +++++ docs/adr/ADR-2026-04-22-version-source.md | 46 ++++++ docs/system.md | 152 ++++++++++++++++++ 4 files changed, 253 insertions(+) create mode 100644 docs/adr/ADR-2026-04-22-cli-parser-library.md create mode 100644 docs/adr/ADR-2026-04-22-version-source.md create mode 100644 docs/system.md diff --git a/app/__main__.py b/app/__main__.py index e69de29..f752d6f 100644 --- a/app/__main__.py +++ b/app/__main__.py @@ -0,0 +1,14 @@ +"""CLI entrypoint for temple8 — invoked via `python -m app`.""" + +import argparse +import importlib.metadata + + +def build_parser() -> argparse.ArgumentParser: ... + + +def main() -> None: ... + + +if __name__ == "__main__": + main() diff --git a/docs/adr/ADR-2026-04-22-cli-parser-library.md b/docs/adr/ADR-2026-04-22-cli-parser-library.md new file mode 100644 index 0000000..87d1a71 --- /dev/null +++ b/docs/adr/ADR-2026-04-22-cli-parser-library.md @@ -0,0 +1,41 @@ +# ADR-2026-04-22-cli-parser-library + +## Status + +Accepted + +## Context + +The `cli-entrypoint` feature requires a CLI parsing library to handle `--help` and `--version` +flags. The decision is which library to use. The feature constraint is explicit: zero new +runtime dependencies. The template must be installable with no extras, and the CLI skeleton is +a demonstration feature, not a production CLI framework. + +Alternatives evaluated: +- `argparse` (Python stdlib) +- `click` (third-party) +- `typer` (third-party, built on click) + +## Decision + +Use `argparse` from the Python stdlib. + +## Reason + +The zero-dependency constraint is non-negotiable for a template that must install cleanly with +`uv sync` and no extras. `argparse` is sufficient for a 2-flag (`--help`, `--version`) CLI +skeleton, and its `action="version"` built-in satisfies the version-output criterion directly. + +## Alternatives Considered + +- **`click`**: ergonomic, widely used, but adds a runtime dependency — violates the + zero-dependency constraint. +- **`typer`**: built on click + type hints, even heavier — same violation. + +## Consequences + +- (+) Zero install footprint — no `requirements.txt` entry, no version pinning for the CLI layer +- (+) `argparse` `action="version"` handles exit-0 and version string format natively +- (+) `build_parser()` is independently testable without subprocess overhead +- (-) `argparse` API is more verbose than click/typer for complex CLIs — acceptable for a + 2-flag demonstration skeleton diff --git a/docs/adr/ADR-2026-04-22-version-source.md b/docs/adr/ADR-2026-04-22-version-source.md new file mode 100644 index 0000000..2fe62b3 --- /dev/null +++ b/docs/adr/ADR-2026-04-22-version-source.md @@ -0,0 +1,46 @@ +# ADR-2026-04-22-version-source + +## Status + +Accepted + +## Context + +The `cli-entrypoint` feature requires the `--version` flag to print the application's version +string. The feature rule states: "the version string is always read from package metadata at +runtime; it is never hardcoded." The decision is how to access that metadata. + +Alternatives evaluated: +- `importlib.metadata.version()` (Python stdlib, since 3.8) +- Read `pyproject.toml` directly at runtime with `tomllib` +- Hardcode the version string in `__main__.py` +- Expose a `__version__` constant in `app/__init__.py` + +## Decision + +Use `importlib.metadata.version("temple8")` at runtime. + +## Reason + +`importlib.metadata` is the canonical stdlib API for reading installed package metadata since +Python 3.8. It reads from the distribution's `METADATA` file, which is the single source of +truth set by `pyproject.toml`. This satisfies the "never hardcoded" rule with the least code +and zero file I/O complexity. + +## Alternatives Considered + +- **Hardcoded string**: violates the explicit feature rule; drifts from `pyproject.toml` over + time. +- **Read `pyproject.toml` at runtime with `tomllib`**: works, but requires file path resolution + and adds I/O; `importlib.metadata` is simpler and the stdlib-blessed approach. +- **`app.__version__` constant in `__init__.py`**: requires a second source of truth; still + needs `importlib.metadata` or hardcoding to populate it — one extra indirection with no + benefit. + +## Consequences + +- (+) Single source of truth: `pyproject.toml` → installed metadata → runtime +- (+) Works correctly in editable installs (`uv sync`) and wheel installs +- (+) Zero additional imports beyond stdlib +- (-) Requires the package to be installed (not just on `sys.path` as a raw directory) — + acceptable since the feature's Given step is "the application package is installed" diff --git a/docs/system.md b/docs/system.md new file mode 100644 index 0000000..ec51e1c --- /dev/null +++ b/docs/system.md @@ -0,0 +1,152 @@ +# System Overview: temple8 + +> Current-state description of the production system. +> Rewritten by the system-architect at Step 2 for each feature cycle. +> Reviewed by the product-owner at Step 5. +> Contains only completed features — nothing from backlog or in-progress. + +--- + +## Summary + +temple8 is a Python project template that gives engineers a production-ready skeleton with zero +boilerplate. It ships with a single demonstration feature — a CLI entrypoint (`python -m app`) — +that exercises the full five-step delivery workflow end-to-end. The system is a single Python +package (`app`) with no runtime dependencies beyond the Python stdlib. + +--- + +## Actors + +| Actor | Needs | +|-------|-------| +| `Developer` | Run `python -m app --help` to verify the CLI is wired up; run `--version` to confirm the installed package version | + +--- + +## Structure + +| Module | Responsibility | +|--------|----------------| +| `app/__main__.py` | CLI entrypoint: parses `--help` and `--version` flags; reads version from package metadata | +| `app/__init__.py` | Package marker; no public API | + +--- + +## Key Decisions + +- Use `argparse` (stdlib) for CLI parsing — zero new dependencies (ADR-2026-04-22-cli-parser-library) +- Read version from `importlib.metadata` at runtime — single source of truth, never hardcoded (ADR-2026-04-22-version-source) + +--- + +## Configuration Keys + +| Key | Type | Default | Description | +|-----|------|---------|-------------| +| `project.name` | string | `"temple8"` | Application name; read from installed package metadata | +| `project.description` | string | `"From zero to hero — production-ready Python, without the ceremony."` | Tagline; set as `argparse` description | +| `project.version` | string | `"7.1.20260422"` | Calver version; read at runtime via `importlib.metadata` | + +--- + +## External Dependencies + +| Dependency | What it provides | Why not replaced | +|------------|------------------|-----------------| +| `argparse` | CLI argument parsing | stdlib; zero install cost; sufficient for 2-flag skeleton | +| `importlib.metadata` | Runtime package metadata access | stdlib; canonical API since Python 3.8 | + +--- + +## Active Constraints + +- Zero new runtime dependencies — all CLI and metadata functionality uses Python stdlib only +- All production code lives in `app/__main__.py` — no new source files +- Version format is calver (`major.minor.YYYYMMDD`); tests must not assume semver + +--- + +## Domain Model + +### Bounded Contexts + +| Context | Responsibility | Key Modules | +|---------|----------------|-------------| +| `CLI` | Expose the application as a command-line tool; parse flags; print help and version | `app/__main__.py` | + +### Entities + +| Name | Type | Description | Bounded Context | +|------|------|-------------|-----------------| +| `ArgumentParser` | Value Object (stdlib) | Configured parser with `--help` and `--version` actions | `CLI` | + +### Actions + +| Name | Actor | Object | Description | +|------|-------|--------|-------------| +| `build_parser` | `__main__` module | → `argparse.ArgumentParser` | Constructs and returns the configured CLI parser | +| `main` | `__main__` module | `sys.argv` → exit | Parses arguments and dispatches; `argparse` handles exit codes natively | + +### Relationships + +| Subject | Relation | Object | Cardinality | Notes | +|---------|----------|--------|-------------|-------| +| `main` | calls | `build_parser` | 1:1 | Parser constructed fresh on each invocation | +| `build_parser` | reads | `importlib.metadata` | 1:1 | Version string fetched at parser construction time | + +### Module Dependency Graph + +``` +app/__main__.py ──► argparse (stdlib) + ──► importlib.metadata (stdlib) +``` + +--- + +## Context + +```mermaid +C4Context + title System Context — temple8 + + Person(dev, "Developer", "Python engineer using the template") + + System(temple8, "temple8", "Production-ready Python project template with CLI entrypoint") + + Rel(dev, temple8, "Runs `python -m app --help` / `--version`", "CLI / subprocess") +``` + +--- + +## Container + +```mermaid +C4Container + title Container Diagram — temple8 + + Person(dev, "Developer", "") + + System_Boundary(app_boundary, "temple8") { + Container(cli, "CLI Entrypoint", "Python / argparse", "Parses --help and --version; reads version from package metadata") + } + + Rel(dev, cli, "Invokes via `python -m app`") +``` + +--- + +## ADRs + +See `docs/adr/` for the full decision record. Each ADR contains a `## Context` section with the Q&A that produced the decision. + +| ADR | Decision | +|-----|----------| +| `ADR-2026-04-22-cli-parser-library` | Use `argparse` (stdlib) for CLI parsing — zero new dependencies | +| `ADR-2026-04-22-version-source` | Read version from `importlib.metadata` at runtime — never hardcoded | + +--- + +## Completed Features + +*(none yet — cli-entrypoint is in-progress)* From b0394d0078c419fda4e410f14f5369817c975974 Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:35:32 -0400 Subject: [PATCH 07/27] feat(cli-entrypoint): generate test stubs --- .../cli_entrypoint/help_output_test.py | 27 +++++++++++++++++++ .../unrecognised_arguments_test.py | 24 +++++++++++++++++ .../cli_entrypoint/version_output_test.py | 26 ++++++++++++++++++ 3 files changed, 77 insertions(+) create mode 100644 tests/features/cli_entrypoint/help_output_test.py create mode 100644 tests/features/cli_entrypoint/unrecognised_arguments_test.py create mode 100644 tests/features/cli_entrypoint/version_output_test.py diff --git a/tests/features/cli_entrypoint/help_output_test.py b/tests/features/cli_entrypoint/help_output_test.py new file mode 100644 index 0000000..9d6509e --- /dev/null +++ b/tests/features/cli_entrypoint/help_output_test.py @@ -0,0 +1,27 @@ +"""Tests for help output story.""" + +import pytest + + +@pytest.mark.skip(reason="not yet implemented") +def test_cli_entrypoint_c1a2b3d4() -> None: + """ + Given: the application package is installed + When: the user runs `python -m app --help` + Then: the output contains the application name "temple8" + And: the output contains the tagline + And: the process exits with code 0 + """ + raise NotImplementedError + + +@pytest.mark.skip(reason="not yet implemented") +def test_cli_entrypoint_e5f6a7b8() -> None: + """ + Given: the application package is installed + When: the user runs `python -m app --help` + Then: the output contains "--help" + And: the output contains "--version" + """ + raise NotImplementedError + diff --git a/tests/features/cli_entrypoint/unrecognised_arguments_test.py b/tests/features/cli_entrypoint/unrecognised_arguments_test.py new file mode 100644 index 0000000..b84fc41 --- /dev/null +++ b/tests/features/cli_entrypoint/unrecognised_arguments_test.py @@ -0,0 +1,24 @@ +"""Tests for unrecognised arguments story.""" + +import pytest + + +@pytest.mark.skip(reason="not yet implemented") +def test_cli_entrypoint_e7f8a9b0() -> None: + """ + Given: the application package is installed + When: the user runs `python -m app --unknown-flag` + Then: the process exits with code 2 + """ + raise NotImplementedError + + +@pytest.mark.skip(reason="not yet implemented") +def test_cli_entrypoint_b1c2d3e4() -> None: + """ + Given: the application package is installed + When: the user runs `python -m app` with no arguments + Then: the process exits with code 0 + """ + raise NotImplementedError + diff --git a/tests/features/cli_entrypoint/version_output_test.py b/tests/features/cli_entrypoint/version_output_test.py new file mode 100644 index 0000000..f1d3000 --- /dev/null +++ b/tests/features/cli_entrypoint/version_output_test.py @@ -0,0 +1,26 @@ +"""Tests for version output story.""" + +import pytest + + +@pytest.mark.skip(reason="not yet implemented") +def test_cli_entrypoint_c9d0e1f2() -> None: + """ + Given: the application package is installed + When: the user runs `python -m app --version` + Then: the output contains "temple8" + And: the output contains the version string from package metadata + And: the process exits with code 0 + """ + raise NotImplementedError + + +@pytest.mark.skip(reason="not yet implemented") +def test_cli_entrypoint_a3b4c5d6() -> None: + """ + Given: the application package is installed + When: the user runs `python -m app --version` + Then: the version in the output matches `importlib.metadata.version("temple8")` + """ + raise NotImplementedError + From 97b961ee56fa90bd0f8bac0ee316bb1929e644ca Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:35:45 -0400 Subject: [PATCH 08/27] chore: cli-entrypoint transition to STEP-3-WORKING --- WORK.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/WORK.md b/WORK.md index 31a661a..bc72e60 100644 --- a/WORK.md +++ b/WORK.md @@ -14,6 +14,6 @@ Each item carries exactly the variables defined by `FLOW.md`: - @id: cli-entrypoint - @state: STEP-2-READY - @branch: [NONE] + @state: STEP-3-WORKING + @branch: feat/cli-entrypoint From c06c2811517bec80949e6ea00b022b8198d6991e Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:37:47 -0400 Subject: [PATCH 09/27] fix(cli-entrypoint): convert module dependency graph to mermaid for consistency --- .opencode/skills/architect/system.md.template | 8 ++++++-- docs/system.md | 11 ++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.opencode/skills/architect/system.md.template b/.opencode/skills/architect/system.md.template index 5112f3a..c1fac2a 100644 --- a/.opencode/skills/architect/system.md.template +++ b/.opencode/skills/architect/system.md.template @@ -86,8 +86,12 @@ ### Module Dependency Graph -``` - ──► ──► () +```mermaid +graph LR + [""] + [" (stdlib|external)"] + + --> ``` --- diff --git a/docs/system.md b/docs/system.md index ec51e1c..bdb4c90 100644 --- a/docs/system.md +++ b/docs/system.md @@ -97,9 +97,14 @@ package (`app`) with no runtime dependencies beyond the Python stdlib. ### Module Dependency Graph -``` -app/__main__.py ──► argparse (stdlib) - ──► importlib.metadata (stdlib) +```mermaid +graph LR + main["app/__main__.py"] + argparse["argparse (stdlib)"] + metadata["importlib.metadata (stdlib)"] + + main --> argparse + main --> metadata ``` --- From bb8b061be61f15b0276123c8bd22619d99e6a7e5 Mon Sep 17 00:00:00 2001 From: nullhack Date: Wed, 22 Apr 2026 14:40:57 -0400 Subject: [PATCH 10/27] =?UTF-8?q?fix(cli-entrypoint):=20reorder=20system.m?= =?UTF-8?q?d=20sections=20=E2=80=94=20zoom-in=20narrative=20from=20context?= =?UTF-8?q?=20to=20detail?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .opencode/skills/architect/system.md.template | 88 ++++++++--------- docs/system.md | 94 +++++++++---------- 2 files changed, 91 insertions(+), 91 deletions(-) diff --git a/.opencode/skills/architect/system.md.template b/.opencode/skills/architect/system.md.template index c1fac2a..36a8606 100644 --- a/.opencode/skills/architect/system.md.template +++ b/.opencode/skills/architect/system.md.template @@ -13,47 +13,55 @@ --- -## Actors +## Context -| Actor | Needs | -|-------|-------| -| `` | | +```mermaid +C4Context + title System Context — ---- + Person(, "", "") -## Structure + System(, "", "") -| Module | Responsibility | -|--------|----------------| -| `` | | + System_Ext(, "", "") + + Rel(, , "