diff --git a/CHANGELOG.md b/CHANGELOG.md index c7a0930..16ec0d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## [v1.0.0] - First Stone - 2026-05-06 + +### Added + +- **Specification reference page**: `docs/index.html` — semver.org-style specification with numbered rules, RFC 2119 language, formal syntax grammar, normative D3 diagrams, and FAQ +- **Remove fuzzy match operator**: `~=` (APPROXIMATELY_EQUAL) removed from the specification and reference implementation; flows using `~=` now produce `FlowParseError` with state context + +### Changed + +- **Condition operators reduced from 7 to 6**: `==`, `!=`, `>=`, `<=`, `>`, `<` +- **`flowr/domain/condition.py`**: removed `APPROXIMATELY_EQUAL` enum value, `~=` from operator prefix map, and approximate comparison logic +- **`flowr/domain/loader.py`**: added `_validate_condition_operators()` to reject `~=` in all when-clause forms (inline dict, list-form, named references) with `FlowParseError` +- **Specification documents**: `flow_definition_spec.md`, `glossary.md`, `system.md`, `product_definition.md` updated to list exactly 6 operators +- **ADR_20260426_fuzzy_match_algorithm.md**: deprecated with removal notice +- **README.md**: banner image uses absolute URL for PyPI rendering, guard conditions explicitly list 6 operators, documentation links use absolute URLs +- **pyproject.toml**: version bumped to 1.0.0, status promoted to Production/Stable, Documentation URL points to spec page, added PyPI classifiers + +### Removed + +- `~=` condition operator (APPROXIMATELY_EQUAL) — no longer a valid operator in the flowr specification +- 6 obsolete tests referencing `~=` parsing and evaluation + All notable changes to this project will be documented in this file. ## [v0.5.0+20260505] - Fine Sift - 2026-05-05 diff --git a/README.md b/README.md index abda2fa..9f91b06 100644 --- a/README.md +++ b/README.md @@ -1,52 +1,58 @@
-flowr — non-deterministic state machine specification to knead workflows +flowr — non-deterministic state machine specification to knead workflows

[![Coverage](https://img.shields.io/badge/coverage-100%25-brightgreen?style=for-the-badge)](https://nullhack.github.io/flowr/coverage/) [![CI](https://img.shields.io/github/actions/workflow/status/nullhack/flowr/ci.yml?style=for-the-badge&label=CI)](https://github.com/nullhack/flowr/actions/workflows/ci.yml) -[![Python](https://img.shields.io/badge/python-%E2%89%A513.0-blue?style=for-the-badge)](https://www.python.org/downloads/) +[![Python](https://img.shields.io/badge/python-%E2%89%A53.13-blue?style=for-the-badge)](https://www.python.org/downloads/) [![PyPI](https://img.shields.io/pypi/v/flowr?color=%2300FF41&style=for-the-badge)](https://pypi.org/project/flowr/) [![MIT License](https://img.shields.io/badge/license-MIT-green?style=for-the-badge)](https://github.com/nullhack/flowr/blob/main/LICENSE) -**Define workflow state machines in YAML. Validate, query, and track them from the terminal.** +**A declarative, validatable YAML format for non-deterministic state machine workflows.** + +[Read the specification →](https://nullhack.github.io/flowr/)
--- -> **⚠️ Beta — do not install.** This project is under active development. The API, package structure, and configuration may change without notice until the first stable release. +## The specification -You write a flow definition in YAML. flowr checks that it is structurally valid, tells you what states exist and which transitions are available, and keeps track of where you are — across invocations, across subflows. One specification format. One CLI. No runtime engine, no side effects, no opinions about what your workflow should *do* — only what it *is* and whether it *holds together*. +flowr defines what a workflow **is** — its states, transitions, guard conditions, subflows — not what it **does**. No execution engine. No side effects. No opinions about retries, timeouts, or error handling. A YAML file declares structure. A validator checks integrity. Tools query, track, and visualise. [The format is the foundation.](https://nullhack.github.io/flowr/) ---- +What the specification covers: -## Who is this for? +- **States** with unique ids, per-state attributes, and transition mappings +- **Transitions** that resolve to state ids or declared exit names +- **Guard conditions** gated by evidence-based expressions using 6 operators (`==`, `!=`, `>=`, `<=`, `>`, `<`) +- **Named condition groups** reusable across transitions on the same state +- **Subflows** with call-stack semantics — push on entry, pop on exit +- **Within-flow cycles** for iterative workflows +- **Validation rules** — structural integrity checks independent of any runtime -### Agent Operators — Persist workflow state across CLI invocations +What the specification does **not** cover: -You run `flowr check`, then `flowr transition`, then `flowr check` again — each time passing the flow name and current state by hand. Or you let sessions track it: `flowr session init deploy-flow`, `flowr --session transition approve`, `flowr --session check`. The session file remembers where you are. Push into a subflow; pop back out. No state to reconstruct, no context to pass. - -### Developers — Validate and query workflow definitions from code or terminal - -You write a flow YAML. You need to know: is it valid? Which states can I reach from here? Does this transition have guard conditions? `flowr validate`, `flowr states`, `flowr next`, `flowr check` answer these questions — from the terminal for humans, from the Python library for tools. +- Execution engines, side-effect hooks, retry logic, timeout handling +- Parallel (fork-join) states +- Orchestration, scheduling, or event dispatch -### Tool Authors — Build on a specification, not a runtime - -flowr defines a YAML format for non-deterministic state machines with per-state attributes, guard conditions, and subflows. The validator enforces structural constraints. The library parses flows into dataclasses. No execution engine, no side-effect hooks — a clean foundation for editors, visualizers, or orchestration layers. +→ **[Full specification with examples and visual diagrams](https://nullhack.github.io/flowr/)** --- -## What it does +## The reference implementation + +This repository contains a Python reference implementation — a CLI and library that validates, queries, and tracks flow definitions conforming to the specification. The specification is the contract; this code is one tool that honours it. ``` flowr validate deploy.yaml → valid: True flowr states deploy.yaml → prepare, execute, review flowr next deploy.yaml review → approve → deployed [blocked] need: score=>=80 - → reject → failed + → reject → failed flowr transition deploy.yaml review approve --evidence score=85 - → from: review, to: deployed + → from: review, to: deployed flowr session init deploy-flow → session created at state: prepare flowr --session transition approve → from: prepare, to: review flowr mermaid deploy.yaml → stateDiagram-v2 ... @@ -56,7 +62,7 @@ flowr mermaid deploy.yaml → stateDiagram-v2 ... **Query.** States, transitions, conditions, attributes — ask any question the flow can answer. -**Sessions.** Init, show, set-state, transition, list. Subflow push/pop for nested workflows. Auto-enters initial subflow on `session init`. One `--session` flag turns any command session-aware (including `validate` and `states`). +**Sessions.** Init, show, set-state, transition, list. Subflow push/pop for nested workflows. Auto-enters initial subflow on `session init`. One `--session` flag turns any command session-aware. **Config.** `flowr config` shows where every value comes from — default, pyproject.toml, or CLI override. @@ -146,6 +152,22 @@ default_session = default (default) --- +## Who is this for? + +### Specification adopters + +You build editors, visualizers, CI systems, or orchestration layers. You need a structural format for workflow state machines — not an execution engine. The flowr specification gives you states, transitions, guard conditions, and subflows in a declarative YAML format with a conforming validator. Any tool can parse it. [Read the spec.](https://nullhack.github.io/flowr/) + +### Agent operators + +You run `flowr check`, then `flowr transition`, then `flowr check` again — each time passing the flow name and current state by hand. Or you let sessions track it: `flowr session init deploy-flow`, `flowr --session transition approve`, `flowr --session check`. The session file remembers where you are. Push into a subflow; pop back out. No state to reconstruct, no context to pass. + +### Developers + +You write a flow YAML. You need to know: is it valid? Which states can I reach from here? Does this transition have guard conditions? `flowr validate`, `flowr states`, `flowr next`, `flowr check` answer these questions — from the terminal for humans, from the Python library for tools. + +--- + ## CLI Reference | Command | Description | @@ -192,18 +214,12 @@ Hexagonal architecture. Domain has no infrastructure dependencies. CLI is the pr --- -## Why does this exist - -No existing YAML standard covers non-deterministic state machine workflows with per-state agent assignment and filesystem-as-source-of-truth. Existing solutions (XState, SCXML, Serverless Workflow, BPMN) target execution engines or deterministic workflows. flowr fills this gap: a declarative, validatable, toolable format for workflows that branch on evidence rather than control flow. - ---- - ## Documentation -- **[flowr docs](https://nullhack.github.io/flowr/)** — hosted documentation -- **[Flow Definition Specification](docs/spec/flow_definition_spec.md)** — authoritative YAML format reference -- **[System Overview](docs/spec/system.md)** — architecture, domain model, module structure -- **[Product Definition](docs/spec/product_definition.md)** — product boundaries, users, and scope +- **[Specification](https://nullhack.github.io/flowr/)** — the flowr format with examples and visual diagrams +- **[Flow Definition Specification](https://github.com/nullhack/flowr/blob/main/docs/spec/flow_definition_spec.md)** — authoritative YAML format reference +- **[System Overview](https://github.com/nullhack/flowr/blob/main/docs/spec/system.md)** — architecture, domain model, module structure +- **[Product Definition](https://github.com/nullhack/flowr/blob/main/docs/spec/product_definition.md)** — product boundaries, users, and scope --- @@ -222,4 +238,4 @@ uv run task static-check # type checking ## License -MIT — see [LICENSE](LICENSE). \ No newline at end of file +MIT — see [LICENSE](LICENSE). diff --git a/docs/adr/ADR_20260426_condition_inlining.md b/docs/adr/ADR_20260426_condition_inlining.md index fb548af..4860820 100644 --- a/docs/adr/ADR_20260426_condition_inlining.md +++ b/docs/adr/ADR_20260426_condition_inlining.md @@ -50,4 +50,4 @@ Keeping `GuardCondition` unchanged preserves backward compatibility and simplici | Risk | Probability | Impact | Mitigation | Accepted? | |------|------------|--------|------------|-----------| -| Loader complexity increases with three when forms | Medium | Low | Well-tested with comprehensive BDD scenarios | Yes | \ No newline at end of file +| Loader complexity increases with three when forms | Medium | Low | Well-tested with thorough BDD scenarios | Yes | \ No newline at end of file diff --git a/docs/adr/ADR_20260426_fuzzy_match_algorithm.md b/docs/adr/ADR_20260426_fuzzy_match_algorithm.md index 474942c..e534a68 100644 --- a/docs/adr/ADR_20260426_fuzzy_match_algorithm.md +++ b/docs/adr/ADR_20260426_fuzzy_match_algorithm.md @@ -2,7 +2,7 @@ ## Status -Accepted +**Deprecated** — The `~=` operator was removed from the flowr specification in v1.0.0 (2026-05-06). It was unused in practice and added unnecessary complexity. See feature `remove-fuzzy-match-operator`. This ADR is retained for historical reference only. ## Context diff --git a/docs/assets/banner.svg b/docs/assets/banner.svg index 5781de7..6dd31bd 100644 --- a/docs/assets/banner.svg +++ b/docs/assets/banner.svg @@ -1,25 +1,25 @@ - + - + - flowr + flowr - - - \ No newline at end of file + + + diff --git a/docs/assets/logo.svg b/docs/assets/logo.svg index 6ba5ebf..f66bb78 100644 --- a/docs/assets/logo.svg +++ b/docs/assets/logo.svg @@ -1,44 +1,29 @@ - + - - - - - - + + - - + + - - - - + + + - - - - + + + - - - - - \ No newline at end of file + + + + diff --git a/docs/assets/workflow.svg b/docs/assets/workflow.svg deleted file mode 100644 index 054ce70..0000000 --- a/docs/assets/workflow.svg +++ /dev/null @@ -1,433 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - Temple8 — Feature Development Workflow - feature-flow.yaml + scope-cycle.yaml + arch-cycle.yaml + tdd-cycle.yaml - - - - Product Owner - - Software Engineer - - System Architect - - Idle / Gate - - Subflow - - Post-Mortem - - Contract Gate - - Exit - - - - - - idle - product-owner - - - - select-feature / discover - - - - - STEP 1 — SCOPE (subflow) - - - step-1-scope - Scope subflow: backlog-criteria → discovery → stories → criteria - product-owner - - - - Scope Cycle (scope-cycle.yaml) - - - backlog-criteria - - discovery - - stories - - criteria - - - - discover - - baselined - - committed - - - - more-discovery - - - - exit: complete - - - - exit: complete - - - - blocked - - - - - STEP 2 — ARCHITECTURE (subflow) - - - step-2-arch - Arch subflow: read → interview → validate → design → stubs - system-architect - - - - complete - - - - blocked - - - - Arch Cycle (arch-cycle.yaml) - - - read - - interview - gap analysis - - validate - ADR approval - - design - stubs + model - - stubs - - - - ready - - - - - - - adrs_drafted == "true" - - adrs_approved == "true" - - stubs_written == "true" - - - - gaps-found - - - - adrs-rejected - - - - exit: blocked - - - - exit: complete - - - - - STEP 3 — TDD LOOP (subflow) - - - step-3-working - TDD subflow: setup → red → green → refactor - software-engineer - - - - complete - - - - blocked - - - - TDD Cycle (tdd-cycle.yaml) - - setup - - red - - green - - refactor - - - - branch-ready - - - - - - more-ids → red - - - - exit: blocked - - - - exit: complete - - - - - STEP 4 — VERIFY - - - step-4-ready - Adversarial review — system-architect - - - all_tests_pass - == "true" - - - - complete - - - - rejected - - - - - STEP 5 — ACCEPT - - - step-5-ready - Demo & validate — product-owner - - - review_approved - == "true" - - - approved - - - step-5-merge - Merge to main with --no-ff — software-engineer - - - acceptance_passed - == "true" - - - accepted - - - step-5-complete - Move feature to completed/ — product-owner - - - merge_complete - == "true" - - - merged - - - - feature-archived → idle - - - - failed - - - - - POST-MORTEM (failure restart) - - - post-mortem - Write post-mortem, create fix branch - product-owner + software-engineer - - - - restart → step-2-arch - - - - - Parent Flow Transitions (feature-flow.yaml) - - - - - From → To - Trigger (exit name for subflows) - Contract (requires) - - - idle → step-1-scope - select-feature / discover - - - - step-1-scope → step-2-arch - complete - - - - step-1-scope → idle - blocked - - - - step-2-arch → step-3-working - complete - - - - step-2-arch → step-1-scope - blocked - - - - step-3-working → step-4-ready - complete - - - - step-3-working → step-1-scope - blocked - - - - step-4-ready → step-5-ready - approved - all_tests_pass == "true" - - - step-4-ready → step-3-working - rejected - - - - step-5-ready → step-5-merge - accepted - review_approved == "true" - - - step-5-merge → step-5-complete - merged - acceptance_passed == "true" - - - step-5-complete → idle - feature-archived - merge_complete == "true" - - - step-5-ready → post-mortem - failed - - - - post-mortem → step-2-arch - restart - - - - - - Subflow State Details - - - - - scope-cycle (Step 1) — exits: complete, blocked - backlog-criteria → [exit:complete] (criteria-done) - discovery → stories (baselined) | discovery → discovery (more-discovery) - stories → criteria (stories-committed) | criteria → [exit:complete] (criteria-committed) - - - - - arch-cycle (Step 2) — exits: complete, blocked - read → [exit:blocked] (spec-gap) | read → interview (ready) - interview → interview (gaps-found) | interview → validate (adrs-drafted) - validate → interview (adrs-rejected) | validate → design (adrs-approved) - design → stubs (stubs-written) | stubs → [exit:complete] (test-fast-green) - - - - - tdd-cycle (Step 3) — exits: complete, blocked - setup → red (branch-ready | branch-exists) | red → [exit:blocked] (spec-gap) - red → green (test-written) | green → refactor (test-passing) - refactor → red (more-ids) | refactor → [exit:complete] (all-green) - \ No newline at end of file diff --git a/docs/branding/branding.md b/docs/branding/branding.md index d57de8d..00f97e5 100644 --- a/docs/branding/branding.md +++ b/docs/branding/branding.md @@ -20,23 +20,24 @@ Agents read this file before generating release names, C4 diagrams, README banne The palette is drawn from flour, wheat, and crust — the transformation from raw grain to structured form. Every colour choice serves legibility first; decoration is secondary. -- **Background/flour:** `#faf8f3` → `#f0ebe3` — flour white, the canvas for state diagrams -- **Primary text:** `#3d2b1f` → `#2a1a10` — dark bran, the grain that carries weight -- **Accent/crust:** `#c49a3c` → `#daa840` — golden crust, used for borders, arrows, structural lines — never body text -- **Secondary/malt:** `#6b8f71` → `#4a7a50` — malt green, for states, labels, secondary hierarchy -- **Stone/bran:** `#e8e2d8` → `#c4baa8` — the structural colour; table borders, diagram dividers +- **Background/flour:** `#ffffff` → `#f8fafc` — clean white, the canvas for state diagrams +- **Primary text:** `#0f172a` → `#475569` — slate dark, the grain that carries weight +- **Accent/crust:** `#3b82f6` → `#60a5fa` — blue accent, used for borders, arrows, structural lines — never body text +- **Secondary/malt:** `#10b981` — emerald green, for states, labels, secondary hierarchy +- **Stone/bran:** `#e2e8f0` → `#f1f5f9` — the structural colour; table borders, diagram dividers +- **Supplementary:** `#8b5cf6` (purple), `#14b8a6` (teal), `#f59e0b` (amber), `#ef4444` (danger) - **Logo:** `docs/assets/logo.svg` - **Banner:** `docs/assets/banner.svg` -> Dark bran `#2a1a10` on flour `#faf8f3` achieves >12:1 contrast (WCAG AAA). Crust gold is decorative; it never carries meaning that must be read. +> Slate `#0f172a` on white `#ffffff` achieves >18:1 contrast (WCAG AAA). Blue accent is decorative; it never carries meaning that must be read. ### Logo -A grain-to-graph mark — a single wheat grain at top (organic, rounded) that transforms into three branching paths below (geometric, directional), ending in open circles representing states. The grain IS the graph: the raw material becomes structure. Dark bran `#3d2b1f` for paths, grain outline, and seed line; crust gold `#c49a3c` for grain fill. Transparent background. +A seed-to-graph mark — a filled blue circle at top (the seed, the initial state) that branches into three curved paths below, ending in open circles representing reachable states. The seed IS the graph: the raw material becomes structure. Slate `#0f172a` for paths and outlines; blue accent `#3b82f6` for the seed fill. Transparent background. ### Banner -Flour `#faf8f3` background. Centred `flowr` wordmark in a clean sans-serif with letter-spacing — `flow` in dark bran `#2a1a10`, `r` in crust gold `#c49a3c`. A thin crust-gold rule below the title. No logo mark, no subtitle, no vertical divider. +White `#ffffff` background. Centred `flowr` wordmark in a clean sans-serif with letter-spacing — `flow` in slate `#0f172a`, `r` in blue accent `#3b82f6`. A thin blue-accent rule below the title. No logo mark, no subtitle, no vertical divider. ## Release Naming diff --git a/docs/features/in-progress/remove-fuzzy-match-operator.feature b/docs/features/in-progress/remove-fuzzy-match-operator.feature new file mode 100644 index 0000000..76adb2f --- /dev/null +++ b/docs/features/in-progress/remove-fuzzy-match-operator.feature @@ -0,0 +1,84 @@ +Feature: Remove Fuzzy Match (~=) Operator + + The `~=` (APPROXIMATELY_EQUAL) operator provides 5% tolerance numeric matching + in guard conditions. It is unused in practice and adds unnecessary complexity + to the specification and codebase. This feature removes it entirely from the + flowr specification, reference implementation, tests, and documentation. + + Status: BASELINED (2026-05-06) + + Rules (Business): + - The `~=` operator is not a valid condition operator in flowr v1 + - Flows containing `when: { field: "~=value" }` produce a validation error + - The specification documents list exactly 6 operators: ==, !=, >=, <=, >, < + + Constraints: + - Error messages follow existing FlowParseError conventions with location context + + ## Frozen Examples Rule + + After a feature is BASELINED, all `Example:` blocks are immutable. Changes require + `@deprecated` on the old Example (preserving the original @id) and a new Example + with a new @id. This prevents scope creep and maintains traceability. + + `@id` tags are for traceability only — do NOT add priority tags (e.g. @must, @should, + @could) to Examples. MoSCoW classification is an internal triage step, not a Gherkin tag. + + ## Pre-mortem + + Imagine this feature was built and all tests pass, but it doesn't work for the user. + + | Failure Mode | Risk | Covered By | + |-------------|------|------------| + | `~=` silently accepted as bare string value (implicit `==`) instead of raising error | High — user gets no feedback that their flow is wrong | @id:7aef4c1b | + | APPROXIMATELY_EQUAL member persists in ConditionOperator enum | Medium — dead code, potential future misuse | @id:3170064f | + | Spec docs or glossary still list ~= as valid operator (e.g. Guard Condition entry) | Medium — contradicts implementation | @id:817a1558 | + | ADR left without deprecation context — future readers don't know ~= was removed | Low — documentation hygiene | @id:452ceae3 | + | Glossary "Fuzzy Match" entry not marked retired (append-only glossary) | Low — glossary convention, not operator table | In scope of implementation but not a separate Example; covered by 003's scope including glossary.md | + + All failure modes have corresponding Examples. No additional Examples needed. + + ## Questions + + | ID | Question | Status | Answer / Assumption | + |----|----------|--------|---------------------| + | Q1 | Should ~= produce a specific error or fall through to implicit ==? | Resolved | Clear parse error following FlowParseError convention | + | Q2 | Should we supersede the ADR? | Resolved | Add deprecation note to existing ADR only | + | Q3 | Should docs/index.html be updated? | Resolved | No, out of scope (already omits ~=) | + + ## Changes + + | Session | Q-IDs | Change | + |---------|-------|--------| + | 2026-05-06 S1 | Q1-Q3 | Created: remove ~= from code, tests, spec docs, ADR | + + Rule: Remove ~= operator from specification and implementation + As a flow author + I want the ~= operator removed from the flowr specification + So that the specification is simpler with only operators I actually need + + @id:7aef4c1b + Example: ~= operator is not recognized + Given a flow file with `when: { score: "~=100" }` + When the flow is loaded + Then a FlowParseError is raised indicating ~= is not a valid operator + + @id:3170064f + Example: ConditionOperator enum has 6 operators + Given the ConditionOperator enum + When its values are listed + Then it contains exactly EQUALS, NOT_EQUALS, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, GREATER_THAN, LESS_THAN + And does not contain APPROXIMATELY_EQUAL + + @id:817a1558 + Example: Specification documents list 6 operators + Given the specification documents (flow_definition_spec.md, glossary.md, product_definition.md) + When the operator list is checked + Then exactly 6 operators are listed: ==, !=, >=, <=, >, < + And ~= does not appear in any operator table or definition + + @id:452ceae3 + Example: Fuzzy match ADR has deprecation note + Given ADR_20260426_fuzzy_match_algorithm.md + When the document is read + Then a deprecation notice is present indicating ~= has been removed from the specification diff --git a/docs/index.html b/docs/index.html index bbcbaf1..ee9bea7 100644 --- a/docs/index.html +++ b/docs/index.html @@ -3,241 +3,1266 @@ - flowr — Documentation + flowr Specification 1.0.0 + + -
-
- - - - - - - - - - - - -
-
-

flowr

-

Non-deterministic state machine specification to knead workflows.

-
-
- -
- -

Stakeholder

- + -

Architect

- +
+
+

Introduction

+

+ In software systems, workflows are state machines. Development cycles, review processes, deployment pipelines, content moderation. All are sequences of states with transitions governed by conditions. Yet no declarative, validatable format exists for defining these state machines independently of their execution. +

+

+ This specification proposes a YAML format for non-deterministic state machine workflows. A flow definition declares states, transitions, and exit contracts. Transitions may be gated by evidence-based conditions. States may invoke subflows with call-stack semantics. Within-flow cycles are permitted for iterative workflows. The validator checks structural integrity. Tools query, track, and visualise. No execution engine. No side effects. Pure structure. +

+

+ This is not a new or revolutionary idea. State machines are well-understood. The gap is not in the concept but in the format: no declarative, validatable YAML standard exists for non-deterministic state machine workflows that treats validation as a first-class concern independent of execution. By giving a precise definition to this format, it becomes possible to build shared tooling (validators, editors, visualisers, session trackers) that work across any project that adopts the specification. The format is the foundation. +

+
+ +
+

Overview

+

A complete flow definition with all structural elements highlighted using the colour guide above.

+
flow: deploy                        # unique name
+version: 1.0.0                     # semver
+params:                             # optional parameters
+  - environment
+exits: [deployed, failed]            # declared outcomes
+attrs:                              # opaque metadata
+  owner: SE                        # extension field
+  git: feature                     # extension field
+
+states:
+  - id: build                       # unique within flow
+    next:
+      ok: test
+      fail: failed
+
+  - id: test
+    attrs:
+      timeout: 300
+    conditions:                       # named condition groups
+      quality:
+        coverage: ">=80"
+    next:
+      pass:
+        to: staging
+        when: quality             # named condition ref
+      fail: failed
+
+  - id: staging
+    flow: smoke-test               # subflow invocation
+    flow-version: "^1"
+    next:
+      pass: deployed
+      fail: review
+
+  - id: review
+    next:
+      retry: staging                # cycle back
+      abort: failed
+
+ +
+

Specification

+

The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.

+ +

Top-Level Structure

+
    +
  1. +

    A flow definition MUST contain flow (unique name string), version (semver), exits (non-empty list of exit names), and states (ordered list of state objects).

    +
    flow: deploy
    +version: 1.0.0
    +exits: [deployed, failed]
    +states: [...]
    +
  2. +
  3. +

    The first state in states is the initial state. A flow MUST contain at least one state.

    +
  4. +
  5. +

    exits declares the outcomes this flow offers to callers. Every exit name MUST be referenced by at least one state's next mapping. exits MUST be a non-empty list.

    +
  6. +
+ +

States

+
    +
  1. +

    Each state MUST have a unique id within the flow. State ids MUST NOT match any exit name (ambiguous reference).

    +
  2. +
  3. +

    Each state MUST have a next mapping (trigger → target), unless the state only references exits as terminal targets.

    +
  4. +
  5. +

    A state MAY declare attrs: an opaque dict. State-level attrs replace (not merge) flow-level attrs. The specification does not interpret attrs.

    +
    states:
    +  - id: build
    +    attrs:
    +      timeout: 600
    +      docker: true
    +    next:
    +      ok: test
    +
  6. +
+ +

Transitions

+
    +
  1. +

    Every next target MUST resolve to either a state id or an exit name. A target that matches both is a validation error. A target that matches neither is a validation error.

    +
  2. +
  3. +

    A transition MAY include when: a dict of guard conditions. Evidence keys MUST match when keys exactly (closed schema). No extra keys, no missing keys.

    +
    next:
    +  approve:
    +    to: deployed
    +    when: { score: ">=80" }
    +
  4. +
  5. +

    Guard conditions use expression operators: == (equality), != (inequality), >= <= > < (numeric comparison). A plain value without an operator prefix is an implicit == (e.g., status: approved is equivalent to status: ==approved). Numeric extraction is applied to both sides: >=80% vs evidence 75% compares 80 vs 75. Multiple conditions in a when dict are AND-combined. No inheritance. Every condition is explicit on the transition where it applies.

    +
    +
    Rule 9. Guarded transition
    +
    +
    +
  6. +
+ +

Named Condition Groups

+
    +
  1. +

    A state MAY declare conditions: a mapping of named condition groups. Each group is a condition-map (key-value pairs of expressions). Named groups are referenced by transitions via when.

    +
    states:
    +  - id: review
    +    conditions:
    +      quality_gate:
    +        score: ">=80"
    +        coverage: ">=90"
    +    next:
    +      approve:
    +        to: published
    +        when: quality_gate        # named ref
    +
  2. +
  3. +

    The when field on a transition accepts three forms: a dict (inline condition-map), a string (reference to a named group), or a list (mix of named refs and inline dicts). All conditions are AND-combined. A named ref that does not match a group defined on the same state MUST cause a validation error.

    +
    next:
    +  deploy:
    +    to: production
    +    when:
    +      - quality_gate            # named ref
    +      - { override: "==yes" }    # inline
    +  reject: failed
    +
  4. +
+ +

Subflows

+
    +
  1. +

    A state with a flow field becomes a subflow invocation. The flow value is a relative file path. Parent next keys MUST match child exits exactly.

    +
    +
    Rule 12. Subflow nesting
    +
    +
    +
  2. +
  3. +

    Subflows use a call-stack: push on entry, pop on exit. Context is isolated to the current flow. A flow-version field MAY constrain compatible child versions using semver ranges.

    +
  4. +
+ +

Validation

+
    +
  1. +

    A conforming validator MUST check all of the following at load time:

    +
      +
    • Every next target resolves to a state id or exit name
    • +
    • No next target is ambiguous (matches both state id and exit name)
    • +
    • Parent next keys match child exits exactly
    • +
    • No cross-flow cycles (detected via DFS)
    • +
    • Exit names in exits are referenced by at least one state
    • +
    • Named condition references in when resolve to a group defined on the same state
    • +
    • Params without defaults are provided at flow invocation time
    • +
    +
  2. +
+ +

Sessions

+
    +
  1. +

    A session tracks flow, state, name, created_at, updated_at, stack (subflow call stack), and params. Session writes MUST be atomic (temp-file-then-rename). Filesystem is the source of truth.

    +
    flow: deploy
    +state: review
    +name: default
    +created_at: "2026-05-01T10:00:00"
    +updated_at: "2026-05-01T14:25:00"
    +stack: []
    +params: {}
    +
  2. +
+ +

Cycles and Versioning

+
    +
  1. +

    Within-flow cycles are allowed (a state MAY transition to an earlier state in the same flow). Cross-flow cycles are forbidden.

    +
    +
    Rule 16. Within-flow cycle
    +
    +
    +
  2. +
  3. +

    Flows SHOULD use semver for versioning. Adding a new exit is a minor bump. Adding states is a patch. Removing or renaming exits is a major (breaking) change.

    +
  4. +
+ +

Extension Fields

+
    +
  1. +

    A flow definition MAY contain fields not specified in this document. Such extension fields are not interpreted by a conforming validator. The keys defined in this specification (flow, version, params, exits, attrs, states, id, next, to, when, conditions, flow, flow-version) are reserved. Implementations MUST NOT assign semantics to reserved keys beyond what this specification defines.

    +
  2. +
  3. +

    The attrs field is the designated extension point. Implementations SHOULD place implementation-specific data (agent assignments, tool configuration, environment variables) inside attrs rather than as top-level keys. This keeps the structural contract separate from tool-specific configuration.

    +
  4. +
+
+ +
+

Formal Syntax

+

The following grammar defines valid flow definition structure:

+
+<flow-definition> ::= + flow: <string> + version: <semver> + params: <param-list>? + exits: [<exit-name>, ...] + attrs: <mapping>? + states: [<state>, ...] + +<state> ::= + id: <identifier> + next: <transition-map> + conditions: <condition-groups>? # named condition groups + flow: <flow-reference>? # subflow invocation + flow-version: <semver-range>? + attrs: <mapping>? # replaces flow-level + +<transition-map> ::= + <trigger>: <target> # simple + | <trigger>: { to: <target>, when: <when-clause> } # guarded + +<target> ::= <state-id> | <exit-name> + +<when-clause> ::= + <condition-map> # inline dict + | <identifier> # named ref to condition group + | [<when-clause>, ...] # list: AND-combined + +<condition-groups> ::= + { <identifier>: <condition-map>, ... } + +<condition-map> ::= + { <key>: <expression>, ... } + +<expression> ::= + <value> # implicit == (exact match) + | ==<value> # equality + | !=<value> # inequality + | >=<numeric> # greater or equal + | <=<numeric> # less or equal + | ><numeric> # greater than + | <<numeric> # less than + +<param-list> ::= + [<string>, ...] # required params + | [{ name: <string>, default: <value>? }, ...] # optional with default + +<extension-fields> ::= + <string>: <value> # any non-reserved key + # reserved: flow, version, params, + # exits, attrs, states, id, next, + # to, when, conditions, flow, flow-version +
+
+ +
+

Visual Reference

+

The following diagrams illustrate valid structural patterns. They are normative: they depict what the specification permits.

+ +

Minimal Flow

+

The smallest valid flow: one state, one exit, one transition.

+
+
+
+ +

Non-Deterministic Branching

+

A state with multiple outgoing transitions. The actor chooses which path to take, not the machine. This is what makes flowr non-deterministic.

+
+
+
+ +

Guarded Transition

+

A transition that requires evidence. The actor sends a trigger with evidence, and the condition engine validates it. The guarded path is visually distinct.

+
+
+
+ +

Subflow Invocation

+

A parent state invokes a child flow. The call-stack pushes on entry, pops on exit. Parent next keys match child exits.

+
+
+
+ +

Within-Flow Cycle

+

A state transitions back to an earlier state in the same flow. Permitted. Cross-flow cycles are forbidden.

+
+
+
+
+ +
+

Normative Examples

+

Complete, valid flow definitions that serve as reference implementations.

+ +

1. Deploy Pipeline

+

A linear flow with exits. The first state is initial. States reference exits in next.

+
+
+
flow: deploy
+version: 1.0.0
+exits: [deployed, failed]
+
+states:
+  - id: prepare
+    next:
+      ready: execute
+
+  - id: execute
+    next:
+      success: deployed
+      error: failed
+
+
+
+
+
+
+
+ +

2. Review with Guard Conditions

+

Guarded transitions requiring evidence. Both approve and reject are gated. The actor provides evidence and the engine validates it.

+
+
+
flow: review
+version: 1.0.0
+exits: [approved, rejected]
+
+states:
+  - id: pending
+    next:
+      submit: under-review
+
+  - id: under-review
+    next:
+      approve:
+        to: approved
+        when: { score: ">=80" }
+      reject:
+        to: rejected
+        when: { score: "<40" }
+
+
+
+
+
+
+
+ +

3. TDD Cycle (Within-Flow Cycle)

+

States loop back to earlier states. The red state is revisited for each new test example.

+
+
+
flow: tdd-cycle
+version: 1.0.0
+exits: [all_green, blocked]
+
+states:
+  - id: red
+    next:
+      test_written: green
+      blocked: blocked
+
+  - id: green
+    next:
+      test_passes: refactor
+
+  - id: refactor
+    next:
+      next_example: red
+      all_pass: all_green
+
+
+
+
+
+
+
+ +

4. Feature Flow (Subflow Invocation)

+

A parent flow invokes a child flow via flow:. Parent next keys match child exits exactly.

+
+
+
# Parent
+flow: feature-flow
+version: 1.0.0
+exits: [completed, cancelled]
+
+states:
+  - id: scope
+    flow: scope-cycle
+    next:
+      complete: build
+      blocked: cancelled
+
+  - id: build
+    next:
+      done: completed
+
+
+
+
+
+
+
+
+ +
+

Conformance

+

A conforming implementation MUST satisfy all rules marked MUST in this specification. A conforming implementation SHOULD satisfy all rules marked SHOULD unless there is a documented reason not to.

+

Conformance levels:

+ + + + + + + + + +
LevelMeaningRequirement
MUSTRequired for all conforming implementationsImmutable loaded flows, closed evidence schema, validation rules
SHOULDRecommended but optionalFilesystem wins over session cache on conflict, semver for flows
MAYOptional extensionPer-state attrs, flow params, Mermaid export
+
+ +
+

FAQ

+ +
+

Why not BPMN, SCXML, Serverless Workflow, XState, or Temporal?

+

Existing solutions target execution engines or are framework-specific. They define what the workflow does: side effects, retries, timeouts, error handling. flowr defines what the workflow is: its structure, states, transitions, and guard conditions. By staying agnostic to execution, any tool (editors, visualizers, CI systems, AI agents) can build on the same structural foundation without coupling to a runtime.

+
+ +
+

Why YAML and not JSON or XML?

+

YAML is the most human-readable format for nested structures with string keys. It supports comments (needed for specification examples), requires less punctuation than JSON, and is more concise than XML. The format prioritises authoring experience.

+
+ +
+

Why no execution engine?

+

flowr defines what a workflow is, not what it does. Execution engines are opinionated. They prescribe side effects, error handling, retry logic. flowr stays agnostic so any tool (editors, visualizers, CI systems, AI agents) can build on the same structural foundation without coupling to a runtime.

+
+ +
+

How do I handle parallel states?

+

Parallel (fork-join) states are out of scope for v1. A flow is a sequence of states with transitions. If your workflow needs parallelism, model each branch as a separate subflow and coordinate through a parent flow.

+
+ +
+

Can a state have no next?

+

A state MUST have next unless it only references exits as terminal targets. Every state must declare where it can go, even if the only option is an exit.

+
+ +
+

What happens if evidence doesn't match a condition?

+

The transition is blocked. The actor receives a warning indicating which conditions failed and what evidence is required. No state change occurs.

+
+ +
+

How do subflow versions work?

+

flow-version accepts a semver range (e.g., "^1"). The validator checks that the referenced flow's version satisfies the constraint. Breaking changes (renamed exits) require a major version bump.

+
+ +
+

Why do attrs replace instead of merge?

+

Merge semantics require defining deep-merge rules, conflict resolution, and precedence, adding complexity without universal benefit. Replace is unambiguous: if a state declares attrs, those are its attrs. If it doesn't, it inherits flow-level attrs.

+
+ +
+

Can I use flowr without the CLI?

+

Yes. flowr is a specification first. The CLI is a reference implementation. Any tool can parse the YAML format and implement validation, querying, or visualisation independently. The specification is the contract, not the tool.

+
+ +
+

Can I add custom fields to a flow definition?

+

Yes. Fields not defined in this specification are extension fields and are not interpreted by a conforming validator. The reserved keys (flow, version, params, exits, attrs, states, id, next, to, when, conditions, flow, flow-version) belong to the spec. Place implementation-specific data inside attrs to keep the structural contract clean.

+
+
+ + + + + - \ No newline at end of file + diff --git a/docs/interview-notes/IN_20260426_spec_design.md b/docs/interview-notes/IN_20260426_spec_design.md index ddda6e0..f1dadc2 100644 --- a/docs/interview-notes/IN_20260426_spec_design.md +++ b/docs/interview-notes/IN_20260426_spec_design.md @@ -3,7 +3,7 @@ > **Status:** COMPLETE > **Interviewer:** PO > **Participant(s):** Stakeholder -> **Session type:** Domain deep-dive +> **Session type:** Domain walkthrough --- diff --git a/docs/interview-notes/IN_20260506_remove-fuzzy-match.md b/docs/interview-notes/IN_20260506_remove-fuzzy-match.md new file mode 100644 index 0000000..60ba189 --- /dev/null +++ b/docs/interview-notes/IN_20260506_remove-fuzzy-match.md @@ -0,0 +1,52 @@ +# IN_20260506_remove-fuzzy-match — Remove ~= operator from specification + +> **Status:** COMPLETE +> **Interviewer:** PO +> **Participant(s):** Stakeholder +> **Session type:** Feature specification + +--- + +## Feature: remove-fuzzy-match-operator + +| ID | Question | Answer | +|----|----------|--------| +| Q1 | What is changing? | Complete removal of the `~=` (APPROXIMATELY_EQUAL) operator from the flowr specification and reference implementation. The 5% tolerance numeric matching operator will no longer be a valid condition operator. | +| Q2 | Why remove it? | Unused in practice. Adds complexity to the specification and codebase without providing value. The concept is over-engineered for what flowr needs. | +| Q3 | What defines success? | `~=` removed from: `ConditionOperator` enum, `_OPERATOR_PREFIXES` list, `_compare_numeric` function, all tests, spec docs (`flow_definition_spec.md`), glossary (`glossary.md`), system.md, product_definition.md. Existing ADR gets a deprecation note. Flows using `~=` produce a clear `FlowParseError` following current error conventions. | +| Q4 | How should flows using ~= fail? | Clearest approach using current conventions. The `FlowParseError` pattern (e.g. `f"Unknown condition reference '{name}'..."`) should be followed. After removal, `~=` will simply not be recognized as a valid operator prefix, so `parse_condition` will treat it as a bare value (implicit `==`). A validation-level check may be needed to catch it explicitly. | +| Q5 | What must never happen? | Silent acceptance of `~=` as a valid operator after removal. | +| Q6 | What about the ADR? | Add a deprecation note to `ADR_20260426_fuzzy_match_algorithm.md`. Do not supersede with a new ADR. | +| Q7 | What's out of scope? | `docs/index.html` is out of scope (already omits `~=`). No changes to the spec page. | + +--- + +## Scope Confirmation + +| Artifact | Action | +|----------|--------| +| `flowr/domain/condition.py` | Remove `APPROXIMATELY_EQUAL` enum, `"~="` from `_OPERATOR_PREFIXES`, `_compare_numeric` case | +| `tests/unit/condition_test.py` | Remove `~=` parse test (line 64) | +| `tests/features/.../condition_operators_test.py` | Remove/update `~=` feature tests (lines 68-122, including 2 skipped tests) | +| `docs/spec/flow_definition_spec.md` | Remove `~=` from operator table (line 250), examples (lines 85, 91), note (line 256), v1 scope (line 378) | +| `docs/spec/glossary.md` | Remove fuzzy-match term (lines 90-96), remove `~=` from guarded-transition definition (line 102) | +| `docs/spec/system.md` | Remove `~=` references (lines 137, 159, 195) | +| `docs/spec/product_definition.md` | Remove `~=` from expression list (line 18) | +| `docs/adr/ADR_20260426_fuzzy_match_algorithm.md` | Add deprecation note at top | +| `docs/index.html` | **Out of scope** (already omits `~=`) | +| `.flowr/flows/*.yaml` | No changes needed (no flows use `~=`) | + +--- + +## Quality Attributes + +| ID | Attribute | Scenario | Target | Priority | +|----|-----------|----------|--------|----------| +| QA1 | Correctness | When a flow file contains `when: { value: "~=100" }`, the validator rejects it | Clear error message following FlowParseError convention | Must | + +--- + +## Action Items + +- [ ] Write feature file with BDD scenarios covering code removal and doc updates +- [ ] Implement removal via TDD cycle diff --git a/docs/post-mortem/PM_20260501_skipping-docs-before-planning.md b/docs/post-mortem/PM_20260501_skipping-docs-before-planning.md index 5b9836f..9996b2d 100644 --- a/docs/post-mortem/PM_20260501_skipping-docs-before-planning.md +++ b/docs/post-mortem/PM_20260501_skipping-docs-before-planning.md @@ -6,7 +6,7 @@ Planning flow — the agent attempted to enter feature-selection and feature-spe ## Root Cause -The agent treated file loss as a signal to "move forward anyway" rather than a signal to "reconstruct prerequisites first." The discovery-flow produces these documents through its states (interview notes → event storming → language definition → domain modeling → scope boundary → product definition). The agent skipped the entire discovery flow, jumped to planning, and started writing feature specs and code without ensuring the required inputs existed. Additionally, the agent created `flowr/infrastructure/config.py` and `flowr/infrastructure/session_store.py` as production code — work that should only happen during the TDD cycle in the development flow, not during discovery or planning. +The agent treated file loss as a signal to "move forward anyway" rather than a signal to "reconstruct prerequisites first." The discovery-flow produces these documents through its states (interview notes → event storming → language definition → domain modeling → scope boundary → product definition). The agent skipped the entire discovery flow, jumped to planning, and started writing feature specs and code without ensuring the required inputs existed. The agent also created `flowr/infrastructure/config.py` and `flowr/infrastructure/session_store.py` as production code — work that should only happen during the TDD cycle in the development flow, not during discovery or planning. ## Missed Gate diff --git a/docs/post-mortem/PM_20260502_agent-role-violation-tdd.md b/docs/post-mortem/PM_20260502_agent-role-violation-tdd.md index 1edd11d..94b2cb4 100644 --- a/docs/post-mortem/PM_20260502_agent-role-violation-tdd.md +++ b/docs/post-mortem/PM_20260502_agent-role-violation-tdd.md @@ -31,7 +31,7 @@ The orchestrator skipped the dispatch step and collapsed all three roles into on The **agent dispatch protocol** was violated. Every flow state has an `owner` field that names the responsible agent. The orchestrator must dispatch to that agent with the listed skills loaded — never perform the work directly. -Additionally, the **TDD minimum rule** was violated: the SE writes only the minimum production code needed to make the failing test pass. Convention fixes (exception renaming, line length, docstrings, import cleanup) are not minimum — they are review concerns. +The **TDD minimum rule** was also violated: the SE writes only the minimum production code needed to make the failing test pass. Convention fixes (exception renaming, line length, docstrings, import cleanup) are not minimum — they are review concerns. ## Fix diff --git a/docs/post-mortem/PM_20260502_flow-bookkeeping-skipped.md b/docs/post-mortem/PM_20260502_flow-bookkeeping-skipped.md index 90925ef..2932267 100644 --- a/docs/post-mortem/PM_20260502_flow-bookkeeping-skipped.md +++ b/docs/post-mortem/PM_20260502_flow-bookkeeping-skipped.md @@ -17,7 +17,7 @@ The agent treated the development work (TDD, review, commit, merge) as the finis The agent manually wrote the session YAML file for session-management-core (`flow: development-flow, state: project-structuring`) instead of using `flowr check` → `flowr transition` to drive state changes. When the implementation was complete, the agent did not transition through commit → done → feature-development completed → acceptance → delivery. -Additionally, feature files are supposed to move from `backlog/` to `in-progress/` when development starts, and from `in-progress/` to `completed/` when accepted. The agent never moved any feature files. +Feature files are also supposed to move from `backlog/` to `in-progress/` when development starts, and from `in-progress/` to `completed/` when accepted. The agent never moved any feature files. ## Missed Gate diff --git a/docs/post-mortem/PM_20260502_partial-delivery-without-approval.md b/docs/post-mortem/PM_20260502_partial-delivery-without-approval.md index a392114..97057df 100644 --- a/docs/post-mortem/PM_20260502_partial-delivery-without-approval.md +++ b/docs/post-mortem/PM_20260502_partial-delivery-without-approval.md @@ -42,7 +42,7 @@ The stakeholder was never asked: "Is it acceptable to deliver session management The `break-down-feature` and `write-bdd-features` planning states should validate that the decomposition preserves the stakeholder's intent. Splitting a feature requires stakeholder approval — the agent must ask, not assume. -Additionally, the `confirm-baseline` state should verify that the baselined feature file covers all requirements from the interview notes. A gap analysis between interview Q&As and the feature file's @id examples would have caught this. +The `confirm-baseline` state should also verify that the baselined feature file covers all requirements from the interview notes. A gap analysis between interview Q&As and the feature file's @id examples would have caught this. ## Fix diff --git a/docs/post-mortem/PM_20260505_subflow-mechanism-non-functional.md b/docs/post-mortem/PM_20260505_subflow-mechanism-non-functional.md index 60c5a13..68f79fd 100644 --- a/docs/post-mortem/PM_20260505_subflow-mechanism-non-functional.md +++ b/docs/post-mortem/PM_20260505_subflow-mechanism-non-functional.md @@ -26,7 +26,7 @@ The session-management feature was implemented in two phases (core + extended) a | Transition inside discovery-flow | Move to next state | Success (no subflow needed) | — | | Exit discovery-flow → enter architecture-flow | `main-flow/architecture` → enter `architecture-flow/architecture-assessment` | Session at invalid state `main-flow/complete` | Exit uses exit name directly, not parent transition target | -Additionally, even if the subflow mechanism had worked, the agent UX would have been broken: +Even if the subflow mechanism had worked, the agent UX would have been broken: - `next` showed only target state names without trigger names — agents couldn't discover which trigger to use - `next` hid guarded/blocked transitions — agents saw fewer options than actually available diff --git a/docs/spec/flow_definition_spec.md b/docs/spec/flow_definition_spec.md index 744e758..c41becc 100644 --- a/docs/spec/flow_definition_spec.md +++ b/docs/spec/flow_definition_spec.md @@ -82,13 +82,13 @@ states: next: approve: to: approved - when: { score: ">=80%", verdict: "~=pass" } + when: { score: ">=80%", verdict: "!=fail" } reject: to: rejected when: { verdict: "!=pass" } ``` -Actor sends trigger `approve` with evidence `{ score: "92%", verdict: "passing" }` → condition `>=80%` extracts 92 vs 80 (pass), `~=pass` fuzzy-matches "passing" (pass) → transition fires. +Actor sends trigger `approve` with evidence `{ score: "92%", verdict: "passing" }` → condition `>=80%` extracts 92 vs 80 (pass), `!=fail` checks "passing" != "fail" (pass) → transition fires. ### Within-Flow Cycle @@ -247,13 +247,12 @@ The `when` dict uses expression strings as values: | `<=N` | Less than or equal | `<=5` | | `>N` | Greater than | `>0` | | `=80%` vs evidence `75%` → compares 80 vs 75). Plain strings without operators are treated as `==value`. Evidence keys must exactly match `when` keys — closed schema, no extra or missing keys. -**Note:** All evidence values are coerced to strings before comparison. YAML booleans become lowercase (`True` → `"true"`, `False` → `"false"`), YAML numbers become numeric strings (`80` → `"80"`). The `~=` operator applies ONLY to numeric values (5% tolerance); it is not valid for string matching. See ADR_20260426_evidence_type_system and ADR_20260426_fuzzy_match_algorithm. +**Note:** All evidence values are coerced to strings before comparison. YAML booleans become lowercase (`True` → `"true"`, `False` → `"false"`), YAML numbers become numeric strings (`80` → `"80"`). See ADR_20260426_evidence_type_system. --- @@ -375,4 +374,4 @@ Fields: `flow` (current flow name — changes when entering subflows), `state` ( | `agent` (state-level) | `attrs` (opaque) | Project-specific, not a library concern | | `exits` conditional | `exits` always required | Exits are a contract; must be declared | | No `version` | `version` required | Enables semver compatibility checks | -| No `~=` operator | `~=` added | Fuzzy match for approximate comparisons | \ No newline at end of file +| No `~=` operator | `~=` added then removed | Fuzzy match removed — unused, added complexity | \ No newline at end of file diff --git a/docs/spec/glossary.md b/docs/spec/glossary.md index bea28b3..e646b60 100644 --- a/docs/spec/glossary.md +++ b/docs/spec/glossary.md @@ -87,19 +87,21 @@ Entries are sorted alphabetically. ## Fuzzy Match -**Definition:** The `~=` condition operator that performs approximate numeric matching; passes if the evidence value is within 5% of the condition value after numeric extraction. +**Status:** RETIRED (2026-05-06) — The `~=` operator has been removed from the flowr specification. See feature `remove-fuzzy-match-operator` and ADR_20260426_fuzzy_match_algorithm (deprecated). -**Aliases:** approximate match, ~= operator +**Definition:** ~~The `~=` condition operator that performs approximate numeric matching; passes if the evidence value is within 5% of the condition value after numeric extraction.~~ No longer part of the specification. -**Example:** "The condition `~=100` with evidence `97` passes because |97 - 100| / 100 = 0.03 ≤ 0.05 — within 5% tolerance." +**Aliases:** ~~approximate match, ~= operator~~ -**Source:** 2026-04-26 — Session 2 (Q20); ADR_20260426_fuzzy_match_algorithm +**Example:** ~~"The condition `~=100` with evidence `97` passes because |97 - 100| / 100 = 0.03 ≤ 0.05 — within 5% tolerance."~~ + +**Source:** 2026-04-26 — Session 2 (Q20); ADR_20260426_fuzzy_match_algorithm. Retired 2026-05-06. --- ## Guard Condition -**Definition:** A `when` clause on a transition that specifies conditions which must all be satisfied (AND-combined) for the transition to fire; conditions use operators like `==`, `!=`, `>=`, `<=`, `>`, `<`, and `~=`. +**Definition:** A `when` clause on a transition that specifies conditions which must all be satisfied (AND-combined) for the transition to fire; conditions use operators `==`, `!=`, `>=`, `<=`, `>`, `<`. **Aliases:** when clause, transition guard diff --git a/docs/spec/product_definition.md b/docs/spec/product_definition.md index 230a612..4eedac1 100644 --- a/docs/spec/product_definition.md +++ b/docs/spec/product_definition.md @@ -15,7 +15,7 @@ - A **session manager** that tracks workflow state across CLI invocations (init, show, set-state, transition, list) - A **Mermaid converter** that generates state diagrams from flow definitions - **Enforces valid transitions** — lists available next steps AND rejects invalid ones -- **Verifies guard conditions** at transition time using simple expressions (`==`, `!=`, `>=`, `<=`, `>`, `<`, `~=`) against closed evidence schemas +- **Verifies guard conditions** at transition time using simple expressions (`==`, `!=`, `>=`, `<=`, `>`, `<`) against closed evidence schemas - **Validates structural constraints** — missing fields, ambiguous targets, cross-flow cycles, subflow exit contracts ## What flowr IS NOT @@ -177,6 +177,33 @@ These gates supplement the general Definition of Done above. All must pass befor - [ ] `ruff check` and `ruff format` pass with zero errors - [ ] `mypy` type-checking passes with no new errors +### Feature-Specific Definition of Done: remove-fuzzy-match-operator + +These gates supplement the general Definition of Done above. All must pass before this feature is considered complete. + +**Design Correctness:** + +- [ ] All 4 BDD scenarios pass (remove-fuzzy-match-001, remove-fuzzy-match-002, remove-fuzzy-match-003, remove-fuzzy-match-004) +- [ ] `~=` operator produces a `FlowParseError` with location context — not silently accepted as a bare string value (remove-fuzzy-match-001) +- [ ] `ConditionOperator` enum contains exactly 6 members: `EQUALS`, `NOT_EQUALS`, `GREATER_THAN_OR_EQUAL`, `LESS_THAN_OR_EQUAL`, `GREATER_THAN`, `LESS_THAN` — no `APPROXIMATELY_EQUAL` (remove-fuzzy-match-002) +- [ ] Specification documents (`flow_definition_spec.md`, `glossary.md`, `product_definition.md`, `system.md`) list exactly 6 operators (`==`, `!=`, `>=`, `<=`, `>`, `<`) with zero references to `~=` in operator tables or definitions (remove-fuzzy-match-003) +- [ ] `ADR_20260426_fuzzy_match_algorithm.md` contains a deprecation notice indicating `~=` has been removed from the specification (remove-fuzzy-match-004) +- [ ] No references to `~=` or `APPROXIMATELY_EQUAL` remain in `flowr/domain/condition.py` or any other production module + +**Structure:** + +- [ ] Changes are limited to: `flowr/domain/condition.py` (operator removal), spec docs (operator table updates), ADR (deprecation note) +- [ ] No new modules or public interfaces introduced — this is a removal-only feature +- [ ] Tests cover the error path for `~=` and the enum membership check without coupling to internal implementation details + +**Conventions:** + +- [ ] Error message for `~=` follows existing `FlowParseError` format with location context +- [ ] Glossary "Fuzzy Match" entry marked retired (append-only convention) +- [ ] `ruff check .` passes with zero errors +- [ ] `task test` passes with zero failures +- [ ] Ubiquitous language used consistently: "condition operator" (not "comparison operator" or "match operator") + --- ## Scope Changes diff --git a/docs/spec/system.md b/docs/spec/system.md index 9c828c6..d9985bb 100644 --- a/docs/spec/system.md +++ b/docs/spec/system.md @@ -134,7 +134,7 @@ Developers interact via the CLI (`python -m flowr `) or the Python A - PyYAML is the only runtime dependency — all flow definition and session parsing uses `yaml.safe_load` - All evidence values are coerced to strings before condition evaluation (ADR_20260422_cli_parser_library) -- The ~= operator applies ONLY to numeric values (5% tolerance); no string fuzzy matching (ADR_20260426_fuzzy_match_algorithm) +- The ~= operator has been removed from the specification (ADR_20260426_fuzzy_match_algorithm, deprecated 2026-05-06); six operators remain: ==, !=, >=, <=, >, < - Validator returns ValidationResult with Violation list — no exceptions for validation failures (ADR_20260426_validation_result) - Version format is calver (`major.minor.YYYYMMDD`); tests must not assume semver - CLI exit codes: 0 = success, 1 = command failed, 2 = usage error (ADR_20260426_cli_io_convention) @@ -156,7 +156,7 @@ Developers interact via the CLI (`python -m flowr `) or the Python A - Use `argparse` (stdlib) for CLI parsing — zero new dependencies (ADR_20260422_cli_parser_library) - Read version from `importlib.metadata` at runtime — single source of truth, never hardcoded (ADR_20260422_version_source) - Evidence type system: coerce all evidence values to strings; YAML booleans become lowercase, YAML numbers become numeric strings (ADR_20260426_evidence_type_system) -- Fuzzy match: ~= applies ONLY to numeric values with 5% tolerance; no string fuzzy matching (ADR_20260426_fuzzy_match_algorithm) +- Fuzzy match: ~= removed from specification (2026-05-06); six operators remain: ==, !=, >=, <=, >, < (ADR_20260426_fuzzy_match_algorithm, deprecated) - Validation result: return ValidationResult with list of Violation objects (severity, message, location) — collect all violations at once (ADR_20260426_validation_result) - CLI I/O convention: positional YAML path; --evidence/--evidence-json; 3-tier exit codes (0/1/2); stdout=results/stderr=errors; key-value text output (ADR_20260426_cli_io_convention) - Subflow resolution: flow field is relative file path from root flow directory; `.yaml` extension optional; output as /; subflow exit resolves through parent transition map with chaining support (ADR_20260426_subflow_resolution, amended 2026-05-05) @@ -192,7 +192,7 @@ See `docs/features/` for accepted features. | 2026-04-22 | ADR_20260422_version_source | Version read from importlib.metadata at runtime | Feature cli-entrypoint | | 2026-04-26 | ADR_20260426_cli_io_convention | CLI I/O conventions established | Feature flowr-cli | | 2026-04-26 | ADR_20260426_evidence_type_system | Evidence values coerced to strings | Feature flow-definition-spec | -| 2026-04-26 | ADR_20260426_fuzzy_match_algorithm | ~= operator restricted to numeric values only | Feature flow-definition-spec | +| 2026-04-26 | ADR_20260426_fuzzy_match_algorithm | ~= operator deprecated and removed | Feature remove-fuzzy-match-operator | | 2026-04-26 | ADR_20260426_validation_result | ValidationResult with Violation list | Feature flow-definition-spec | | 2026-04-26 | ADR_20260426_subflow_resolution | Subflow lookup by relative path | Feature flow-definition-spec | | 2026-04-26 | ADR_20260426_condition_inlining | Named condition groups inlined at load time | Feature named-condition-groups | diff --git a/flowr/domain/condition.py b/flowr/domain/condition.py index fecf143..ac86806 100644 --- a/flowr/domain/condition.py +++ b/flowr/domain/condition.py @@ -13,13 +13,11 @@ class ConditionOperator(Enum): LESS_THAN_OR_EQUAL = "<=" GREATER_THAN = ">" LESS_THAN = "<" - APPROXIMATELY_EQUAL = "~=" _OPERATOR_PREFIXES: list[tuple[str, ConditionOperator]] = [ (">=", ConditionOperator.GREATER_THAN_OR_EQUAL), ("<=", ConditionOperator.LESS_THAN_OR_EQUAL), - ("~=", ConditionOperator.APPROXIMATELY_EQUAL), ("==", ConditionOperator.EQUALS), ("!=", ConditionOperator.NOT_EQUALS), (">", ConditionOperator.GREATER_THAN), @@ -62,8 +60,7 @@ def _compare_numeric( return e_num > c_num case ConditionOperator.LESS_THAN: return e_num < c_num - case ConditionOperator.APPROXIMATELY_EQUAL: - return abs(e_num - c_num) / abs(c_num) <= 0.05 + return None # pragma: no cover diff --git a/flowr/domain/loader.py b/flowr/domain/loader.py index ebe7f8c..64ed72d 100644 --- a/flowr/domain/loader.py +++ b/flowr/domain/loader.py @@ -117,6 +117,15 @@ def _dict_to_param(raw: Any) -> Param: # noqa: ANN401 return Param(name=str(raw)) +def _validate_condition_operators(conditions: dict[str, str], state_id: str) -> None: + """Reject unsupported condition operators in when clauses.""" + for _key, value in conditions.items(): + if value.startswith("~="): + raise FlowParseError( + f"Unsupported condition operator '~=' in state '{state_id}'" + ) + + def resolve_when_clause( when_clause: dict[str, str] | list | str, conditions: dict[str, dict[str, str]] | None, @@ -124,6 +133,7 @@ def resolve_when_clause( ) -> tuple[GuardCondition, frozenset[str] | None]: """Resolve a when clause into a GuardCondition and referenced groups.""" if isinstance(when_clause, dict): + _validate_condition_operators(when_clause, state_id) return GuardCondition(conditions=when_clause), None items = [when_clause] if isinstance(when_clause, str) else list(when_clause) @@ -136,6 +146,8 @@ def resolve_when_clause( elif isinstance(item, dict): resolved.update(item) + _validate_condition_operators(resolved, state_id) + return ( GuardCondition(conditions=resolved), frozenset(referenced) if referenced else None, diff --git a/pyproject.toml b/pyproject.toml index c6f8cd1..e4c7a69 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,19 @@ [project] name = "flowr" -version = "0.5.0" +version = "1.0.0" description = "non-deterministic state machine specification to knead workflows" readme = "README.md" requires-python = ">=3.13" license = "MIT" +classifiers = [ + "Intended Audience :: Developers", + + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.13", + "Topic :: Software Development :: Libraries :: Python Modules", + "Topic :: Software Development :: Quality Assurance", + "Typing :: Typed", +] authors = [ { name = "eol", email = "nullhack@users.noreply.github.com" } ] @@ -17,7 +26,8 @@ dependencies = [ [project.urls] Repository = "https://github.com/nullhack/flowr" -Documentation = "https://github.com/nullhack/flowr/tree/main/docs/api/" +Documentation = "https://nullhack.github.io/flowr/" +Spec = "https://nullhack.github.io/flowr/" [project.optional-dependencies] dev = [ diff --git a/tests/features/flow_definition_spec/condition_operators_test.py b/tests/features/flow_definition_spec/condition_operators_test.py index ce2644e..fc4c8d4 100644 --- a/tests/features/flow_definition_spec/condition_operators_test.py +++ b/tests/features/flow_definition_spec/condition_operators_test.py @@ -1,7 +1,5 @@ """Tests for condition operators story.""" -import pytest - from flowr.domain.condition import ( ConditionOperator, evaluate_condition, @@ -63,32 +61,6 @@ def test_flow_definition_spec_7ea0ad82() -> None: ) -def test_flow_definition_spec_980735f8() -> None: - """ - Given: a when condition score: "~=100" and evidence score: "97" - When: the condition is evaluated - Then: the condition is satisfied because 97 is within 5 percent of 100 - """ - assert evaluate_condition( - ConditionOperator.APPROXIMATELY_EQUAL, - "100", - "97", - ) - - -def test_flow_definition_spec_c91e0aaa() -> None: - """ - Given: a when condition score: "~=100" and evidence score: "90" - When: the condition is evaluated - Then: the condition is not satisfied because 90 is more than 5 percent away from 100 - """ - assert not evaluate_condition( - ConditionOperator.APPROXIMATELY_EQUAL, - "100", - "90", - ) - - def test_parse_condition_plain_string() -> None: """Plain string condition (no prefix) is treated as equality.""" operator, value = parse_condition("pass") @@ -101,26 +73,3 @@ def test_parse_condition_with_operator() -> None: operator, value = parse_condition(">=80%") assert operator == ConditionOperator.GREATER_THAN_OR_EQUAL assert value == "80%" - - -@pytest.mark.deprecated -@pytest.mark.skip(reason="deprecated: ~= string matching removed") -def test_flow_definition_spec_bdd51f94() -> None: - """ - Given: a when condition verdict: "~=pass" and evidence verdict: "passing_grade" - When: the condition is evaluated - Then: the condition is satisfied because pass is a - case-insensitive substring of passing_grade - """ - raise NotImplementedError - - -@pytest.mark.deprecated -@pytest.mark.skip(reason="deprecated: ~= string matching removed") -def test_flow_definition_spec_7711a3c7() -> None: - """ - Given: a when condition verdict: "~=pass" and evidence verdict: "fail" - When: the condition is evaluated - Then: the condition is not satisfied - """ - raise NotImplementedError diff --git a/tests/features/remove_fuzzy_match_operator/__init__.py b/tests/features/remove_fuzzy_match_operator/__init__.py new file mode 100644 index 0000000..3b4b2e5 --- /dev/null +++ b/tests/features/remove_fuzzy_match_operator/__init__.py @@ -0,0 +1 @@ +"""Tests for remove fuzzy match operator feature.""" diff --git a/tests/features/remove_fuzzy_match_operator/condition_removal_test.py b/tests/features/remove_fuzzy_match_operator/condition_removal_test.py new file mode 100644 index 0000000..8bab9fa --- /dev/null +++ b/tests/features/remove_fuzzy_match_operator/condition_removal_test.py @@ -0,0 +1,50 @@ +"""Tests for remove-fuzzy-match-operator feature.""" + +import pytest + +from flowr.domain.condition import ConditionOperator +from flowr.domain.loader import FlowParseError, load_flow + + +def test_remove_fuzzy_match_operator_7aef4c1b() -> None: + """ + Given: a flow file with `when: { score: "~=100" }` + When: the flow is loaded + Then: a FlowParseError is raised indicating ~= is not a valid operator + """ + yaml_str = """\ +flow: test +version: "1.0" +exits: + - done +states: + - id: idle + next: + proceed: + to: done + when: + score: "~=100" +""" + with pytest.raises(FlowParseError, match="~="): + load_flow(yaml_str) + + +def test_remove_fuzzy_match_operator_3170064f() -> None: + """ + Given: the ConditionOperator enum + When: its values are listed + Then: it contains exactly EQUALS, NOT_EQUALS, GREATER_THAN_OR_EQUAL, + LESS_THAN_OR_EQUAL, GREATER_THAN, LESS_THAN + And does not contain APPROXIMATELY_EQUAL + """ + expected = { + "EQUALS", + "NOT_EQUALS", + "GREATER_THAN_OR_EQUAL", + "LESS_THAN_OR_EQUAL", + "GREATER_THAN", + "LESS_THAN", + } + actual = {op.name for op in ConditionOperator} + assert actual == expected + assert not hasattr(ConditionOperator, "APPROXIMATELY_EQUAL") diff --git a/tests/unit/condition_test.py b/tests/unit/condition_test.py index 172616a..ee09fde 100644 --- a/tests/unit/condition_test.py +++ b/tests/unit/condition_test.py @@ -61,16 +61,6 @@ def test_parse_condition_all_operators() -> None: assert parse_condition("<=80")[0] == ConditionOperator.LESS_THAN_OR_EQUAL assert parse_condition(">80")[0] == ConditionOperator.GREATER_THAN assert parse_condition("<3")[0] == ConditionOperator.LESS_THAN - assert parse_condition("~=100")[0] == ConditionOperator.APPROXIMATELY_EQUAL - - -def test_approximate_equal_exact_match() -> None: - """Approximate match passes when values are exactly equal.""" - assert evaluate_condition( - ConditionOperator.APPROXIMATELY_EQUAL, - "100", - "100", - ) def test_less_than_or_equal() -> None: diff --git a/tests/unit/loader_test.py b/tests/unit/loader_test.py index 30f7984..9680870 100644 --- a/tests/unit/loader_test.py +++ b/tests/unit/loader_test.py @@ -223,6 +223,49 @@ def test_resolve_subflows_missing_file(tmp_path: Path) -> None: assert flows[0].flow == "parent" +def test_load_flow_rejects_fuzzy_match_in_named_condition_ref() -> None: + """load_flow raises FlowParseError when a named condition uses ~= operator.""" + yaml_str = """\ +flow: test +version: "1.0" +exits: + - done +states: + - id: idle + conditions: + fuzzy_check: + score: "~=100" + next: + proceed: + to: done + when: + - fuzzy_check +""" + with pytest.raises(FlowParseError, match="~="): + load_flow(yaml_str) + + +def test_load_flow_rejects_fuzzy_match_in_list_form_inline_when() -> None: + """load_flow raises FlowParseError when list-form inline dict + when clause uses the ~= operator. + """ + yaml_str = """\ +flow: test +version: "1.0" +exits: + - done +states: + - id: idle + next: + proceed: + to: done + when: + - score: "~=100" +""" + with pytest.raises(FlowParseError, match="~="): + load_flow(yaml_str) + + def test_flow_parser_protocol() -> None: """FlowParser is a Protocol for YAML parsing backends.""" diff --git a/uv.lock b/uv.lock index 8df9cfd..58dd9bb 100644 --- a/uv.lock +++ b/uv.lock @@ -334,7 +334,7 @@ wheels = [ [[package]] name = "flowr" -version = "0.5.0" +version = "1.0.0" source = { virtual = "." } dependencies = [ { name = "pyyaml" },