diff --git a/AGENTS.md b/AGENTS.md index d1aa2a0..70991a1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,26 +55,26 @@ This is the heart of the product. Everything under `forge/` is **instructional t operational references for project layout or rendering/testing, but those references must not restate `.plain` authoring rules. Integration rules load only for integration work. Claude skips rereading rules already supplied natively; other agents read only rules not already in context. -- **Example hygiene is load-bearing.** The rules teach `.plain` syntax, and a `.plain` example written the wrong way *leaks* — agents imitate it. Bare-continuation examples may appear **only** inside blocks explicitly labeled BAD / WRONG / `Before:` / `Too complex:`. Every "good"/"after"/"acceptable"/canonical/`## Format` example must make each line a valid `- ` list item. (`forge/rules/bullet-continuation.md` is the canonical rule.) +- **Example hygiene is load-bearing.** The rules teach `.plain` syntax, and a `.plain` example written the wrong way *leaks* — agents imitate it. Bare-continuation examples may appear **only** inside blocks explicitly labeled BAD / WRONG / `Before:` / `Too complex:`. Every "good"/"after"/"acceptable"/canonical/`## Format` example must make each line a valid `- ` list item, except in `***functional specs***`, where each functionality is a `# ` heading, a prose statement line, and `- ` clarifications indented by two spaces. (`forge/rules/bullet-continuation.md` and `forge/rules/func-specs.md` are the canonical rules.) - **Skills are narrow and chained.** Each `forge/skills//SKILL.md` has YAML frontmatter (`name` = dir name, `description` = a `>-` block ending in a "Use when…" trigger) and a prose body. Most spec-touching skills invoke `load-plain-reference` first so it can select the relevant rules. Large or operational detail belongs in a skill's `references/` directory and loads only when needed. Only the three test-script generators (`implement-{unit-testing,conformance-testing,prepare-environment}-script`) carry an `assets/` dir of reference `.sh`/`.ps1` scripts. ### The `.plain` model the content describes -A `.plain` file is a module: YAML frontmatter (`description`, `import:`, `requires:`, `exported_concepts:`, `required_concepts:`) plus up to five triple-asterisk sections. **Where a fact lives is strict and enforced** — the renderer reads each concept only from its owning section, so a misplaced fact is silently ignored: +A `.plain` file is a module: YAML frontmatter (`description`, `import:`, `requires:`, `exported_concepts:`, `required_concepts:`) plus up to four triple-asterisk sections. Each functional spec is a `# ` heading followed by its body (`forge/rules/semantic-ids.md`). The test scenarios of a functionality live in a second file kind, the **test plain file** at `tests//.plain` — frontmatter `module:` + `func_id:`, sections `***conformance tests***` and `***acceptance tests***` (`forge/rules/test-plain-files.md`); the renderer generates exactly those scenarios and never invents a test. **Where a fact lives is strict and enforced** — the renderer reads each concept only from its owning section, so a misplaced fact is silently ignored: | Section | Owns | |---|---| | `***definitions***` | concepts as `:CamelCaseToken:`, define-before-use, globally unique, no cycles | | `***implementation reqs***` | HOW it's built (tech/architecture) **and everything about `:UnitTests:`** | -| `***functional specs***` | WHAT it does — observable, language-agnostic; each spec ≤200 changed LOC; rendered incrementally top-to-bottom | +| `***functional specs***` | WHAT it does — observable, language-agnostic; one `# func-id` heading per functionality; each spec ≤200 changed LOC; rendered incrementally top-to-bottom | | `***test reqs***` | **everything about `:ConformanceTests:`** (framework, run command, mocking/network policy) | -| `***acceptance tests***` | nested under a functional spec (not top-level); full end-to-end workflows | +| test plain file (`***conformance tests***`, `***acceptance tests***`) | the scenarios of one functionality; conformance scenarios describe the expected use, acceptance scenarios the use that is not expected (invalid parameters, wrong or missing authentication, missing data); at least one conformance scenario when conformance tests are enabled | Other constraints the rules enforce (see `forge/rules/`): functional specs are mandatorily routed through `add-functional-spec(s)` (never hand-authored), checked for the 200-LOC limit (`analyze-if-func-spec-too-complex` → `break-down-func-spec`) and for conflicts (`analyze-func-specs` → `resolve-spec-conflict`); external artifacts (JSON Schema, OpenAPI, payloads) are **linked** as single local text files under `resources/`, never transcribed and never folders/URLs/binaries; generated output under `plain_modules//code/` (implementation + unit tests) and `plain_modules//tests/` (conformance tests) is read-only — fixes go back into the spec and re-render. ### The skill lifecycle (orchestration) -`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (config and script checks, `plain-parser check` on every top module, plus rule checks the parser can't do) then hand off the render target. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. +`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, a test plain file with conformance scenarios per functionality via `add-test-scenarios`, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (config and script checks, `plain-parser check` on every top module, plus rule checks the parser can't do) then hand off the render target. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold; `migrate-plain-syntax` converts a project from the old syntax (nested acceptance tests, `- ` functional specs), which no other skill edits. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. ## Memory / persistent context diff --git a/CLAUDE.md b/CLAUDE.md index d1aa2a0..70991a1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -55,26 +55,26 @@ This is the heart of the product. Everything under `forge/` is **instructional t operational references for project layout or rendering/testing, but those references must not restate `.plain` authoring rules. Integration rules load only for integration work. Claude skips rereading rules already supplied natively; other agents read only rules not already in context. -- **Example hygiene is load-bearing.** The rules teach `.plain` syntax, and a `.plain` example written the wrong way *leaks* — agents imitate it. Bare-continuation examples may appear **only** inside blocks explicitly labeled BAD / WRONG / `Before:` / `Too complex:`. Every "good"/"after"/"acceptable"/canonical/`## Format` example must make each line a valid `- ` list item. (`forge/rules/bullet-continuation.md` is the canonical rule.) +- **Example hygiene is load-bearing.** The rules teach `.plain` syntax, and a `.plain` example written the wrong way *leaks* — agents imitate it. Bare-continuation examples may appear **only** inside blocks explicitly labeled BAD / WRONG / `Before:` / `Too complex:`. Every "good"/"after"/"acceptable"/canonical/`## Format` example must make each line a valid `- ` list item, except in `***functional specs***`, where each functionality is a `# ` heading, a prose statement line, and `- ` clarifications indented by two spaces. (`forge/rules/bullet-continuation.md` and `forge/rules/func-specs.md` are the canonical rules.) - **Skills are narrow and chained.** Each `forge/skills//SKILL.md` has YAML frontmatter (`name` = dir name, `description` = a `>-` block ending in a "Use when…" trigger) and a prose body. Most spec-touching skills invoke `load-plain-reference` first so it can select the relevant rules. Large or operational detail belongs in a skill's `references/` directory and loads only when needed. Only the three test-script generators (`implement-{unit-testing,conformance-testing,prepare-environment}-script`) carry an `assets/` dir of reference `.sh`/`.ps1` scripts. ### The `.plain` model the content describes -A `.plain` file is a module: YAML frontmatter (`description`, `import:`, `requires:`, `exported_concepts:`, `required_concepts:`) plus up to five triple-asterisk sections. **Where a fact lives is strict and enforced** — the renderer reads each concept only from its owning section, so a misplaced fact is silently ignored: +A `.plain` file is a module: YAML frontmatter (`description`, `import:`, `requires:`, `exported_concepts:`, `required_concepts:`) plus up to four triple-asterisk sections. Each functional spec is a `# ` heading followed by its body (`forge/rules/semantic-ids.md`). The test scenarios of a functionality live in a second file kind, the **test plain file** at `tests//.plain` — frontmatter `module:` + `func_id:`, sections `***conformance tests***` and `***acceptance tests***` (`forge/rules/test-plain-files.md`); the renderer generates exactly those scenarios and never invents a test. **Where a fact lives is strict and enforced** — the renderer reads each concept only from its owning section, so a misplaced fact is silently ignored: | Section | Owns | |---|---| | `***definitions***` | concepts as `:CamelCaseToken:`, define-before-use, globally unique, no cycles | | `***implementation reqs***` | HOW it's built (tech/architecture) **and everything about `:UnitTests:`** | -| `***functional specs***` | WHAT it does — observable, language-agnostic; each spec ≤200 changed LOC; rendered incrementally top-to-bottom | +| `***functional specs***` | WHAT it does — observable, language-agnostic; one `# func-id` heading per functionality; each spec ≤200 changed LOC; rendered incrementally top-to-bottom | | `***test reqs***` | **everything about `:ConformanceTests:`** (framework, run command, mocking/network policy) | -| `***acceptance tests***` | nested under a functional spec (not top-level); full end-to-end workflows | +| test plain file (`***conformance tests***`, `***acceptance tests***`) | the scenarios of one functionality; conformance scenarios describe the expected use, acceptance scenarios the use that is not expected (invalid parameters, wrong or missing authentication, missing data); at least one conformance scenario when conformance tests are enabled | Other constraints the rules enforce (see `forge/rules/`): functional specs are mandatorily routed through `add-functional-spec(s)` (never hand-authored), checked for the 200-LOC limit (`analyze-if-func-spec-too-complex` → `break-down-func-spec`) and for conflicts (`analyze-func-specs` → `resolve-spec-conflict`); external artifacts (JSON Schema, OpenAPI, payloads) are **linked** as single local text files under `resources/`, never transcribed and never folders/URLs/binaries; generated output under `plain_modules//code/` (implementation + unit tests) and `plain_modules//tests/` (conformance tests) is read-only — fixes go back into the spec and re-render. ### The skill lifecycle (orchestration) -`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (config and script checks, `plain-parser check` on every top module, plus rule checks the parser can't do) then hand off the render target. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. +`forge-plain` is the top-level orchestrator: a short Phase 0 intent interview followed by four gated phases — (1) definitions + functional specs, (2) implementation reqs / tech stack, (3) testing (unit→impl reqs, conformance→test reqs, a test plain file with conformance scenarios per functionality via `add-test-scenarios`, generate `test_scripts/`, build `config.yaml`, probe host via `check-plain-env`), (4) validate via `plain-healthcheck` (config and script checks, `plain-parser check` on every top module, plus rule checks the parser can't do) then hand off the render target. Phases 1–3 are **one-question-at-a-time, write-to-disk-immediately**. `add-feature` is the same authoring loop scoped to one feature on an existing project; `init-plain-project` is a no-interview scaffold; `migrate-plain-syntax` converts a project from the old syntax (nested acceptance tests, `- ` functional specs), which no other skill edits. The installer ships `forge/rules/*.md` beside the skills; native rule consumers load them directly, while other agents reach them through `load-plain-reference`. ## Memory / persistent context diff --git a/README.md b/README.md index 2fabb41..be3b356 100644 --- a/README.md +++ b/README.md @@ -20,12 +20,13 @@ Pick whichever entry point matches how much upfront design you want: ### 2. Grow an existing project - **`add-feature`** — takes a feature request in plain English and runs the same one-question-at-a-time loop that `forge-plain` uses, scoped to a single feature against an existing `.plain` file. -- **Per-section authoring skills** — `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-concept`, `add-acceptance-test`, `add-resource`, `add-template`. Each enforces the relevant ***plain syntax rules (concept uniqueness, complexity limits, bullet syntax, linked-resource constraints, …) so hand-authoring doesn't drift from the language. +- **Per-section authoring skills** — `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-concept`, `add-test-scenarios`, `add-resource`, `add-template`. Each enforces the relevant ***plain syntax rules (concept uniqueness, complexity limits, bullet syntax, linked-resource constraints, …) so hand-authoring doesn't drift from the language. `add-test-scenarios` writes the test plain file of a functionality — `tests//.plain` with the conformance and acceptance scenarios the renderer implements, and nothing else. - **Module-management skills** — `create-import-module`, `create-requires-module`, `refactor-module`, `consolidate-concepts` for restructuring as the project grows. ### 3. Validate and maintain - **`plain-healthcheck`** — the verification gate. Inventories every `.plain` module, validates every `config.yaml`, parses every top module with [`plain-parser`](https://github.com/plainlang/plain-parser), and checks every module against the ***plain rules. Returns `PASS` / `FAIL` with a numbered punch-list when something is broken. +- **`migrate-plain-syntax`** — converts a project from the old syntax (nested acceptance tests, `- ` functional specs) to `# func-id` headings and test plain files under `tests/`. Run it when a skill or `plain-parser` reports the old syntax. - **`init-config-file`** — assembles the canonical `config.yaml` per part of a project from the decisions made during interviewing. - **`check-plain-env`** — probes the host machine for everything the project needs (language toolchains, external services, system binaries, drivers) and emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands. - **`analyze-func-specs`** / **`analyze-if-func-spec-too-complex`** / **`break-down-func-spec`** / **`resolve-spec-conflict`** — the spec-quality toolchain that runs behind `add-feature` and `forge-plain` but can also be invoked directly when reviewing or refactoring. @@ -276,7 +277,7 @@ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them int | `add-implementation-requirement` | Add a non-functional requirement to `***implementation reqs***` | | `add-test-requirement` | Add a testing requirement to `***test reqs***` | | `add-concept` | Define a new concept in `***definitions***` | -| `add-acceptance-test` | Add verification criteria under a functional spec | +| `add-test-scenarios` | Write or update the test plain file of a functionality (`tests//.plain`): conformance scenarios for its expected use and acceptance scenarios for its unexpected use, in plain English | | `add-resource` | Link an external file (schema, API spec) to a spec | | `add-template` | Create or include a reusable Liquid template | @@ -294,8 +295,9 @@ On `install`, the CLI reads `forge/skills` and `forge/rules` and writes them int | Skill | Description | |-------|-------------| | `init-config-file` | Build / finalize the project's `config.yaml` file(s) from the decisions made in Phase 3. Knows the full set of valid keys, refuses to write secrets or per-invocation options, and produces one config per part of the project. Run at the end of `forge-plain` (just before `plain-healthcheck`) and any time the testing surface changes. | -| `plain-healthcheck` | Verification gate: validates every `config.yaml`, confirms each `*-script` field points at a real file in `test_scripts/`, runs `plain-parser check` on every top module, and checks every module against the rules. Run whenever anything in the project is finalized — at the end of `forge-plain`, at the end of `add-feature`, after `debug-specs`, and after any single-skill edit that touches the renderable surface. | +| `plain-healthcheck` | Verification gate: validates every `config.yaml`, confirms each `*-script` field points at a real file in `test_scripts/`, runs `plain-parser check` on every top module, and checks every module and test plain file against the rules. Run whenever anything in the project is finalized — at the end of `forge-plain`, at the end of `add-feature`, after `debug-specs`, and after any single-skill edit that touches the renderable surface. | | `check-plain-env` | Read the project's `.plain` files, `test_scripts/`, `config.yaml`(s), and `resources/`, then probe the host for every requirement **the package manager can't install**: language toolchains (`python` + `pip`, `node` + `npm`, JDK + `mvn`, Go, Rust, .NET, etc.), external services (Postgres, Redis, Docker, ...), system binaries that language packages wrap (`ffmpeg`, `tesseract`, `pdftoppm`, browser binaries, ...), hardware / drivers / accelerators (NVIDIA driver → CUDA toolkit → cuDNN → framework-sees-GPU chain), credential env vars, and `plain-parser`. Does **not** probe individual language packages — `pip install -r requirements.txt` (and equivalents) handle those when the test scripts run. Emits a `PASS` / `WARN` / `FAIL` report with OS-specific install commands for any gaps. Read-only — never installs anything. Run on first-time setup, before rendering on a new machine, after adding a new tech to a project, or any time `command not found` shows up in test output. | +| `migrate-plain-syntax` | Convert a project from the old syntax to `# func-id` headings and test plain files under `tests/` | | `analyze-if-func-spec-too-complex` | Check if a spec exceeds the 200-line complexity limit | | `analyze-func-specs` | Check a batch of specs (2+) against each other in one call and return every conflicting pair | | `analyze-2-func-specs` | Legacy: check exactly two specs for conflicts (prefer `analyze-func-specs`) | diff --git a/forge/rules/bullet-continuation.md b/forge/rules/bullet-continuation.md index f55ce6b..f173486 100644 --- a/forge/rules/bullet-continuation.md +++ b/forge/rules/bullet-continuation.md @@ -4,7 +4,7 @@ description: Bullet and continuation syntax for every section in .plain files # Rules for bullets and continuations in `.plain` files -These rules apply to every section and to concept explanations. +These rules apply to every `- ` bullet in every section, to concept explanations, and to the bullets under the statement line of a functionality or inside a scenario of a test plain file. ## No line-length limit @@ -25,17 +25,22 @@ clarification of the statement above it. There is no third, typographic reason t indistinguishable from a deliberate sub-point - The test: if content deserves its own line, it deserves its own `- ` bullet; if it doesn't, keep it on the parent's line -- What the renderer does enforce is the top level: every statement in a section must be a `- ` - list item — a flush-left prose line is a syntax error. A bare continuation, by contrast, is - accepted and folded into its parent item verbatim +- What the renderer does enforce is the top level of `***definitions***`, `***implementation reqs***`, + and `***test reqs***`: every statement there must be a `- ` list item — a flush-left prose line is + a syntax error. A bare continuation, by contrast, is accepted and folded into its parent item verbatim +- `***functional specs***` has a different top level: each functionality is a `# ` heading, + its first body line is prose, and the clarifications under it are `- ` bullets indented by two + spaces (see `func-specs.md`). A scenario in a test plain file has the same shape (see + `test-plain-files.md`) BAD — line broken for width (bare continuation): ```plain ***functional specs*** -- :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(), - which returns a list of :EventEnvelope: dicts conforming to the gateway contract. +# handle-stripe-webhook +:GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(), +which returns a list of :EventEnvelope: dicts conforming to the gateway contract. ``` GOOD — one valid list item: @@ -43,7 +48,8 @@ GOOD — one valid list item: ```plain ***functional specs*** -- :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(), which returns a list of :EventEnvelope: dicts conforming to the gateway contract. +# handle-stripe-webhook +:GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(), which returns a list of :EventEnvelope: dicts conforming to the gateway contract. ``` BEST — separate clarifications expressed as nested bullets: @@ -51,7 +57,8 @@ BEST — separate clarifications expressed as nested bullets: ```plain ***functional specs*** -- :GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(). +# handle-stripe-webhook +:GatewayWebhook: hands off :StripeRequest: to :StripeIntegration:.handle(). - The method returns a list of :EventEnvelope: dicts. - The dicts conform to the gateway's :EventEnvelope: contract. ``` @@ -59,7 +66,7 @@ BEST — separate clarifications expressed as nested bullets: ## Presenting `.plain` examples - Show every example under its owning section header -- Separate top-level list items with one blank line +- Separate top-level list items, functionalities, and scenarios with one blank line - Keep nested clarifications directly under their parent without a blank line - BAD, WRONG, `Before:`, and `Too complex:` examples may intentionally demonstrate invalid syntax diff --git a/forge/rules/concise-specs-examples.md b/forge/rules/concise-specs-examples.md index d6e833e..9309627 100644 --- a/forge/rules/concise-specs-examples.md +++ b/forge/rules/concise-specs-examples.md @@ -19,11 +19,14 @@ BAD: ```plain ***functional specs*** -- In order to provide users with a robust and seamless experience, :TaskService: exposes a comprehensive endpoint for the creation of new :Task: items. +# create-task +In order to provide users with a robust and seamless experience, :TaskService: exposes a comprehensive endpoint for the creation of new :Task: items. -- The endpoint accepts a request and, provided that the incoming payload is valid and well-formed, proceeds to persist the :Task: appropriately. +# persist-task +The endpoint accepts a request and, provided that the incoming payload is valid and well-formed, proceeds to persist the :Task: appropriately. -- If the payload is not valid, an appropriate error is returned to the caller so that the user is informed of the problem. +# reject-invalid-payload +If the payload is not valid, an appropriate error is returned to the caller so that the user is informed of the problem. ``` GOOD: @@ -31,7 +34,8 @@ GOOD: ```plain ***functional specs*** -- A :User: can create a :Task: via `POST /tasks`. +# create-task +A :User: can create a :Task: via `POST /tasks`. - The request body is a :Task: without `id`. - The response is `201` with the created :Task:. - A request without a name returns `400`. @@ -46,7 +50,8 @@ BAD: ```plain ***functional specs*** -- Because support staff frequently need to find customers quickly, and because scanning the full list is slow and error-prone, :CustomerDirectory: offers a search capability that greatly improves their daily workflow. +# search-customers +Because support staff frequently need to find customers quickly, and because scanning the full list is slow and error-prone, :CustomerDirectory: offers a search capability that greatly improves their daily workflow. ``` GOOD: @@ -54,7 +59,8 @@ GOOD: ```plain ***functional specs*** -- :CustomerDirectory: returns :Customer: items whose name or email contains a search term, case-insensitively. +# search-customers +:CustomerDirectory: returns :Customer: items whose name or email contains a search term, case-insensitively. ``` Why the feature exists changes nothing about what is built. What "search" matches does. @@ -86,7 +92,8 @@ BAD: ```plain ***functional specs*** -- :OrderApi: returns a list of :Order: items over HTTP. +# list-orders +:OrderApi: returns a list of :Order: items over HTTP. - The list contains :Order: items. - If there are no :Order: items, the list is empty. - Each :Order: has an `id`, which is unique. @@ -98,7 +105,8 @@ GOOD: ```plain ***functional specs*** -- :OrderApi: returns the :Order: items of the authenticated :User: via `GET /orders`, newest first. +# list-orders +:OrderApi: returns the :Order: items of the authenticated :User: via `GET /orders`, newest first. - The response is `200` with a JSON array, empty when the :User: has no :Order: items. ``` @@ -113,11 +121,14 @@ BAD: ```plain ***functional specs*** -- :OrderService: validates all incoming data before processing it. +# validate-input +:OrderService: validates all incoming data before processing it. -- :OrderService: handles errors and returns meaningful error messages. +# handle-errors +:OrderService: handles errors and returns meaningful error messages. -- :OrderService: saves the :Order: to storage so that it is not lost. +# save-order +:OrderService: saves the :Order: to storage so that it is not lost. ``` GOOD: @@ -125,7 +136,8 @@ GOOD: ```plain ***functional specs*** -- :OrderService: rejects an :Order: whose `total` does not equal the sum of its :OrderLine: amounts, with `422`. +# reject-wrong-total +:OrderService: rejects an :Order: whose `total` does not equal the sum of its :OrderLine: amounts, with `422`. ``` Validating input, reporting errors, and persisting on a save operation belong in the `***implementation reqs***` and a baseline is assumed. If there is a specific use case for a particular operation, it should be documented there, not in the `***functional specs***`. The one fact worth stating is *which* rule is enforced and what the caller sees when it fails. @@ -218,7 +230,8 @@ BAD: ```plain ***functional specs*** -- :ReportGenerator: is responsible for facilitating the utilization of a plurality of :DataSource: instances in order to initiate the generation of a :Report: prior to the performance of the validation of the resulting output. +# build-report +:ReportGenerator: is responsible for facilitating the utilization of a plurality of :DataSource: instances in order to initiate the generation of a :Report: prior to the performance of the validation of the resulting output. ``` GOOD: @@ -226,7 +239,8 @@ GOOD: ```plain ***functional specs*** -- :ReportGenerator: builds a :Report: from one or more :DataSource: items, then validates it. +# build-report +:ReportGenerator: builds a :Report: from one or more :DataSource: items, then validates it. ``` Same sequence, plain words. See the substitution table in `concise-specs.md`. @@ -238,7 +252,8 @@ BAD: ```plain ***functional specs*** -- A :User: can archive a :Project:. +# archive-project +A :User: can archive a :Project:. - Archiving is an action the user performs on a project. - The archive action is available to users who have access to the project. - Once archived, the project is in the archived state. @@ -249,7 +264,8 @@ GOOD: ```plain ***functional specs*** -- A :User: can archive a :Project: they own. +# archive-project +A :User: can archive a :Project: they own. - An archived :Project: is excluded from :ProjectList:. - Archiving a :Project: that is already archived returns `409`. ``` @@ -263,7 +279,8 @@ BAD: ```plain ***functional specs*** -- :SessionStore: expires a :Session: 30 minutes after its last use. +# expire-session +:SessionStore: expires a :Session: 30 minutes after its last use. - Sessions do not live forever; they time out. - The timeout is 30 minutes. ``` @@ -273,7 +290,8 @@ GOOD: ```plain ***functional specs*** -- :SessionStore: expires a :Session: 30 minutes after its last use. +# expire-session +:SessionStore: expires a :Session: 30 minutes after its last use. - A request carrying an expired :Session: returns `401`. ``` @@ -284,11 +302,14 @@ BAD: ```plain ***functional specs*** -- A :User: can upload a :Document:. +# upload-document +A :User: can upload a :Document:. -- The system stores the submitted file in :DocumentStore:. +# store-document +The system stores the submitted file in :DocumentStore:. -- The uploaded asset is retrievable by its identifier. +# retrieve-document +The uploaded asset is retrievable by its identifier. ``` Three names — `:Document:`, "file", "asset" — read as three things. A reader cannot tell whether "asset" is a fourth concept, and the renderer cannot either. @@ -298,7 +319,8 @@ GOOD: ```plain ***functional specs*** -- A :User: can upload a :Document:. +# upload-document +A :User: can upload a :Document:. - :DocumentStore: stores the :Document: and assigns it a :DocumentId:. - A :User: can retrieve a :Document: by its :DocumentId:. ``` @@ -310,13 +332,17 @@ BAD: ```plain ***functional specs*** -- This spec is intentionally kept small so that it stays within the complexity limit. +# export-csv-size-note +This spec is intentionally kept small so that it stays within the complexity limit. -- As described above, :ExportService: writes a :CsvFile:. +# export-csv-reference +As described above, :ExportService: writes a :CsvFile:. -- No changes are made to the existing :ImportService: or to the user interface. +# export-csv-no-changes +No changes are made to the existing :ImportService: or to the user interface. -- Future specs will extend this behavior with additional formats. +# export-csv-future +Future specs will extend this behavior with additional formats. ``` GOOD: @@ -324,7 +350,8 @@ GOOD: ```plain ***functional specs*** -- :ExportService: writes the :Order: items of a :BillingPeriod: to a :CsvFile:. +# export-csv +:ExportService: writes the :Order: items of a :BillingPeriod: to a :CsvFile:. - The header row is `id,customer,total,currency`. - An empty :BillingPeriod: produces a :CsvFile: with only the header row. ``` @@ -338,13 +365,14 @@ BAD: ```plain ***functional specs*** -- :PricingEngine: applies discounts. - The discount matrix is as follows: - | Tier | Volume | Discount | - |---|---|---| - | Bronze | 0-99 | 0% | - | Silver | 100-499 | 5% | - | Gold | 500+ | 12% | +# apply-discounts +:PricingEngine: applies discounts. +The discount matrix is as follows: +| Tier | Volume | Discount | +|---|---|---| +| Bronze | 0-99 | 0% | +| Silver | 100-499 | 5% | +| Gold | 500+ | 12% | ``` GOOD: @@ -352,7 +380,8 @@ GOOD: ```plain ***functional specs*** -- :PricingEngine: applies the :DiscountTier: matrix in [resources/discount-tiers.json](resources/discount-tiers.json) to an :Order: total. +# apply-discounts +:PricingEngine: applies the :DiscountTier: matrix in [resources/discount-tiers.json](resources/discount-tiers.json) to an :Order: total. - An :Order: below the lowest tier threshold gets no discount. ``` @@ -373,7 +402,8 @@ BAD — everything in one place: ***functional specs*** -- :ImageResizer:, implemented as a Python class using Pillow with a thread pool of 4 workers, resizes an uploaded :Image: to a thumbnail, where a thumbnail is defined as a JPEG at most 256 pixels on its longest edge. +# resize-image +:ImageResizer:, implemented as a Python class using Pillow with a thread pool of 4 workers, resizes an uploaded :Image: to a thumbnail, where a thumbnail is defined as a JPEG at most 256 pixels on its longest edge. ``` GOOD — each fact in the section that owns it: @@ -395,7 +425,8 @@ GOOD — each fact in the section that owns it: ***functional specs*** -- :ImageResizer: produces a :Thumbnail: from an :Image:. +# resize-image +:ImageResizer: produces a :Thumbnail: from an :Image:. - An :Image: already within the :Thumbnail: bounds is returned unchanged. - An :Image: that is not a supported format raises :UnsupportedImageError:. ``` @@ -406,26 +437,26 @@ Use `resolve-section-ownership` to perform the split. It is not the same as spli The most common single case of this is a functional spec that states a mechanism rather than a result — a retry schedule, a cache, a token refresh. `impl-reqs.md` owns HOW; a functional spec keeps what a caller observes. `concise-specs-integration-examples.md` works through several. -## Narrated acceptance tests +## Narrated scenarios BAD: ```plain -***acceptance tests*** +***conformance tests*** -- The tester should first open the application and log in with a valid account, then navigate to the projects area where they will be able to see the list of projects, and then they should create a new project by filling in the form, after which they should verify that everything worked as expected and the project shows up correctly. +# create-project +The tester should first open the application and log in with a valid account, then navigate to the projects area where they will be able to see the list of projects, and then they should create a new project by filling in the form, after which they should verify that everything worked as expected and the project shows up correctly. ``` GOOD: ```plain -***acceptance tests*** +***conformance tests*** -- A :User: creates a :Project: and finds it in :ProjectList:. - - Sign in as a :User: with no :Project: items. - - Create a :Project: named `Apollo`. +# create-project +A signed-in :User: with no :Project: items creates a :Project: named `Apollo` and finds it in :ProjectList:. - :ProjectList: contains exactly one :Project:, named `Apollo`. ``` -One narrated paragraph becomes an ordered set of checkable steps, and "everything worked as +One narrated paragraph becomes one checkable sentence, and "everything worked as expected" becomes an assertion. diff --git a/forge/rules/concise-specs-integration-examples.md b/forge/rules/concise-specs-integration-examples.md index d21d5ca..ed694ce 100644 --- a/forge/rules/concise-specs-integration-examples.md +++ b/forge/rules/concise-specs-integration-examples.md @@ -31,11 +31,14 @@ BAD: ***functional specs*** -- :ProviderClient: retries the request when :ProviderApi: returns 408, 425, 429, 500, 502, 503, or 504, and also on connection errors, DNS failures, socket timeouts, TLS handshake failures, and connection resets. +# retry-transient-errors +:ProviderClient: retries the request when :ProviderApi: returns 408, 425, 429, 500, 502, 503, or 504, and also on connection errors, DNS failures, socket timeouts, TLS handshake failures, and connection resets. -- :ProviderClient: does not retry on 400, 401, 403, 404, 405, 409, 410, 415, or 422, and raises immediately. +# no-retry-permanent-errors +:ProviderClient: does not retry on 400, 401, 403, 404, 405, 409, 410, 415, or 422, and raises immediately. -- On 429, :ProviderClient: waits and tries again, unless the retry budget is exhausted. +# retry-on-429 +On 429, :ProviderClient: waits and tries again, unless the retry budget is exhausted. ``` GOOD: @@ -91,13 +94,17 @@ BAD: ***functional specs*** -- :ProviderApi: returns `200` with a JSON body when a request succeeds. +# provider-success-response +:ProviderApi: returns `200` with a JSON body when a request succeeds. -- :ProviderApi: returns `404` when the requested :Customer: does not exist. +# provider-not-found +:ProviderApi: returns `404` when the requested :Customer: does not exist. -- :ProviderApi: rate-limits clients and includes the remaining quota in the response headers. +# provider-rate-limit +:ProviderApi: rate-limits clients and includes the remaining quota in the response headers. -- :ProviderApi: requires the `Authorization` header on every request. +# provider-auth-header +:ProviderApi: requires the `Authorization` header on every request. ``` GOOD: @@ -123,7 +130,8 @@ GOOD: ***functional specs*** -- :ProviderClient: fetches a :Customer: from :ProviderApi: by :CustomerId:. +# fetch-customer +:ProviderClient: fetches a :Customer: from :ProviderApi: by :CustomerId:. ``` Every BAD bullet is true, and none of them is deleted — they are moved. The success shape, the `404`, and the `Authorization` requirement are what an OpenAPI file exists to state; the rate-limit model has its own conventional files. Provider behavior with no machine-readable home goes in `resources/-notes.md`. `integrations.md` carries the full artifact table. @@ -147,17 +155,21 @@ BAD — four specs, one operation: ***functional specs*** -- A call to `fetch` fails before any request to :ProviderApi: is sent when `PROVIDER_API_KEY` is unset or empty. +# fetch-requires-api-key +A call to `fetch` fails before any request to :ProviderApi: is sent when `PROVIDER_API_KEY` is unset or empty. - The failure names `PROVIDER_API_KEY`. -- A call to `fetch` reads the field catalogue from :ProviderApi: once, before requesting any :Record:. +# fetch-reads-catalogue +A call to `fetch` reads the field catalogue from :ProviderApi: once, before requesting any :Record:. - The catalogue yields the name of every field the account defines. - A call to `fetch` whose catalogue read fails requests no :Record: items. -- A request for :Record: items from :ProviderApi: names every field in the catalogue and asks for at most 100 records. +# fetch-requests-records +A request for :Record: items from :ProviderApi: names every field in the catalogue and asks for at most 100 records. - Archived :Record: items are not requested. -- A call to `fetch` requests pages until :ProviderApi: returns a response carrying no next-page cursor. +# fetch-pages-records +A call to `fetch` requests pages until :ProviderApi: returns a response carrying no next-page cursor. - Each page after the first is requested with the cursor from the previous response. - The :Record: items of every page are carried forward together. ``` @@ -175,7 +187,8 @@ GOOD — one spec, with the result stated first: ***functional specs*** -- :RecordSource: `fetch()` returns every non-archived :Record: with every field :ProviderApi: defines, paging under [resources/pagination.yaml](resources/pagination.yaml). +# fetch-records +:RecordSource: `fetch()` returns every non-archived :Record: with every field :ProviderApi: defines, paging under [resources/pagination.yaml](resources/pagination.yaml). - :RecordSource: reads the field catalogue once per `fetch()`. - `fetch()` fails before any request when `PROVIDER_API_KEY` is unset or empty, naming it. - A failed catalogue read fails `fetch()` before any :Record: is requested. @@ -190,9 +203,11 @@ BAD: ```plain ***functional specs*** -- :ProviderAuth: first reads the client id and the client secret, then builds a form-encoded body containing `grant_type=client_credentials`, the client id, and the client secret, then posts that body to the token endpoint, then parses the JSON response, then extracts the `access_token` and the `expires_in` fields, then stores the token in memory together with the moment it expires, and then attaches it to subsequent requests as a bearer token. +# obtain-token +:ProviderAuth: first reads the client id and the client secret, then builds a form-encoded body containing `grant_type=client_credentials`, the client id, and the client secret, then posts that body to the token endpoint, then parses the JSON response, then extracts the `access_token` and the `expires_in` fields, then stores the token in memory together with the moment it expires, and then attaches it to subsequent requests as a bearer token. -- Before each request, :ProviderAuth: compares the current time to the stored expiry and, if the token has expired or is about to expire, repeats the whole procedure described above to obtain a new token. +# refresh-token +Before each request, :ProviderAuth: compares the current time to the stored expiry and, if the token has expired or is about to expire, repeats the whole procedure described above to obtain a new token. ``` GOOD: diff --git a/forge/rules/concise-specs.md b/forge/rules/concise-specs.md index 1b5d8ed..be6aa79 100644 --- a/forge/rules/concise-specs.md +++ b/forge/rules/concise-specs.md @@ -4,7 +4,7 @@ description: Conciseness and plain-language rules that keep .plain specs reviewa # Rules for keeping `.plain` specs concise -These rules apply to every section, to concept explanations, and to acceptance tests. `concise-specs-examples.md` holds a worked BAD to GOOD pair for each failure mode named here, and `concise-specs-integration-examples.md` covers the failure modes specific to integration specs. +These rules apply to every section, to concept explanations, and to scenarios in test plain files. `concise-specs-examples.md` holds a worked BAD to GOOD pair for each failure mode named here, and `concise-specs-integration-examples.md` covers the failure modes specific to integration specs. ## Why this matters diff --git a/forge/rules/func-specs.md b/forge/rules/func-specs.md index b9a8a24..ac00889 100644 --- a/forge/rules/func-specs.md +++ b/forge/rules/func-specs.md @@ -1,11 +1,19 @@ --- -description: Rules for writing ***functional specs*** and ***acceptance tests*** in .plain files +description: Rules for writing ***functional specs*** in .plain files --- # Rules for writing `***functional specs***` When writing or editing a `***functional specs***` section in a `.plain` file, always follow these rules: +## Shape +- Each functional spec, called a functionality, is a `# ` heading followed by its body; the func-id follows `semantic-ids.md` +- The first body line is the statement of the behavior, written as prose without a leading `- ` +- Clarifications follow the statement as `- ` bullets indented by two spaces, one fact per bullet; a deeper level adds two more spaces (see `bullet-continuation.md`) +- No body line starts with `# ` (that starts the next functionality) or with `***` +- One blank line separates functionalities +- The test scenarios of a functionality live in its test plain file, never in this section (see `test-plain-files.md`) + ## Voice - Write each functional spec as a **statement of fact** — present tense, indicative mood — describing what the software *is* or *does* once built, not as a command telling a developer what to build - **Name a concrete subject**: an actor (`A :User: can …`), a named component or concept (`:PdfRenderer: lays out …`, `:Order: processing depends on …`), or the affected data in passive voice (`All :Resource: items are returned`) @@ -18,11 +26,14 @@ BAD — imperative commands, or the vague "The system": ```plain ***functional specs*** -- Implement the entry point for :App:. +# entry-point +Implement the entry point for :App:. -- The system shows :TaskList:. +# show-task-list +The system shows :TaskList:. -- Add :Task:. +# add-task +Add :Task:. ``` GOOD — statements of fact with concrete subjects: @@ -30,11 +41,14 @@ GOOD — statements of fact with concrete subjects: ```plain ***functional specs*** -- :App: has an entry point. +# entry-point +:App: has an entry point. -- The :TaskList: is displayed. +# show-task-list +The :TaskList: is displayed. -- A :User: can add a :Task:. Only valid :Task: items are added. +# add-task +A :User: can add a :Task:. Only valid :Task: items are added. ``` ## Conciseness @@ -80,17 +94,17 @@ GOOD — statements of fact with concrete subjects: ## Disambiguation - Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation -- If a single line is not enough to fully disambiguate the behavior, use **nested sub-bullets** to add detail -- Nested lines clarify the parent spec — they do not introduce separate functionality -- Even with nested detail, the spec must still imply ≤ 200 lines of code +- If the statement line is not enough to fully disambiguate the behavior, add **indented `- ` bullets** under it to add detail +- The bullets clarify the statement — they do not introduce separate functionality +- Even with the bullets, the spec must still imply ≤ 200 lines of code ## Testability - Every functional spec must describe behavior that can be verified through an observable interface - State the triggering input or action and the deterministic outcome a test can observe - Include relevant outputs, state changes, errors, or externally visible side effects - Do not rely on generated implementation details as the only way to verify the behavior -- If a black-box conformance test cannot be derived from the spec, make the behavior more explicit -- Testability does not require an acceptance test; acceptance tests are reserved for full workflows +- If a black-box conformance scenario cannot be written from the spec, make the behavior more explicit +- When conformance tests are enabled, every functionality has a test plain file with at least one conformance scenario (see `test-plain-files.md`); write it in the same step as the functional spec ## Deterministic interface - Specs must be detailed enough that a developer can use the built software without reading the generated code @@ -102,28 +116,25 @@ GOOD — statements of fact with concrete subjects: - `requires` modules only pass on functional specs and `exported_concepts` — never implementation reqs, so do not rely on implementation reqs to convey behavior - Behavior that downstream modules need must be expressed in functional specs, not elsewhere -## Acceptance tests -- Nest `***acceptance tests***` under a functional spec when verification criteria are needed -- Each acceptance test must be a **full workflow test** — a specific scenario that exercises the functional spec end-to-end, not a unit-level check of a single field or condition -- Do not restate the obvious behavior from the functional spec — simple, direct verifications are already auto-generated as conformance tests. Acceptance tests must go beyond that: multi-step workflows, interactions between concepts, edge-case scenarios, or end-to-end sequences that prove the feature works in a realistic context -- Each acceptance test must be a direct logical consequence of the parent spec — it illustrates, not extends -- Acceptance tests must describe concrete, verifiable outcomes — not vague qualities -- Acceptance tests must not contradict, narrow, or extend beyond the parent spec +## Test scenarios live in test plain files +- A core spec never holds a `***acceptance tests***` section, nested or top-level; that is the old syntax (see `module-structure.md`) +- Conformance and acceptance scenarios for a functionality go into `tests//.plain`, written with `add-test-scenarios` (see `test-plain-files.md`) +- A functional spec and its test plain file change together: a spec edit without a scenario review is not finished ## Format ```plain ***functional specs*** -- :App: has an entry point. +# entry-point +:App: has an entry point. -- A :User: can add a :Task:. Only valid :Task: items are added. +# add-task +A :User: can add a :Task:. Only valid :Task: items are added. -- A :User: can send a :Message: to a :Conversation:. +# send-message +A :User: can send a :Message: to a :Conversation:. - A :Message: must have non-empty content. - The :Message: is appended to the end of the :Conversation:. - All :Participant: members of the :Conversation: can see the new :Message:. - - ***acceptance tests*** - - Sending a :Message: to a :Conversation: with three participants should make the message visible to all three. ``` diff --git a/forge/rules/impl-reqs.md b/forge/rules/impl-reqs.md index 30ce7c1..e0c422c 100644 --- a/forge/rules/impl-reqs.md +++ b/forge/rules/impl-reqs.md @@ -32,7 +32,7 @@ When writing or editing an `***implementation reqs***` section in a `.plain` fil - Behavior and features → `***functional specs***` - Concept definitions → `***definitions***` - **Conformance-test guidance** → `***test reqs***` -- **Acceptance-test scenarios** → `***acceptance tests***` nested under the relevant functional spec +- **Test scenarios**, conformance and acceptance → the functionality's test plain file (see `test-plain-files.md`) ## Unit tests vs conformance tests (common mistake) - Unit-test guidance (`:UnitTests:` framework, structure, mocking) goes **here**, not in `***test reqs***` diff --git a/forge/rules/linked-resources.md b/forge/rules/linked-resources.md index 624a612..2f4eaad 100644 --- a/forge/rules/linked-resources.md +++ b/forge/rules/linked-resources.md @@ -38,7 +38,8 @@ A linked resource **must not** be any of the following: ***functional specs*** -- A :User: can add a :Task: by POSTing :TaskCreateRequest: to the `POST /tasks` endpoint of :TasksAPI:. +# add-task +A :User: can add a :Task: by POSTing :TaskCreateRequest: to the `POST /tasks` endpoint of :TasksAPI:. - The endpoint responds per :TasksAPI:. ``` @@ -55,9 +56,11 @@ A linked resource **must not** be any of the following: ***functional specs*** -- A :User: can add a :Task: using :TaskModalSpec:. +# add-task +A :User: can add a :Task: using :TaskModalSpec:. -- A :User: can edit a :Task: using :TaskModalSpec:. +# edit-task +A :User: can edit a :Task: using :TaskModalSpec:. ``` ## File location and path resolution diff --git a/forge/rules/module-structure.md b/forge/rules/module-structure.md index 27a30de..c71a7e1 100644 --- a/forge/rules/module-structure.md +++ b/forge/rules/module-structure.md @@ -9,7 +9,8 @@ When creating a new `.plain` file or reviewing the structure of a whole file, al ## File skeleton - A `.plain` file is one **module**: optional YAML frontmatter between `---` markers, followed by sections marked with `***section name***` headers - The frontmatter, when present, must be the first thing in the file -- Every content line inside a section must be a `- ` list item — see `bullet-continuation.md` +- Every content line inside `***definitions***`, `***implementation reqs***`, and `***test reqs***` is a `- ` list item — see `bullet-continuation.md` +- Every functionality in `***functional specs***` is a `# ` heading followed by its body — see `func-specs.md` and `semantic-ids.md` - Every line must be as short and plain as its content allows — see `concise-specs.md` ## Frontmatter fields @@ -23,18 +24,19 @@ When creating a new `.plain` file or reviewing the structure of a whole file, al | `required_concepts` | Declare concepts an import module expects the importing module to define | `required-concepts.md` | ## Sections -- There are exactly four top-level section markers: `***definitions***`, `***implementation reqs***`, `***test reqs***`, and `***functional specs***` +- A core spec has exactly four top-level section markers: `***definitions***`, `***implementation reqs***`, `***test reqs***`, and `***functional specs***` - Each section appears **at most once** per file; all are optional (which ones are allowed depends on the module kind below) -- `***acceptance tests***` is **never a top-level section** — it appears only nested under a single functional spec (see `func-specs.md`) +- `***conformance tests***` and `***acceptance tests***` never appear in a core spec, not even nested under a functional spec; they are the two sections of a test plain file (see `test-plain-files.md`) - Write sections in canonical order: `***definitions***` → `***implementation reqs***` → `***test reqs***` → `***functional specs***` - Definitions come first because every concept must be defined before it is referenced - - Functional specs come last because they are rendered incrementally and their nested acceptance tests close the file + - Functional specs come last because they are rendered incrementally ## Module kinds - A **root module** lives at the repository root and carries behavior: it contains `***functional specs***` - To be renderable, a root module needs at least one functional spec plus the implementation reqs to build it — its own or from an `import`ed module (implementation reqs are not inherited through `requires`). A frontmatter-only file is a valid scaffold, but it cannot be rendered until a functional spec is added - A **requires module** is a root module that is used by another root module via `requires` directive (see `requires-modules.md`) - An **import module** provides shared content that other modules pull in via `import`: it contains only `***definitions***`, `***implementation reqs***`, and/or `***test reqs***` — never `***functional specs***`, never `requires` (see `import-modules.md`) +- A **test plain file** holds the scenarios of one functionality of a root module; its frontmatter `module:` and `func_id:` bind it to that functionality, it lives under `tests/`, and it has no section of a core spec (see `test-plain-files.md`) ## Section ownership (where a fact lives) - The renderer reads each kind of fact **only from its owning section** — a fact placed in the wrong section is silently ignored, not flagged @@ -47,7 +49,7 @@ When creating a new `.plain` file or reviewing the structure of a whole file, al | HOW the software is built — tech stack, architecture, coding standards — and **everything about `:UnitTests:`** | `***implementation reqs***` | `impl-reqs.md` | | **Everything about `:ConformanceTests:`** — framework, run command, mocking and network policy | `***test reqs***` | `test-reqs.md` | | WHAT the software does — observable, language-agnostic behavior | `***functional specs***` | `func-specs.md` | -| End-to-end workflow verification of one functional spec | nested `***acceptance tests***` | `func-specs.md` | +| Test scenarios of one functionality, conformance and acceptance | the test plain file `tests//.plain` | `test-plain-files.md` | ## Comments - Lines beginning with `>` are ignored when rendering @@ -67,9 +69,12 @@ When creating a new `.plain` file or reviewing the structure of a whole file, al ***functional specs*** -- A :Widget: can be created with up to {{ max_tags }} tags. +# create-widget +A :Widget: can be created with up to {{ max_tags }} tags. {% if max_tags > 1 %} -- A :Widget: can have its tags listed. + +# list-widget-tags +A :Widget: can have its tags listed. {% endif %} ``` @@ -99,7 +104,14 @@ import: ***functional specs*** -- A :Task: can be added to the :TaskList:. +# add-task +A :Task: can be added to the :TaskList:. -- The :TaskList: is displayed. +# show-task-list +The :TaskList: is displayed. ``` + +## Old syntax +- A core spec is in the old syntax when it holds an `***acceptance tests***` section (nested under a functional spec), or when its `***functional specs***` are written as `- ` bullets with no `# ` headings +- The old syntax is not supported: the renderer rejects it, and no skill edits a file written in it +- When a skill meets the old syntax, it stops, tells the user which file and which sign it found, and points to the `migrate-plain-syntax` skill, which converts the whole project in one pass diff --git a/forge/rules/requires-modules.md b/forge/rules/requires-modules.md index f1ce700..5612d1d 100644 --- a/forge/rules/requires-modules.md +++ b/forge/rules/requires-modules.md @@ -13,6 +13,7 @@ When creating or editing a `.plain` file that uses `requires`, or one that is re - The `***functional specs***` of the required module **and of all its ancestors** become visible as **previous functional specs** — this visibility **is transitive** down the whole chain - Of a required module's definitions, only the concepts listed in its `exported_concepts` are visible; the module's other definitions stay internal. Each exported concept arrives with its full definition text - Export visibility **is not transitive**: an ancestor's exports are visible only if that ancestor is itself listed in `requires` +- The test plain files of a required module stay with that module; the current module never lists scenarios for a required module's functionality (see `test-plain-files.md`) ## Chain topology diff --git a/forge/rules/semantic-ids.md b/forge/rules/semantic-ids.md new file mode 100644 index 0000000..ed68251 --- /dev/null +++ b/forge/rules/semantic-ids.md @@ -0,0 +1,54 @@ +--- +description: Syntax, uniqueness, and use of semantic func-ids and scenario-ids in .plain files +--- + +# Rules for semantic IDs + +Every functionality in `***functional specs***` and every scenario in a test plain file carries a semantic ID. The ID is the stable name that test plain files, render commands, render state, change detection, logs, and generated tests use for that functionality or scenario. Always follow these rules when writing or reading one. + +## Two kinds of ID + +| ID | Written as | Identifies | Unique within | +|---|---|---|---| +| func-id | `# ` heading in `***functional specs***` of a core spec | one functionality | its module | +| scenario-id | `# ` heading in `***conformance tests***` or `***acceptance tests***` of a test plain file | one scenario | its functionality, across every test plain file bound to that functionality | + +## Syntax +- Lowercase letters, digits, and hyphens only: `add-task`, `export-csv-2`, `ctrl-d-exit` +- The heading is `# ` (hash, one space) followed by the ID and nothing else +- Nothing else is checked: an ID needs no meaning, no prefix, and no number +- Prefer a short slug that names the behavior (`add-task`, not `fs-3` or `the-user-adds-a-task`), so render logs and test names can be read without the spec open + +## Uniqueness +- A func-id is unique in its module; two modules may use the same func-id +- A scenario-id is unique for its functionality after every test plain file bound to that functionality is merged; two functionalities may use the same scenario-id +- Before adding an ID, search the module (or the functionality's test plain files) for the same heading; a duplicate is a syntax error that `plain-parser check` reports + +## Qualified form +- Where the module is not clear from context, write a func-id as `:`, for example `task-manager:add-task` +- `` is the module name: the `.plain` file name without its extension +- Render ranges, render state, change detection, and logs use the qualified form; a test plain file binds with the frontmatter keys `module:` and `func_id:` instead (see `test-plain-files.md`) + +## Stability +- An ID is an identity, not a label: a functionality keeps its func-id for as long as it exists, even when its text changes +- To the renderer, renaming a func-id is deleting one functionality and adding another; rename only when the user asks, then rename `func_id:` in every test plain file bound to it and move the file to the new conventional path +- Never renumber or reorder IDs; the order of the headings in the file is the render order, the ID carries no order + +## How coding agents use IDs +- Name a functionality by its func-id (`:` when the module is not obvious) in questions to the user, in review snippets, in commit messages, and in render commands; never by position ("the third spec") +- Pick the func-id before writing the functional spec text, check that it is unique in the module, then write the heading and the body (see `func-specs.md`) +- A new functionality is finished only when its test plain file exists at `tests//.plain` with at least one conformance scenario (see `test-plain-files.md`) +- A generated conformance or acceptance test carries the scenario-id of the scenario it implements; when a test fails, the scenario-id in its name leads back to the scenario text +- Everything about one functionality is found through its func-id: the `# ` heading in the core spec, the test plain files whose frontmatter names it, and the generated tests labelled with its scenario-ids + +## Format + +```plain +***functional specs*** + +# add-task +A :User: can add a :Task:. Only valid :Task: items are added. + +# delete-task +A :User: can delete a :Task:. +``` diff --git a/forge/rules/test-plain-files.md b/forge/rules/test-plain-files.md new file mode 100644 index 0000000..9dc21c4 --- /dev/null +++ b/forge/rules/test-plain-files.md @@ -0,0 +1,70 @@ +--- +description: Syntax and authoring rules for test plain files, which hold the conformance and acceptance scenarios of one functionality +--- + +# Rules for test plain files + +A test plain file holds the test scenarios of one functionality. Conformance scenarios test the core functionality, its expected use; acceptance scenarios catch the edge cases, the use that is not expected. The renderer generates conformance and acceptance tests from these scenarios and from nothing else: it never invents a scenario. Scenarios are written by people or by plain-forge at authoring time and are reviewed like any other spec line. Always follow these rules when creating or editing one. + +## File skeleton +- A test plain file is a `.plain` file with YAML frontmatter and two sections, in this order: `***conformance tests***`, then `***acceptance tests***` +- Both sections are required; `***acceptance tests***` may be empty +- The frontmatter has exactly two keys, both required: + - `module:` the name of the module that holds the functionality (the core spec's file name without `.plain`) + - `func_id:` the func-id of the functionality (see `semantic-ids.md`) +- The pair `(module, func_id)` must name a functionality that exists; a pair with no match is an error +- A test plain file has no `***definitions***`, `***implementation reqs***`, `***test reqs***`, or `***functional specs***` +- Comment lines starting with `>` are allowed, as in a core spec + +## Location +- By convention a test plain file lives at `tests//.plain`, with `tests/` next to the core spec +- The location and the file name are convention only: the renderer discovers test plain files by reading the frontmatter of every `.plain` file under its test folders +- plain-forge always writes to the conventional path, one file per functionality +- More than one test plain file may bind to the same functionality; their scenarios are merged, and scenario-ids must be unique across the merged set. Hand-written files may use this; plain-forge keeps one file per functionality + +## Scenarios +- A scenario is a `# ` heading (see `semantic-ids.md`) followed by free text that says what is done and what result is expected +- A scenario body line may be prose, a `- ` bullet, or any other text, as long as it does not start with `# ` (that starts a new scenario) or with `***` +- A scenario may reference every concept visible to its module: its own definitions, its `import` modules, and the `exported_concepts` of its `requires` modules. It defines no concepts +- Scenarios are language-agnostic: they describe behavior through the observable interface (CLI, HTTP, files, UI) and never name a function, class, or file of the implementation +- A scenario tests the functionality as a black box; the generated test never imports the implementation's internals +- A scenario is a direct consequence of the functional spec: it does not contradict, narrow, or extend the behavior the spec states +- Each scenario states one concrete, checkable outcome; "works correctly" is not an outcome +- Scenario text follows `concise-specs.md`: no narration, no rationale, no step a reader would assume + +## The shape plain-forge writes +- plain-forge writes every scenario in plain English, with the same shape as a functionality: one statement line, then `- ` bullets indented by two spaces when the line is not enough; hand-written scenarios may use any shape +- The statement line is one sentence that names the action and the expected result (`Adding a :Task: named "Go" to an empty :TaskList: is rejected.`) +- The bullets list, in order, the state before the action, the action, and each observable result that the statement line does not already carry, one fact per bullet +- `given:` / `when:` / `then:` lists or Gherkin steps are allowed when the user asks for them; keep one shape per project +- Values that matter to the check are concrete (`named "Buy milk"`, `exits with status 0`), never descriptive (`a valid name`) + +## Conformance scenarios +- `***conformance tests***` holds the scenarios of the expected use of the functionality: its main path, with valid parameters, the expected caller, and the data present. For a REST endpoint: what it returns when the caller is authenticated, the data exists, and the parameters are valid +- These scenarios gate the functionality: it is done only when every conformance scenario passes +- When conformance tests are enabled for the module (its `config.yaml` has a `conformance-tests-script`), every functionality has at least one conformance scenario; an empty section is an error +- Cover the main path first, then each variant of expected use the functional spec states (another valid input shape, an optional parameter); stop when every expected behavior has one scenario +- Do not write the same scenario twice in different words; one scenario per distinct outcome + +## Acceptance scenarios +- `***acceptance tests***` holds the scenarios of use that is not expected: invalid parameters, wrong or missing authentication, missing data, an unavailable dependency. For the same REST endpoint: what it returns when authentication is wrong or the parameters are invalid +- Write one for every error and boundary outcome the functional spec states; the section may be empty when the spec states none +- An acceptance scenario is rendered after the conformance tests of its functionality pass +- A scenario that needs a dependency to fail (a backend that does not respond, a database that is unavailable) is written only when `***test reqs***` let the test cause that failure, for example with a local mock of that dependency; otherwise ask the user before writing it +- The kind of use decides the section, not the number of functionalities the scenario touches (see *Scenarios across functionalities*) + +## Scenarios across functionalities +- A scenario may use functionalities rendered before the one it is bound to, for its setup or as steps of a longer path +- Such a scenario is bound to the last functionality it exercises, because the earlier ones must exist when it runs +- It goes under `***conformance tests***` when every step is expected use, and under `***acceptance tests***` when any step is not; plain-forge writes one only when the user asks for it + +## Keeping test plain files in step with functional specs +- Every change to a functional spec is followed by a review of its test plain file: scenarios that no longer follow from the spec are removed or rewritten, and new behavior gets a scenario +- When a functionality is split, each new functionality gets its own test plain file; each scenario moves to the functionality it tests +- When a functionality is deleted, its test plain file is deleted +- When a functionality moves to another module, its test plain file moves to that module's folder and its `module:` key is updated +- When a func-id is renamed, `func_id:` is renamed in every test plain file bound to it and the file moves to the new conventional path + +## Relation to `***test reqs***` +- `***test reqs***` of the module say how `:ConformanceTests:` are built and run (framework, command, network policy); the scenarios say what they check +- A fact about the framework or the run command never goes into a scenario, and a scenario never goes into `***test reqs***` diff --git a/forge/rules/test-reqs.md b/forge/rules/test-reqs.md index 899c1b8..e5509eb 100644 --- a/forge/rules/test-reqs.md +++ b/forge/rules/test-reqs.md @@ -9,7 +9,7 @@ When writing or editing a `***test reqs***` section in a `.plain` file, always f ## Conformance tests only - Test reqs specify how **conformance tests** should be written and run - Unit test guidance goes in `***implementation reqs***`, not here -- Acceptance tests are nested under individual functional specs, not here +- Test scenarios, conformance and acceptance, live in test plain files, not here — see `test-plain-files.md` - Behavioral requirements go in `***functional specs***`, not here ## `:ConformanceTests:` lives here (hard rule) @@ -28,11 +28,11 @@ When writing or editing a `***test reqs***` section in a `.plain` file, always f ## Test type reference -| Test type | Where to specify | Generated | Purpose | -|-----------|-----------------|-----------|---------| -| Unit tests | `***implementation reqs***` | Automatically | Test individual functionalities in isolation | -| Conformance tests | `***test reqs***` | Automatically | Verify each functionality's implementation against its functional spec | -| Acceptance tests | `***acceptance tests***` under a functional spec | Manually | Verify implementation of a functional spec through a specific end-to-end scenario | +| Test type | Where to specify | Generated from | Purpose | +|-----------|-----------------|----------------|---------| +| Unit tests | `***implementation reqs***` | the implementation, by the renderer | Test individual functionalities in isolation | +| Conformance tests | how: `***test reqs***`; what: `***conformance tests***` of the functionality's test plain file | the conformance scenarios, one test per scenario-id | Verify the expected use of one functionality, its main path | +| Acceptance tests | how: `***test reqs***`; what: `***acceptance tests***` of the functionality's test plain file | the acceptance scenarios, one test per scenario-id | Verify one functionality under use that is not expected: invalid parameters, wrong or missing authentication, missing data | ## No duplication - Do not duplicate guidance already present in the file or its imports diff --git a/forge/skills/add-acceptance-test/SKILL.md b/forge/skills/add-acceptance-test/SKILL.md deleted file mode 100644 index bb95abd..0000000 --- a/forge/skills/add-acceptance-test/SKILL.md +++ /dev/null @@ -1,101 +0,0 @@ ---- -name: add-acceptance-test -description: >- - Add acceptance tests under a functional spec in a ***plain spec file. Use - when the user wants to add verification criteria for a specific functional - spec, or after adding a functional spec that needs testable success criteria. ---- - -# Add Acceptance Test - -Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. - -## Workflow - -1. **Identify the target `.plain` file and the functional spec** to attach the acceptance test to. If ambiguous, ask the user. -2. **Read the file** to understand the functional spec, existing acceptance tests (if any), and the `***test reqs***` section (acceptance tests are implemented according to test reqs). -3. **Draft the acceptance test(s)** following the rules below. -4. **Nest them** under the target functional spec using a `***acceptance tests***` subsection. -5. **Read the file again** to confirm correct placement, indentation, and syntax. - -## When to Add Acceptance Tests - -- The functional spec's correct behavior is **non-obvious** or easily misinterpreted. -- The spec involves **edge cases**, boundary conditions, or specific numeric outcomes. -- The user explicitly requests verification criteria. -- The spec was added via the `add-functional-spec` skill and warrants testable success criteria. - -## Format - -Acceptance tests are nested under the functional spec they verify, using a `***acceptance tests***` subsection: - -```plain -***functional specs*** - -- :Task: items are processed in batches of 100. - - ***acceptance tests*** - - Processing 250 :Task: items should result in 3 batches. - - - Each batch should contain at most 100 items. -``` - -Key formatting rules: -- The `***acceptance tests***` header is indented under the functional spec it belongs to. -- Each test bullet is indented under the `***acceptance tests***` header. -- Multiple acceptance tests can be listed under a single functional spec. - - -## Rules - -### Conformance with the Functional Spec - -An acceptance test is essentially an **example that illustrates** the functional spec — it clarifies intent by showing a concrete scenario. It may imply minor code changes, but those should not be substantial. The acceptance test **must be consistent with** the functional spec it is nested under. If the acceptance test asserts behavior that contradicts, narrows, or extends the functional spec in ways not implied by it, the system will reject it as a conflicting acceptance test. Before writing, re-read the parent functional spec and ensure every assertion in the acceptance test is a direct, logical consequence of what the spec states. - -``` -Functional spec: -- :Resource: items are returned sorted by creation date in descending order. - -Good (consistent): -- The first :Resource: in the response should have the most recent creation date. - -Bad (contradicts the spec — the spec says descending, not ascending): -- The first :Resource: in the response should have the oldest creation date. - -Bad (extends beyond the spec — the spec says nothing about limiting results): -- The response should contain at most 50 :Resource: items. -``` - -### Scope - -Each acceptance test verifies a specific, observable aspect of the functional spec it's nested under. Do not test behavior from other functional specs. - -### Testability - -Every acceptance test must describe a concrete, verifiable outcome — not a vague quality. The test should be implementable as an automated conformance test. - -``` -Good: - The response should contain exactly 3 items. -Bad: - The response should be correct. -``` - -### Language Agnosticism - -Like functional specs, acceptance tests must be written in terms of behavior and outcomes — not language-specific constructs. - -### Relationship to Test Reqs - -Acceptance tests extend conformance tests and are implemented according to the `***test reqs***` specification. They do not replace test reqs — they add spec-specific verification on top of the general testing framework defined there. - -## Validation Checklist - -- [ ] Every assertion is a direct logical consequence of the parent functional spec -- [ ] No assertion contradicts, narrows, or extends beyond the parent spec -- [ ] Acceptance test illustrates the spec via example, not introducing substantial new behavior -- [ ] Nested under the correct functional spec -- [ ] Indentation is correct (`***acceptance tests***` indented under the spec) -- [ ] Each test describes a concrete, verifiable outcome -- [ ] Trimmed per `concise-specs.md`: plain words, no obvious steps, no restatement of the parent spec -- [ ] Tests are scoped to the parent functional spec only -- [ ] Written in language-agnostic terms -- [ ] No duplication with existing acceptance tests on the same spec diff --git a/forge/skills/add-feature/SKILL.md b/forge/skills/add-feature/SKILL.md index 1ce3672..68bada9 100644 --- a/forge/skills/add-feature/SKILL.md +++ b/forge/skills/add-feature/SKILL.md @@ -4,7 +4,7 @@ description: >- End-to-end feature addition on an existing ***plain project: runs a short feature-intent interview, then takes the confirmed request and, one question at a time, incrementally writes ***plain specs (concepts, implementation reqs, functional specs, test reqs, - acceptance tests) to disk — asking, authoring, and reviewing per + test scenarios) to disk — asking, authoring, and reviewing per functionality — then closes with a plain-healthcheck gate. Use when the user wants to add a feature to an existing project. Not for bootstrapping a new project from scratch (use forge-plain) or for editing generated code (the @@ -15,6 +15,8 @@ description: >- Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. +If a target `.plain` file is in the old syntax (see *Old syntax* in `module-structure.md`), stop, name the file, and point the user to `migrate-plain-syntax`; do not edit it. + `add-feature` is the continuous-loop counterpart of `forge-plain`. Where `forge-plain` bootstraps an entire project from scratch, `add-feature` adds a single feature to an **existing** set of `.plain` specs. It begins with a short intent phase, then runs the authoring loop scoped to one feature. ## Core loop: one question → one answer → write to disk @@ -49,7 +51,7 @@ Carry the confirmed brief into every later phase. Before advancing, check the Ph Keep this short. The goal is to know enough to ask the **very first** writable question — not to design the whole feature on paper. 1. **Read the confirmed feature-intent brief.** Identify which existing `.plain` file(s) the feature most likely belongs to. -2. **Read the target `.plain` file(s)**, following their `import` and `requires` chains, so the existing definitions, implementation reqs, functional specs, test reqs, and acceptance tests are in context. This is what lets Phase 2 recognize impact when it surfaces. +2. **Read the target `.plain` file(s)**, following their `import` and `requires` chains, and the test plain files under `tests/`, so the existing definitions, implementation reqs, functional specs, test reqs, and scenarios are in context. This is what lets Phase 2 recognize impact when it surfaces. 3. **Pick the target module with one question — only if it is genuinely ambiguous** which file to modify. Otherwise skip it and start authoring immediately. End Phase 1 the moment the target file and a single concrete starter question are clear. Do **not** ask framing, scope, or multi-part design questions here. @@ -77,15 +79,16 @@ Route each answer to the right edit skill — never hand-author a `***plain` sec - **New concept** → `add-concept` (into `***definitions***`; define before any reference). - **New functional spec** → `add-functional-spec`. It runs `analyze-if-func-spec-too-complex` and `analyze-func-specs` for you — let it. **Never hand-author functional specs.** If it reports the spec is too complex, ask the user a follow-up question to split it (the next loop iteration) — do not break it down alone. +- **Changed functional spec** → edit its statement line or `- ` bullets in place, then invoke `add-test-scenarios` so its test plain file follows the new text. - **New implementation req** → `add-implementation-requirement`. When the answer states HOW the functionality is built — a retry or error-handling policy, a data format, a library, an architectural pattern — and it is not already in the file or its imports. -- **New acceptance test** → `add-acceptance-test`, under the relevant functional spec. Only when the *Conformance gate* is satisfied and the answer describes a concrete verification. +- **New test scenario** → `add-test-scenarios`, on the functionality's test plain file. Only when the *Conformance gate* is satisfied and the answer describes a concrete verification. - **New test req** → `add-test-requirement`. Only when conformance testing is configured and the answer changes how conformance tests are run. If the answer contradicts or refines an earlier snippet, fix that snippet in place now (per *Fix contradictions in place* above). ### 2c. Handle conflicts just-in-time -If `add-functional-spec` (via its analyzers) reports a conflict, or the snippet just written would **break** (contradict, invalidate) or **augment** (change the meaning of, add behavior to) an existing concept / functional spec / implementation req / test req / acceptance test, the **next question** must be about that conflict. Show the exact existing snippet in the question and offer: +If `add-functional-spec` (via its analyzers) reports a conflict, or the snippet just written would **break** (contradict, invalidate) or **augment** (change the meaning of, add behavior to) an existing concept / functional spec / implementation req / test req / test scenario, the **next question** must be about that conflict. Show the exact existing snippet in the question and offer: - **(a) keep** the existing spec — back out or narrow what was just written, - **(b) augment** the existing spec — embed the proposed new wording in the question, @@ -99,7 +102,7 @@ Ask the user whether the feature is fully covered. Yes → Phase 3. No → retur ### Conformance gate -Author `***test reqs***` and `***acceptance tests***` **only** when the project has a `config.yaml` with a valid `conformance-tests-script` entry pointing at an existing script in `test_scripts/`. Check the `config.yaml` that covers this module (multi-part projects may have several) and confirm the referenced script exists. If conformance testing is not configured, skip those two authoring paths entirely — concepts, functional specs, and implementation reqs are still authored normally. +Author `***test reqs***` and test plain files **only** when the project has a `config.yaml` with a valid `conformance-tests-script` entry pointing at an existing script in `test_scripts/`. Check the `config.yaml` that covers this module (multi-part projects may have several) and confirm the referenced script exists. If conformance testing is not configured, skip those two authoring paths entirely — concepts, functional specs, and implementation reqs are still authored normally. ## Phase 3 — Final review @@ -111,7 +114,7 @@ Most checks already happened in the loop; this is a slim consistency pass whose - Chronological ordering is correct end-to-end — no spec depends on something that comes after it. - Functional specs are language-agnostic. - Every external interface is explicit (endpoint paths, methods, CLI args, formats). - - Acceptance tests (if any) are consistent with their parent specs. + - Every new or changed functionality has a test plain file whose scenarios follow from its spec, with at least one conformance scenario when conformance tests are enabled. 3. Present the final diff for the modified file(s) and get the user's approval. 4. If the user requests changes, drop **straight back into the one-question loop** — one question, one write, one fix at a time. Do not restart the loop from scratch. 5. **Run `plain-healthcheck`** — the last check this skill runs. It validates every `config.yaml`, runs `plain-parser check` on every top module, and checks every module against the rules, so a feature is never finished while the project would fail to render. If it returns `FAIL`, work through its numbered list (fixing only `.plain` files / `config.yaml` / scripts — never generated code) by dropping back into the loop, then re-run. Repeat until `PASS`. Only then tell the user the feature is ready to render as `.plain`. diff --git a/forge/skills/add-feature/references/checklist.md b/forge/skills/add-feature/references/checklist.md index 6608ae2..a827104 100644 --- a/forge/skills/add-feature/references/checklist.md +++ b/forge/skills/add-feature/references/checklist.md @@ -27,12 +27,12 @@ Use this to verify the workflow was followed — never as a substitute for it. R - [ ] New concepts defined before they are referenced, with no circular references. - [ ] Any answer that contradicted an earlier snippet was fixed in place before the next question — no stale intent left on disk. - [ ] Every conflict / break / augment surfaced by the analyzers was put to the user as the *next* question (keep / augment / replace) and resolved before continuing. -- [ ] `***test reqs***` / `***acceptance tests***` authored only when the Conformance gate holds (a `config.yaml` with a valid `conformance-tests-script` pointing at an existing script). +- [ ] `***test reqs***` / test plain files authored only when the Conformance gate holds (a `config.yaml` with a valid `conformance-tests-script` pointing at an existing script). - [ ] Asked the user whether the feature is fully covered before leaving the loop. ## Phase 3 — Final review - [ ] Read the modified `.plain` file(s) in full. -- [ ] Verified: concepts defined before use, no cycles, chronological ordering correct end-to-end, functional specs language-agnostic, every external interface explicit (endpoints, methods, CLI args, formats), acceptance tests consistent with their parent specs. +- [ ] Verified: concepts defined before use, no cycles, chronological ordering correct end-to-end, functional specs language-agnostic, every external interface explicit (endpoints, methods, CLI args, formats), every new or changed functionality's test plain file in step with its spec. - [ ] Presented the final diff and got the user's approval (change requests dropped back into the one-question loop, not a restart). - [ ] Ran `plain-healthcheck`; worked its numbered list to `PASS` (fixing only `.plain`/`config.yaml`/scripts — never generated code) before declaring the feature ready and reminding the user to re-render `.plain`. diff --git a/forge/skills/add-functional-spec/SKILL.md b/forge/skills/add-functional-spec/SKILL.md index 7c95247..4a0b905 100644 --- a/forge/skills/add-functional-spec/SKILL.md +++ b/forge/skills/add-functional-spec/SKILL.md @@ -19,11 +19,12 @@ Always use the skill `load-plain-reference` to retrieve the ***plain syntax rule 1. **Identify the target `.plain` file.** If ambiguous, ask the user. 2. **Read the entire file** to understand existing definitions, implementation reqs, and all current functional specs (including those in `requires` modules). -3. **Draft the functional spec** following the rules below, then run the review pass in `concise-specs.md` over the draft — trim it to the shortest wording that keeps every fact before any other check. +3. **Pick the func-id and draft the functional spec.** Choose a func-id per `semantic-ids.md` and confirm no `# ` heading with that id exists in the module. Draft the statement line and its indented `- ` clarifications following the rules below, then run the review pass in `concise-specs.md` over the draft — trim it to the shortest wording that keeps every fact before any other check. 4. **Analyze complexity** — use the `analyze-if-func-spec-too-complex` skill to verify the drafted spec implies ≤ 200 LOC. If too complex, use the `break-down-func-spec` skill to split it, then repeat from step 3 for each resulting spec. 5. **Check for conflicts** with every existing functional spec — this is critical. Run `analyze-func-specs` **once** with the new spec plus all existing specs (in the file and in any `requires` chain) as a single batch. The batched analyzer returns every conflicting pair in one call — do **not** invoke a pair-by-pair analyzer. For each conflicting pair it reports, run `resolve-spec-conflict` on that pair; re-run `analyze-func-specs` over the touched set after each resolution until the verdict is `COMPATIBLE`. -6. **Append the spec** to the end of the `***functional specs***` section (specs are chronological; new ones go last). +6. **Append the spec** to the end of the `***functional specs***` section as a `# ` heading followed by its body, one blank line after the previous functionality (specs are chronological; new ones go last). 7. **Read the file again** to confirm correct placement and syntax. +8. **Write the test plain file.** When conformance tests are enabled for the module, invoke `add-test-scenarios` for the new functionality (see *Test Scenarios* below). ## Rules @@ -48,12 +49,13 @@ The new spec must not contradict any existing functional spec. Conflicting specs ### Disambiguation -Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation. If a single line is not enough to fully disambiguate the behavior, use **nested sub-bullets** to add detail. Nested lines clarify the parent spec; they do not introduce separate functionality. Even with nested detail, the spec must still imply ≤ 200 LOC. +Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation. If the statement line is not enough to fully disambiguate the behavior, add **indented `- ` bullets** under it. The bullets clarify the statement; they do not introduce separate functionality. Even with the bullets, the spec must still imply ≤ 200 LOC. ```plain ***functional specs*** -- A :User: can send a :Message: to a :Conversation:. +# send-message +A :User: can send a :Message: to a :Conversation:. - A :Message: must have non-empty content. - The :Message: is appended to the end of the :Conversation:. - All :Participant: members of the :Conversation: can see the new :Message:. @@ -68,9 +70,9 @@ Specs must be detailed enough that a developer can use the built software withou Functionality must be self-contained in the spec text. `requires` modules only import functional specs, so do not rely on implementation reqs to convey behavior — they won't be visible in downstream modules. -## Acceptance Tests +## Test Scenarios -If the functional spec needs verification, use the `add-acceptance-test` skill to add acceptance tests after inserting the spec. +After inserting the spec, invoke `add-test-scenarios` for it. It writes `tests//.plain` with at least one conformance scenario when conformance tests are enabled for the module. ## Validation Checklist @@ -83,3 +85,5 @@ If the functional spec needs verification, use the `add-acceptance-test` skill t - [ ] Trimmed per `concise-specs.md`: nothing obvious, no rationale, no uncheckable adjectives or hedges, plain words throughout - [ ] No redundancy with existing specs - [ ] Placed in correct chronological position (usually at the end) +- [ ] The func-id follows `semantic-ids.md` and is unique in the module +- [ ] `add-test-scenarios` was invoked for the new functionality when conformance tests are enabled diff --git a/forge/skills/add-functional-specs/SKILL.md b/forge/skills/add-functional-specs/SKILL.md index 52866dd..0274163 100644 --- a/forge/skills/add-functional-specs/SKILL.md +++ b/forge/skills/add-functional-specs/SKILL.md @@ -28,7 +28,7 @@ This skill is `add-functional-spec` applied **one spec at a time, in order**, wi 1. **Identify the target `.plain` file.** If ambiguous, ask the user. 2. **Read the entire file** to understand existing definitions, implementation reqs, and all current functional specs (including those reachable via `import` / `requires`). -3. **Draft all the specs you intend to add**, in the chronological order they should appear. Keep the drafts in a working list — nothing is inserted yet. Run the review pass in `concise-specs.md` over every draft before continuing; trim each to the shortest wording that keeps every fact. +3. **Draft all the specs you intend to add**, in the chronological order they should appear, each with its own func-id per `semantic-ids.md`, unique in the module and within the batch. Keep the drafts in a working list — nothing is inserted yet. Run the review pass in `concise-specs.md` over every draft before continuing; trim each to the shortest wording that keeps every fact. 4. **For each drafted spec, in order:** 1. **Analyze complexity.** Run `analyze-if-func-spec-too-complex` on the spec. If the verdict is `TOO COMPLEX`, run `break-down-func-spec`, replace the entry in the working list with the resulting smaller specs (preserving order), and restart this step on the first of the replacements. 2. **Check for conflicts.** Run `analyze-func-specs` **once** with this single input set: @@ -37,8 +37,9 @@ This skill is `add-functional-spec` applied **one spec at a time, in order**, wi - every spec from this batch that has already been inserted. The batched analyzer returns every conflicting pair in one call — do **not** loop over pairs invoking a per-pair analyzer. For each conflicting pair it reports, run `resolve-spec-conflict`. Apply the resolution — which may revise the new spec, an existing spec, or both — then re-run `analyze-func-specs` once over the touched set until the verdict is `COMPATIBLE`. - 3. **Append the spec** to the `***functional specs***` section. Specs are chronological; the new entry goes at the end (unless an earlier existing spec must come *after* it — in that rare case, insert at the correct position). + 3. **Append the spec** to the `***functional specs***` section as a `# ` heading followed by its body. Specs are chronological; the new entry goes at the end (unless an earlier existing spec must come *after* it — in that rare case, insert at the correct position). 5. **Read the file again** to confirm correct placement, ordering, and syntax for the full batch. +6. **Write the test plain files.** When conformance tests are enabled for the module, invoke `add-test-scenarios` once per inserted functionality, in order. Order matters: each new spec is inserted only after its complexity and conflict checks pass, so subsequent specs in the batch are always checked against an already-validated file. @@ -71,12 +72,13 @@ That coverage is achieved by running `analyze-func-specs` **once per spec being ### Disambiguation (per spec) -Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation. If a single line is not enough to fully disambiguate the behavior, use **nested sub-bullets** to add detail. Nested lines clarify the parent spec; they do not introduce separate functionality. Even with nested detail, the spec must still imply ≤ 200 LOC. +Each functional spec must be unambiguous — the renderer should have only one reasonable interpretation. If the statement line is not enough to fully disambiguate the behavior, add **indented `- ` bullets** under it. The bullets clarify the statement; they do not introduce separate functionality. Even with the bullets, the spec must still imply ≤ 200 LOC. ```plain ***functional specs*** -- A :User: can send a :Message: to a :Conversation:. +# send-message +A :User: can send a :Message: to a :Conversation:. - A :Message: must have non-empty content. - The :Message: is appended to the end of the :Conversation:. - All :Participant: members of the :Conversation: can see the new :Message:. @@ -91,9 +93,9 @@ Specs must be detailed enough that a developer can use the built software withou Functionality must be self-contained in the spec text. `requires` modules only import functional specs, so do not rely on implementation reqs to convey behavior — they won't be visible in downstream modules. -## Acceptance Tests +## Test Scenarios -If any of the newly added functional specs need verification, use the `add-acceptance-test` skill to add acceptance tests after the batch has been inserted. Do this per spec, not as part of the bulk pass. +After the batch has been inserted, invoke `add-test-scenarios` once per inserted functionality, in order. It writes each `tests//.plain` with at least one conformance scenario when conformance tests are enabled for the module. Do this per spec, not as part of the bulk pass. ## Anti-Patterns @@ -117,3 +119,5 @@ If any of the newly added functional specs need verification, use the `add-accep - [ ] Every spec trimmed per `concise-specs.md`: nothing obvious, no rationale, no uncheckable adjectives or hedges, plain words throughout - [ ] No redundancy with existing specs or within the batch - [ ] Specs placed in correct chronological positions +- [ ] Every func-id follows `semantic-ids.md` and is unique in the module and within the batch +- [ ] `add-test-scenarios` was invoked for every inserted functionality when conformance tests are enabled diff --git a/forge/skills/add-resource/SKILL.md b/forge/skills/add-resource/SKILL.md index 6b7ea6c..983ffda 100644 --- a/forge/skills/add-resource/SKILL.md +++ b/forge/skills/add-resource/SKILL.md @@ -35,7 +35,8 @@ Use standard markdown link syntax inside any spec section: ***functional specs*** -- An API conforming to the [API specification](resources/api_spec.yaml) is exposed. +# expose-api +An API conforming to the [API specification](resources/api_spec.yaml) is exposed. ``` ## Path Rules diff --git a/forge/skills/add-test-requirement/SKILL.md b/forge/skills/add-test-requirement/SKILL.md index 5257c48..3af681d 100644 --- a/forge/skills/add-test-requirement/SKILL.md +++ b/forge/skills/add-test-requirement/SKILL.md @@ -33,7 +33,7 @@ Test reqs specify how **conformance tests** should be written and run. Common co ## What Does NOT Belong Here - **Unit test guidance** — that goes in `***implementation reqs***` -- **Acceptance tests** — those are nested under individual functional specs in `***functional specs***` +- **Test scenarios** — conformance and acceptance scenarios live in test plain files, written with `add-test-scenarios` (see `test-plain-files.md`) - **Behavioral requirements** — those go in `***functional specs***` - **Technology choices for the implementation** — those go in `***implementation reqs***` @@ -42,8 +42,8 @@ Test reqs specify how **conformance tests** should be written and run. Common co | Test Type | Where to Specify | Purpose | |-----------|-----------------|---------| | Unit tests | `***implementation reqs***` | Test individual functionalities in isolation | -| Conformance tests | `***test reqs***` | Verify implementation conforms to the full spec | -| Acceptance tests | `***acceptance tests***` under a functional spec | Verify a specific functional spec | +| Conformance tests | how: `***test reqs***`; what: `***conformance tests***` of the test plain file | Verify the expected use of one functionality, its main path | +| Acceptance tests | how: `***test reqs***`; what: `***acceptance tests***` of the test plain file | Verify one functionality under use that is not expected: invalid parameters, wrong or missing authentication, missing data | ## Format diff --git a/forge/skills/add-test-scenarios/SKILL.md b/forge/skills/add-test-scenarios/SKILL.md new file mode 100644 index 0000000..455f057 --- /dev/null +++ b/forge/skills/add-test-scenarios/SKILL.md @@ -0,0 +1,96 @@ +--- +name: add-test-scenarios +description: >- + Write or update the test plain file of one functionality + (tests//.plain): conformance scenarios for its expected use + and acceptance scenarios for its unexpected use, in plain English. Use after + a functional spec is added or changed, when a functionality has no + conformance scenario, or when the user asks for a test scenario. +--- + +# Add Test Scenarios + +Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. This skill needs `test-plain-files.md`, `semantic-ids.md`, `func-specs.md`, and `concise-specs.md`. + +## What this skill writes + +One test plain file per functionality, at `tests//.plain`, with the frontmatter keys `module:` and `func_id:` and the two sections `***conformance tests***` and `***acceptance tests***`. Every scenario is a `# ` heading, one plain-English statement line, and `- ` bullets indented by two spaces when the line is not enough. Expected use goes under `***conformance tests***`, use that is not expected under `***acceptance tests***`. The renderer generates one test per scenario and nothing else, so the scenarios in this file are the only tests the functionality gets. + +## When to Use + +- `add-functional-spec` or `add-functional-specs` inserted a functionality — those skills invoke this one for each new functionality. +- A functional spec changed (edit, split, conflict resolution, move) and its scenarios must be brought back in step. +- A functionality has no test plain file, or an empty `***conformance tests***` section, while conformance tests are enabled. +- The user asks for a conformance or acceptance scenario. + +## Conformance gate + +Conformance tests are enabled for a module when the `config.yaml` that governs it has a `conformance-tests-script` entry pointing at an existing script. When they are enabled, every functionality must end up with at least one conformance scenario. When they are not enabled, write a test plain file only if the user asks for one, and say that conformance tests are off for the module. + +## Workflow + +1. **Identify the functionality.** Take the module name and func-id from the caller or the user. If the user points at a spec by its text, find its `# ` heading. If the core spec has no `# ` headings (old syntax), stop and point the user to `migrate-plain-syntax`. +2. **Read the context.** Read the functional spec (heading and body), the `***definitions***` the module sees (its own, its `import` modules, and the `exported_concepts` of its `requires` modules), the `***test reqs***`, and the functional specs that come before this one, because a scenario may rely on behavior they established. +3. **Locate or create the test plain file.** Look for `tests//.plain`. Also scan the frontmatter of every other `.plain` file under `tests/` for the same `(module, func_id)` pair; read those files too, so scenario-ids stay unique across the merged set. Create the file with the frontmatter and both sections if it does not exist. +4. **List and sort the outcomes the spec states.** Walk the statement line and every clarification bullet. Write down the main path and each boundary, validation, and error case the spec names. Sort each outcome: expected use (the main path and its valid variants) goes to `***conformance tests***`; use that is not expected (invalid parameters, wrong or missing authentication, missing data, an unavailable dependency) goes to `***acceptance tests***`. Do not add an outcome the spec does not state: a scenario may not extend the spec. If a needed outcome is missing from the spec, ask the user one question and route the answer into the functional spec through `add-functional-spec` (or an inline edit of the clarification bullets) before writing the scenario. +5. **Draft one scenario per outcome, in its section.** Give each a scenario-id per `semantic-ids.md` (short slug, unique for this functionality). Write the body in plain English: + - One statement line: a sentence that names the action and the expected result, with concrete values. + - When that line is not enough, `- ` bullets indented by two spaces, in order: the state before the action, the one action under test, and each observable result, one fact per bullet. + - `given:` / `when:` / `then:` lists are allowed when the user asks for them; otherwise plain English, and one shape per project. + Test the functionality through the interface the spec names, as a black box. A scenario may use earlier functionalities for its setup; a scenario that spans several functionalities is bound to the last one it exercises and is written only when the user asks for it. +6. **Check what the test can cause.** A scenario that needs a dependency to fail (a backend that does not respond, a database that is unavailable) is written only when `***test reqs***` let the test cause that failure, for example with a local mock of that dependency. Otherwise ask the user one question: add such a test req, or leave the scenario out. +7. **Run the review pass** from `concise-specs.md` over every scenario: no narration, no rationale, no step a reader would assume, concrete values, plain words. +8. **Write the file and read it again.** Confirm the frontmatter keys, the section order, at least one conformance scenario when conformance tests are enabled, and that no body line starts with `# ` or `***`. +9. **Review with the user** when running inside `forge-plain` or `add-feature`: show the scenarios as the snippet for that loop iteration and apply the response before moving on. + +## Updating existing scenarios + +- **The spec changed.** Compare every scenario with the new spec text. Remove or rewrite a scenario that no longer follows from the spec; add a scenario for every new outcome. Keep the scenario-id of a scenario whose meaning is unchanged, because the generated test carries it. +- **The functionality was split.** Each new functionality gets its own test plain file; move each scenario to the functionality it tests and write new scenarios for outcomes that have none. +- **The func-id was renamed.** Rename `func_id:` in every test plain file bound to it and move the file to the new conventional path. +- **The functionality was deleted.** Delete its test plain file; confirm with the user first when it holds hand-written scenarios. +- **The functionality moved to another module.** Move the file to that module's folder under `tests/` and update `module:`. + +## Rules + +- A scenario is a direct consequence of the functional spec: it does not contradict, narrow, or extend it. +- The kind of use decides the section, not the number of functionalities a scenario touches: expected use under `***conformance tests***`, everything else under `***acceptance tests***`. +- Scenarios are language-agnostic and black box: no function, class, or file of the implementation is named. +- Values that matter to the check are concrete (`named "Buy milk"`, `returns 404`); descriptive values (`a valid name`) are not checkable. +- One scenario per distinct outcome; the same outcome is never written twice in different words. +- Framework, run command, and network policy belong in `***test reqs***`, never in a scenario. +- The old nested `***acceptance tests***` subsection under a functional spec is never written. If the core spec still has one, stop and point the user to `migrate-plain-syntax`. + +## Format + +```plain +--- +module: task-manager +func_id: add-task +--- + +***conformance tests*** + +# add-valid-task +Adding a :Task: named "Buy milk" to an empty :TaskList: shows one :Task: named "Buy milk". + +***acceptance tests*** + +# reject-short-name +Adding a :Task: named "Go" to an empty :TaskList: is rejected. + - :App: shows the error "Name must be at least 3 characters". + - :TaskList: stays empty. +``` + +## Validation Checklist + +- [ ] The file is at `tests//.plain` with `module:` and `func_id:` naming an existing functionality +- [ ] `***conformance tests***` comes first, `***acceptance tests***` second; both sections exist +- [ ] At least one conformance scenario when conformance tests are enabled for the module +- [ ] Every outcome the functional spec states has one scenario, in the section its kind of use decides; no scenario states an outcome the spec does not +- [ ] No unexpected-use scenario under `***conformance tests***`, no expected-use scenario under `***acceptance tests***` +- [ ] Every scenario is a `# ` heading, a plain-English statement line, indented bullets when needed, and concrete values +- [ ] Scenario-ids follow `semantic-ids.md` and are unique across every test plain file bound to this functionality +- [ ] Scenarios are black box and language-agnostic +- [ ] Trimmed per `concise-specs.md` +- [ ] No nested `***acceptance tests***` was written into the core spec diff --git a/forge/skills/analyze-func-specs/SKILL.md b/forge/skills/analyze-func-specs/SKILL.md index 89f05cd..76bf2fd 100644 --- a/forge/skills/analyze-func-specs/SKILL.md +++ b/forge/skills/analyze-func-specs/SKILL.md @@ -19,7 +19,7 @@ A **set of two or more functional specs** to check against each other. Callers t - A new spec (or batch of new specs) **plus** the existing specs from the same file and any `requires` modules. - An explicit list of spec snippets the user wants compared. -Each spec must be identifiable. If the caller didn't label them, assign **stable identifiers** in order of appearance: `S1`, `S2`, `S3`, …. Use those identifiers in the output. If the caller provided their own IDs or quoted snippets, use those instead and keep them verbatim. +Each spec must be identifiable. Use its func-id (`:` when the batch spans modules). A draft that has no func-id yet gets a **stable identifier** in order of appearance: `S1`, `S2`, `S3`, …. Use those identifiers in the output. If the caller provided its own labels or quoted snippets, use those instead and keep them verbatim. The skill also needs the `***definitions***` section (and any imported definitions) so referenced `:Concepts:` can be resolved. diff --git a/forge/skills/analyze-if-func-spec-too-complex/SKILL.md b/forge/skills/analyze-if-func-spec-too-complex/SKILL.md index c906565..672c6a7 100644 --- a/forge/skills/analyze-if-func-spec-too-complex/SKILL.md +++ b/forge/skills/analyze-if-func-spec-too-complex/SKILL.md @@ -50,20 +50,25 @@ Too complex: ```plain ***functional specs*** -- A :User: can create, edit, delete, and archive :Task: items, with validation on all fields and confirmation dialogs for destructive actions. +# manage-tasks +A :User: can create, edit, delete, and archive :Task: items, with validation on all fields and confirmation dialogs for destructive actions. ``` Acceptable (one behavior each): ```plain ***functional specs*** -- A :User: can create :Task:. Only valid :Task: items can be added. +# create-task +A :User: can create :Task:. Only valid :Task: items can be added. -- A :User: can edit :Task:. +# edit-task +A :User: can edit :Task:. -- A :User: can delete :Task:. +# delete-task +A :User: can delete :Task:. -- A :User: can archive :Task:. +# archive-task +A :User: can archive :Task:. ``` @@ -84,18 +89,22 @@ Too complex: ```plain ***functional specs*** -- :Order: processing depends on :OrderType:. Standard orders are validated and stored. Express orders skip validation and are queued for immediate dispatch. Bulk orders are split into sub-orders of 100 items each, validated individually, and processed in parallel with progress tracking. +# process-order +:Order: processing depends on :OrderType:. Standard orders are validated and stored. Express orders skip validation and are queued for immediate dispatch. Bulk orders are split into sub-orders of 100 items each, validated individually, and processed in parallel with progress tracking. ``` Acceptable (separate the modes): ```plain ***functional specs*** -- A standard :Order: is validated and stored. +# process-standard-order +A standard :Order: is validated and stored. -- An express :Order: is queued for immediate dispatch without validation. +# process-express-order +An express :Order: is queued for immediate dispatch without validation. -- A bulk :Order: is split into sub-orders of 100 items each. +# split-bulk-order +A bulk :Order: is split into sub-orders of 100 items each. - Each sub-order is processed individually. ``` @@ -107,17 +116,21 @@ Too complex: ```plain ***functional specs*** -- :Resource: items are fetched from the external API with pagination support, retry on transient errors with exponential backoff, caching for 5 minutes, and logging of all API calls. +# fetch-resources +:Resource: items are fetched from the external API with pagination support, retry on transient errors with exponential backoff, caching for 5 minutes, and logging of all API calls. ``` Acceptable (separate concerns): ```plain ***functional specs*** -- :Resource: items are fetched from the external API. +# fetch-resources +:Resource: items are fetched from the external API. -- :Resource: items are fetched from the external API in pages. +# fetch-resources-paged +:Resource: items are fetched from the external API in pages. -- Fetching :Resource: items is retried on transient errors using exponential backoff. +# retry-resource-fetch +Fetching :Resource: items is retried on transient errors using exponential backoff. ``` ### 5. UI Complexity @@ -128,18 +141,22 @@ Too complex: ```plain ***functional specs*** -- The dashboard shows :User: profile, recent :Task: items in a sortable table, a notification bell with unread count, and a sidebar with navigation links that highlights the active page. +# dashboard +The dashboard shows :User: profile, recent :Task: items in a sortable table, a notification bell with unread count, and a sidebar with navigation links that highlights the active page. ``` Acceptable (build incrementally): ```plain ***functional specs*** -- A dashboard page is shown for :User:. +# dashboard-page +A dashboard page is shown for :User:. -- Recent :Task: items are shown in a sortable table on the dashboard. +# dashboard-recent-tasks +Recent :Task: items are shown in a sortable table on the dashboard. -- A notification indicator with the unread count is shown on the dashboard. +# dashboard-notifications +A notification indicator with the unread count is shown on the dashboard. ``` ### 6. Data Transformation Complexity @@ -157,18 +174,21 @@ Too complex (builds the component and uses it in one spec): ```plain ***functional specs*** -- A :Report: is exported to PDF, laying out multi-page tables with repeating headers and page numbers, and charts rendered from :Report: data. +# export-report-pdf +A :Report: is exported to PDF, laying out multi-page tables with repeating headers and page numbers, and charts rendered from :Report: data. ``` Acceptable (component in its own spec, then referenced): ```plain ***functional specs*** -- :PdfRenderer: lays out multi-page tables from structured content. +# pdf-renderer-tables +:PdfRenderer: lays out multi-page tables from structured content. - Table headers repeat at the top of each page. - Each page shows its page number. -- A :Report: is exported to PDF using :PdfRenderer:, embedding charts rendered from :Report: data. +# export-report-pdf +A :Report: is exported to PDF using :PdfRenderer:, embedding charts rendered from :Report: data. ``` When this indicator fires, the fix is `break-down-func-spec` Strategy 6 — extract the component into its own earlier spec and reference it. diff --git a/forge/skills/break-down-func-spec/SKILL.md b/forge/skills/break-down-func-spec/SKILL.md index 96103a6..b34a0d9 100644 --- a/forge/skills/break-down-func-spec/SKILL.md +++ b/forge/skills/break-down-func-spec/SKILL.md @@ -36,12 +36,13 @@ The functional spec to break down, plus the full `.plain` file it belongs to. - Independently meaningful (makes sense on its own with previous specs as context) - Self-contained (does not require a later spec to be useful) - Within the 200 LOC limit -5. **Draft the replacement specs** — write the smaller specs in chronological order. The first replacement spec typically sets up the foundation, and subsequent ones layer behavior on top. +5. **Draft the replacement specs** — write the smaller specs in chronological order, each with its own func-id per `semantic-ids.md`; the replacement that keeps the original's main behavior may keep the original's func-id. The first replacement spec typically sets up the foundation, and subsequent ones layer behavior on top. 6. **Verify functional completeness** — this is critical. The replacement specs taken together must cover **100% of the functionality** described in the original spec. Walk through every behavior, condition, and detail in the original and confirm it appears in exactly one of the replacement specs. Nothing may be lost, weakened, or left implicit. If any functionality from the original is missing, add it to the appropriate replacement spec or create an additional one. 7. **Verify each replacement spec** — run `analyze-if-func-spec-too-complex` on each to confirm it fits within the limit. 8. **Check for conflicts** — run `analyze-func-specs` **once** with the full set of replacement specs plus every existing spec in the file and its `requires` chain. The batched analyzer reports every conflicting pair (replacement × existing and replacement × replacement) in one call — do not loop over pairs. Resolve each reported pair with `resolve-spec-conflict`, then re-run `analyze-func-specs` over the touched set until the verdict is `COMPATIBLE`. -9. **Replace in the file** — remove the original spec and insert the replacement specs in its position, preserving chronological order. -10. **Read the file again** to confirm correct placement and syntax. +9. **Replace in the file** — remove the original functionality (heading and body) and insert the replacement functionalities, each as a `# ` heading with its body, in its position, preserving chronological order. +10. **Bring the test plain files in step** — invoke `add-test-scenarios` for each replacement functionality. It moves every scenario of the original's test plain file to the replacement it tests, writes scenarios for outcomes that have none, and deletes the original's file when its func-id no longer exists. +11. **Read the file again** to confirm correct placement and syntax. ## Splitting Strategies @@ -53,18 +54,22 @@ If the spec bundles multiple independently testable actions, split each into its ```plain ***functional specs*** -- A :User: can create, edit, and delete :Recipe: items, with validation on all fields. +# manage-recipes +A :User: can create, edit, and delete :Recipe: items, with validation on all fields. ``` **After:** ```plain ***functional specs*** -- A :User: can create a :Recipe:. Only valid :Recipe: items can be created. +# create-recipe +A :User: can create a :Recipe:. Only valid :Recipe: items can be created. -- A :User: can edit an existing :Recipe:. Validation rules apply to the edited fields. +# edit-recipe +A :User: can edit an existing :Recipe:. Validation rules apply to the edited fields. -- A :User: can delete a :Recipe:. +# delete-recipe +A :User: can delete a :Recipe:. ``` ### Strategy 2: Separate Setup from Behavior @@ -75,18 +80,22 @@ If the spec introduces a new construct and immediately defines complex behavior ```plain ***functional specs*** -- The :MealPlan: screen displays a weekly grid of :Slot: items, allows drag-and-drop reordering, and shows nutritional totals per day. +# meal-plan-screen +The :MealPlan: screen displays a weekly grid of :Slot: items, allows drag-and-drop reordering, and shows nutritional totals per day. ``` **After:** ```plain ***functional specs*** -- The :MealPlan: screen displays a weekly grid of :Slot: items. +# meal-plan-grid +The :MealPlan: screen displays a weekly grid of :Slot: items. -- A :User: can reorder :Slot: items within a day on the :MealPlan: screen using drag-and-drop. +# reorder-slots +A :User: can reorder :Slot: items within a day on the :MealPlan: screen using drag-and-drop. -- The :MealPlan: screen displays nutritional totals for each day. +# daily-nutrition-totals +The :MealPlan: screen displays nutritional totals for each day. ``` ### Strategy 3: Separate Core Logic from Cross-Cutting Concerns @@ -97,18 +106,22 @@ If the spec mixes primary functionality with error handling, retries, caching, p ```plain ***functional specs*** -- :Ingredient: data is fetched from the external API with pagination, retry on transient errors, and caching for 10 minutes. +# fetch-ingredients +:Ingredient: data is fetched from the external API with pagination, retry on transient errors, and caching for 10 minutes. ``` **After:** ```plain ***functional specs*** -- :Ingredient: data is fetched from the external API. +# fetch-ingredients +:Ingredient: data is fetched from the external API. -- :Ingredient: data is fetched from the external API in pages. +# fetch-ingredients-paged +:Ingredient: data is fetched from the external API in pages. -- Fetching :Ingredient: data is retried on transient errors. +# retry-ingredient-fetch +Fetching :Ingredient: data is retried on transient errors. ``` ### Strategy 4: Separate Conditional Paths @@ -119,20 +132,24 @@ If the spec describes different modes or branches, give each its own spec. ```plain ***functional specs*** -- :MealPlan: generation depends on :DietType:. Standard plans use round-robin assignment. Restrictive plans filter out excluded ingredients first, then apply round-robin. Custom plans allow manual slot-by-slot selection. +# generate-meal-plan +:MealPlan: generation depends on :DietType:. Standard plans use round-robin assignment. Restrictive plans filter out excluded ingredients first, then apply round-robin. Custom plans allow manual slot-by-slot selection. ``` **After:** ```plain ***functional specs*** -- A standard :MealPlan: is generated using round-robin :Recipe: assignment. +# generate-standard-plan +A standard :MealPlan: is generated using round-robin :Recipe: assignment. -- A restrictive :MealPlan: is generated. +# generate-restrictive-plan +A restrictive :MealPlan: is generated. - Excluded :Ingredient: items are filtered out first. - Round-robin :Recipe: assignment is then applied. -- A :User: can manually assign :Recipe: items to :Slot: items for a custom :MealPlan:. +# assign-custom-plan +A :User: can manually assign :Recipe: items to :Slot: items for a custom :MealPlan:. ``` ### Strategy 5: Build UI Incrementally @@ -143,18 +160,22 @@ If the spec describes a full screen, split into layout + individual interactive ```plain ***functional specs*** -- The :Dashboard: screen shows a summary card with stats, a scrollable list of recent :MealPlan: items, a floating action button to create a new plan, and a bottom navigation bar. +# dashboard-screen +The :Dashboard: screen shows a summary card with stats, a scrollable list of recent :MealPlan: items, a floating action button to create a new plan, and a bottom navigation bar. ``` **After:** ```plain ***functional specs*** -- The :Dashboard: screen shows a summary card with :MealFrameStats:. +# dashboard-summary-card +The :Dashboard: screen shows a summary card with :MealFrameStats:. -- The :Dashboard: screen shows a scrollable list of recent :MealPlan: items. +# dashboard-recent-plans +The :Dashboard: screen shows a scrollable list of recent :MealPlan: items. -- The :Dashboard: screen includes a button to create a new :MealPlan:. +# dashboard-new-plan-button +The :Dashboard: screen includes a button to create a new :MealPlan:. ``` ### Strategy 6: Extract a Reusable Technical Component @@ -167,18 +188,21 @@ Define the component concept in `***definitions***` (via `add-concept`) before t ```plain ***functional specs*** -- A :Report: is exported to PDF, laying out multi-page tables with repeating headers, page numbers, and charts rendered from :Report: data. +# export-report-pdf +A :Report: is exported to PDF, laying out multi-page tables with repeating headers, page numbers, and charts rendered from :Report: data. ``` **After:** ```plain ***functional specs*** -- :PdfRenderer: lays out multi-page tables from structured content. +# pdf-renderer-tables +:PdfRenderer: lays out multi-page tables from structured content. - Table headers repeat at the top of each page. - Each page shows its page number. -- A :Report: is exported to PDF using :PdfRenderer:, embedding charts rendered from :Report: data. +# export-report-pdf +A :Report: is exported to PDF using :PdfRenderer:, embedding charts rendered from :Report: data. ``` The extracted component spec and the rewritten original must together still cover 100% of the original functionality (step 6): the original must **reference** the component, never re-describe it. Keep the component's technology and architecture choices in `***implementation reqs***`, not in the functional spec. If the component is a self-contained subsystem or will be reused across modules, promote it to its own module with `create-requires-module` / `refactor-module` instead of a sibling spec. @@ -187,7 +211,7 @@ The extracted component spec and the rewritten original must together still cove The replacement specs take the position of the original spec. Earlier specs remain unchanged. The first replacement spec should make sense given only the specs above it. Each subsequent replacement spec can reference behavior from the ones before it. -If the original spec had acceptance tests, redistribute them to the most appropriate replacement spec — or drop tests that no longer apply to a single smaller spec and rewrite them. +The original functionality's test plain file is split the same way: each scenario moves to the replacement functionality it tests, a scenario that no longer fits one replacement is rewritten, and each replacement ends with at least one conformance scenario when conformance tests are enabled (see `add-test-scenarios`). ## Validation Checklist @@ -203,4 +227,4 @@ If the original spec had acceptance tests, redistribute them to the most appropr - [ ] Replacement specs are language-agnostic - [ ] Replacement specs trimmed per `concise-specs.md` — a split is not a licence to re-add prose - [ ] All external interfaces remain explicit -- [ ] Acceptance tests (if any) have been redistributed or rewritten +- [ ] Test plain files follow the split: one per replacement functionality, every original scenario moved or rewritten, none bound to a func-id that no longer exists diff --git a/forge/skills/check-plain-env/SKILL.md b/forge/skills/check-plain-env/SKILL.md index a84d6a3..b9cb8f7 100644 --- a/forge/skills/check-plain-env/SKILL.md +++ b/forge/skills/check-plain-env/SKILL.md @@ -43,7 +43,7 @@ The skill is a **derive → probe → report** pass. It does not stop at the fir ### Step 1 — Inventory the project 1. **Detect the host OS** first. Run `uname -s 2>/dev/null || echo "$OS"`. Use the result to (a) pick the right script extensions to inspect (`.sh` vs `.ps1`) and (b) pick the right install commands later (Homebrew, apt, dnf, pacman, choco, scoop, winget). -2. **List every `.plain` file** in the repo root and in any subdirectory that contains them. Note which are top modules (not `requires`-ed by anything) and which are import modules (under `template/`). +2. **List every `.plain` file** in the repo root and in any subdirectory that contains them. Note which are top modules (not `requires`-ed by any other module) and which are import modules (under `template/`). 3. **List every `config.yaml`** in the repo and read each one. Note the `*-script` keys it sets and the values they point at. 4. **List every script under `test_scripts/`** (both `.sh` and `.ps1` — projects can ship both). 5. **List every file under `resources/`** if the directory exists. Schemas and protocol files there often imply running services (e.g. a Postgres `.sql` schema implies a Postgres server). diff --git a/forge/skills/consolidate-concepts/SKILL.md b/forge/skills/consolidate-concepts/SKILL.md index 54ee9ba..0054cb0 100644 --- a/forge/skills/consolidate-concepts/SKILL.md +++ b/forge/skills/consolidate-concepts/SKILL.md @@ -72,7 +72,7 @@ For each affected module, determine the frontmatter changes: |--------|------| | Add the new import to `import` list | Module does not already import the target | | Remove concepts from `exported_concepts` | Concepts that were exported only to share definitions — now handled by the import | -| Keep concepts in `exported_concepts` | Concepts that downstream `requires` modules still need via export (because those downstream modules do not import the new template) | +| Keep concepts in `exported_concepts` | Concepts that downstream modules still need via export (because those downstream modules do not import the new template) | **Important:** `import` provides definitions to the importing module. `exported_concepts` on a `requires` module provides concepts to downstream modules in the build chain. If a downstream module will now `import` the shared template directly, the concept can be removed from `exported_concepts`. If a downstream module does **not** import the template, the concept must remain exported. @@ -112,7 +112,7 @@ For each module that had concepts moved out: ### 3c. Update downstream modules (if applicable) -If concepts were removed from a module's `exported_concepts` and downstream `requires` modules relied on them: +If concepts were removed from a module's `exported_concepts` and downstream modules relied on them: 1. Add the new import to the downstream module's `import` list so it gets the concepts directly. 2. Verify the downstream module's specs can still resolve all concept references. @@ -141,7 +141,7 @@ For every affected module (source modules, downstream modules, and the import mo - [ ] No `***functional specs***` were modified in any module - [ ] No `***implementation reqs***` were modified in any module - [ ] No `***test reqs***` were modified in any module -- [ ] No `***acceptance tests***` were modified in any module +- [ ] No test plain file was modified ### 4e. Structural validation @@ -165,7 +165,7 @@ Read each modified `.plain` file in full and present the changes to the user for ### Moving a concept that is only used locally If a concept is only referenced within a single module, consolidating it into a shared import adds unnecessary coupling. Keep it local. -### Forgetting to update downstream requires modules +### Forgetting to update downstream modules When a concept is removed from `exported_concepts`, any module that `requires` the source module and references that concept will break — unless it now imports the shared template. Always trace the full dependency chain. ### Breaking concept ordering @@ -175,7 +175,7 @@ Concepts in the import module must be topologically ordered. If `:Meal:` referen An import module cannot `import` a module that directly or indirectly imports it back. If the shared concepts reference concepts from an existing template, the new import module should itself import that template — not the other way around. ### Removing concepts from exported_concepts prematurely -Only remove a concept from `exported_concepts` if **every** downstream `requires` module that needs it will now get it via `import`. If even one downstream module does not import the shared template, the export must stay. +Only remove a concept from `exported_concepts` if **every** downstream module that needs it will now get it via `import`. If even one downstream module does not import the shared template, the export must stay. ## Validation Checklist @@ -186,8 +186,8 @@ Only remove a concept from `exported_concepts` if **every** downstream `requires - [ ] Every affected module's `import` list includes the target import module - [ ] `exported_concepts` updated correctly — no missing exports, no unnecessary exports - [ ] All concept references in all affected modules resolve correctly -- [ ] No functional specs, implementation reqs, test reqs, or acceptance tests were modified +- [ ] No functional specs, implementation reqs, test reqs, or test plain files were modified - [ ] Import module is in `template/` and has no functional specs or requires - [ ] No circular imports introduced -- [ ] Downstream requires modules updated if needed +- [ ] Downstream modules updated if needed - [ ] User approved the final result diff --git a/forge/skills/create-requires-module/SKILL.md b/forge/skills/create-requires-module/SKILL.md index efbdb85..73f9dc1 100644 --- a/forge/skills/create-requires-module/SKILL.md +++ b/forge/skills/create-requires-module/SKILL.md @@ -19,6 +19,7 @@ When this module is rendered: - The required module's generated code (`plain_modules//code`) is copied as the starting point. - The `***functional specs***` of the required module **and all its ancestors** become visible as **previous functional specs**. - Of the required module's definitions, only the concepts in its `exported_concepts` are visible, each with its full definition text. Export visibility is **not** transitive — to see an ancestor's exports, list that ancestor in `requires` too. +- The required module's test plain files stay with the required module. The new module gets its own test plain files under `tests//`, one per functionality it adds (see `test-plain-files.md`). All modules connected by `requires` form a single tree with a common root. **Diamond joins are prohibited**: multiple `requires` entries must lie on one root-to-tip ancestor path. The deepest entry is the attachment point; the other entries exist only to make those ancestors' exports visible. @@ -34,7 +35,7 @@ If you only need shared definitions and reqs (no functional specs, no generated 2. **Create the `.plain` file at the repository root** with YAML frontmatter containing the `requires` field. Modules with functional specs live at the root, not in `template/`. 3. **Review the functional specs of the whole ancestor chain** — they will be treated as previous requirements. Your new functional specs must not conflict with any of them. 4. **Review each listed module's `exported_concepts`** — only those concepts are available to reference; an ancestor's exports require listing that ancestor. -5. **Add module-specific content** — definitions, implementation reqs, test reqs, and functional specs unique to this module. +5. **Add module-specific content** — definitions, implementation reqs, test reqs, and functional specs unique to this module. Add every functional spec through `add-functional-spec` or `add-functional-specs`, which also create its test plain file. 6. **Check for conflicts** between your new functional specs and any spec in the ancestor chain. ## Format @@ -56,7 +57,8 @@ description: Extended module that builds on base_module ***functional specs*** -- :NewFeature: is available. +# new-feature +:NewFeature: is available. ``` A module can use both `requires` and `import` together. `requires` points to other root-level modules; `import` resolves from the default `template/` directory (no prefix needed). @@ -106,5 +108,6 @@ The current module may or may not be functionally related to the required module - [ ] Every listed module's `exported_concepts` provide the concepts you need - [ ] New functional specs do not conflict with any spec in the ancestor chain - [ ] Module has at least one functional spec and one implementation req +- [ ] Every functional spec has its test plain file under `tests//` - [ ] Both `requires` and `import` are used correctly (not mixed up) - [ ] YAML frontmatter is correctly formatted between `---` markers diff --git a/forge/skills/debug-specs/SKILL.md b/forge/skills/debug-specs/SKILL.md index 117c4dc..e282cb5 100644 --- a/forge/skills/debug-specs/SKILL.md +++ b/forge/skills/debug-specs/SKILL.md @@ -11,6 +11,8 @@ description: >- Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. +If a target `.plain` file is in the old syntax (see *Old syntax* in `module-structure.md`), stop, name the file, and point the user to `migrate-plain-syntax`; do not edit it. + ## When to Use - The user observes a bug in the running application (visual, behavioral, crash, performance). @@ -30,7 +32,7 @@ Generated code in `plain_modules//code/` and `plain_modules//tes ## Phase 1 — Understand the Context -1. **Read the `.plain` file(s)** for the module — frontmatter, definitions, implementation reqs, test reqs, and all functional specs. Also read `import` and `requires` chains. +1. **Read the `.plain` file(s)** for the module — frontmatter, definitions, implementation reqs, test reqs, and all functional specs. Also read `import` and `requires` chains, and the test plain files under `tests//`. 2. **Read the user's observation carefully.** Rephrase it back to confirm understanding. Identify: - What is the expected behavior? - What is the actual behavior? @@ -60,13 +62,13 @@ For each relevant file: ### 2c. Check conformance tests and unit tests (if relevant) If the bug manifests as a test failure: -1. Read the failing test — conformance tests in `plain_modules//tests//`, unit tests inside `plain_modules//code/`. +1. Read the failing test — conformance tests in `plain_modules//tests//`, each labelled with the scenario-id it implements, unit tests inside `plain_modules//code/`. 2. Understand what the test expects vs. what the implementation does. 3. Determine whether the test expectation is correct (matches the spec) or incorrect (doesn't match the spec). ## Phase 3 — Diagnose the Root Cause -Trace the issue from the generated code back to the specs. There are five possible root causes — determine which one applies **before** making changes: +Trace the issue from the generated code back to the specs. There are six possible root causes — determine which one applies **before** making changes: | Root Cause | Symptom | Fix | |------------|---------|-----| @@ -74,6 +76,7 @@ Trace the issue from the generated code back to the specs. There are five possib | **Missing spec** | The desired behavior is not covered by any spec. The renderer had no guidance and either did nothing or made an arbitrary choice. | Add a new functional spec using the `add-functional-spec` skill. | | **Conflicting specs** | Two specs contradict each other, causing the renderer to produce inconsistent behavior. | Use the `resolve-spec-conflict` skill. | | **Incorrect spec** | The spec explicitly describes the wrong behavior. The renderer implemented it faithfully, but the spec itself is wrong. | Rewrite the spec to describe the correct behavior. | +| **Incorrect scenario** | The spec is correct and the code matches it, but a scenario in the test plain file expects an outcome the spec does not state, or misses one it states. | Fix the scenario with `add-test-scenarios`. | | **Missing implementation req** | The spec is correct, but the implementation req doesn't provide enough guidance for the renderer to produce the right code (e.g., missing library, missing architectural constraint, missing platform detail). | Add or update the implementation req using the `add-implementation-requirement` skill. | ### How to diagnose @@ -85,7 +88,9 @@ Trace the issue from the generated code back to the specs. There are five possib 2. **Check for conflicts.** Could another spec be overriding or contradicting this behavior? - Read all functional specs that touch the same concepts. - If two specs are in tension → **conflicting specs**. -3. **Check implementation reqs.** Is the renderer missing guidance on how to implement correctly? +3. **Check the scenario.** When a conformance or acceptance test fails, find its scenario by the scenario-id in the test name and compare it with the spec. + - If the scenario contradicts or extends the spec → **incorrect scenario**. +4. **Check implementation reqs.** Is the renderer missing guidance on how to implement correctly? - Does the implementation req specify the right library, pattern, or constraint? - If a platform-specific or framework-specific detail is missing → **missing implementation req**. @@ -95,10 +100,11 @@ Apply the fix based on the diagnosed root cause. Use the appropriate skill: | Root Cause | Skill to Use | |------------|-------------| -| Ambiguous spec | Edit the spec inline — add sub-bullets or reword for clarity | +| Ambiguous spec | Edit the spec inline — add indented `- ` bullets under the statement line or reword for clarity | | Missing spec | `add-functional-spec` | | Conflicting specs | `resolve-spec-conflict` | | Incorrect spec | Edit the spec inline — rewrite the incorrect part | +| Incorrect scenario | `add-test-scenarios` | | Missing implementation req | `add-implementation-requirement` | ### Fix guidelines @@ -107,6 +113,7 @@ Apply the fix based on the diagnosed root cause. Use the appropriate skill: - **Preserve chronological order.** If adding a new spec, place it correctly relative to existing specs. - **Stay language-agnostic.** Functional specs describe behavior, not implementation. Platform-specific guidance belongs in implementation reqs. - **Respect the 200 LOC limit.** If a fix makes a spec too complex, use `break-down-func-spec` to split it. +- **Keep the test plain file in step.** After changing a functional spec, invoke `add-test-scenarios` for it so its scenarios still follow from the spec. - **Check for new conflicts.** After editing, verify the fix doesn't conflict with other specs by running `analyze-func-specs` once with the edited spec(s) plus every existing spec that touches the same concepts. The batched analyzer reports all conflicting pairs in a single call; resolve each with `resolve-spec-conflict`. ## Phase 5 — Verify and Report @@ -148,7 +155,7 @@ A test is failing. Use the test as the entry point. ``` Read failing test → Understand what it expects → Read the implementation → Determine if the test or the implementation is wrong → Trace back to the spec → -Fix the spec (or test reqs/acceptance tests if the test itself is wrong) +Fix the spec (or the test reqs / the scenario in the test plain file if the test itself is wrong) ``` ### Strategy 4: Differential @@ -179,11 +186,12 @@ A spec fix may affect other specs that reference the same concepts. Always check - [ ] User's observation is clearly understood (expected vs. actual behavior) - [ ] Generated code has been read to understand the current implementation -- [ ] Root cause is diagnosed (ambiguous / missing / conflicting / incorrect / missing impl req) +- [ ] Root cause is diagnosed (ambiguous / missing / conflicting / incorrect / incorrect scenario / missing impl req) - [ ] Fix is applied in the `.plain` file(s) only — no generated code modified - [ ] Fix is minimal and targeted to the observed bug - [ ] Fix does not introduce new conflicts with existing specs - [ ] Modified specs respect the 200 LOC limit - [ ] Modified specs remain language-agnostic - [ ] Chronological ordering is preserved +- [ ] Test plain file of every changed functionality brought in step via `add-test-scenarios` - [ ] Summary of findings and fix presented to the user diff --git a/forge/skills/forge-plain/SKILL.md b/forge/skills/forge-plain/SKILL.md index 8752949..a5f2c33 100644 --- a/forge/skills/forge-plain/SKILL.md +++ b/forge/skills/forge-plain/SKILL.md @@ -54,7 +54,7 @@ Use the dedicated edit skills for every write — never hand-author a `***plain` ## Review loop -After each authoring step, review **only what just changed** — never re-review the whole file. Pick the single most relevant snippet (one concept, one functional spec, the module frontmatter, one requirement, one acceptance test, one script change, one `config.yaml` entry) and embed it directly in the `AskUserQuestion` prompt so the user sees exactly what they approve. Frame each question around one of: +After each authoring step, review **only what just changed** — never re-review the whole file. Pick the single most relevant snippet (one concept, one functional spec, the module frontmatter, one requirement, one test plain file, one script change, one `config.yaml` entry) and embed it directly in the `AskUserQuestion` prompt so the user sees exactly what they approve. Frame each question around one of: - **Missing parts** — something that should be in the snippet but isn't (an attribute, a validation rule, an edge case, a missing concept). - **Possible extensions** — behavior or detail that could reasonably be expanded. @@ -84,7 +84,7 @@ When entering a phase, read its reference file and walk its topics **in order** | 0 — What is the intent? | `references/phase-0-intent.md` | a concise intent brief covering the problem, primary user and outcome, and initial boundary is explicitly confirmed; nothing is written to disk | | 1 — What are we building? | `references/phase-1-product.md` | the new `***definitions***` and `***functional specs***`, plus any technology-free `***implementation reqs***`, are on disk and approved | | 2 — What tech should it use? | `references/phase-2-tech.md` | the new `***implementation reqs***` are on disk and approved | -| 3 — How is testing done? | `references/phase-3-testing.md` | the `***test reqs***` (and `***acceptance tests***` if conformance is on) are on disk, the `test_scripts/` and `config.yaml`(s) exist, and `check-plain-env` passed or each gap was acknowledged | +| 3 — How is testing done? | `references/phase-3-testing.md` | the `***test reqs***` (and the test plain files if conformance is on) are on disk, the `test_scripts/` and `config.yaml`(s) exist, and `check-plain-env` passed or each gap was acknowledged | | 4 — Validate and hand off | `references/phase-4-validate-handoff.md` | `plain-healthcheck` returned `PASS`, and the user has the render target plus every side-channel command | Between phases, summarize and get explicit overall confirmation before continuing — the intent brief diff --git a/forge/skills/forge-plain/references/checklist.md b/forge/skills/forge-plain/references/checklist.md index ce36afe..b64fdac 100644 --- a/forge/skills/forge-plain/references/checklist.md +++ b/forge/skills/forge-plain/references/checklist.md @@ -37,7 +37,7 @@ A box is met only when its source confirms it. Return `APPROVED` only when every - [ ] **[disk]** `.plain` module structure created with YAML frontmatter; template (if any) has no `***functional specs***`. - [ ] **[both]** Every concept authored in `***definitions***` via `add-concept`, defined before use. - [ ] **[both]** Every feature authored as functional specs via `add-functional-spec(s)`, each ≤200 LOC, in chronological build order. -- [ ] **[disk]** No `***test reqs***` or `***acceptance tests***` written in this phase. +- [ ] **[disk]** No `***test reqs***` or test plain files written in this phase. - [ ] **[disk]** Every `***implementation reqs***` bullet written this phase is technology-free policy — none names a language, framework, library, storage engine, or deployment target. - [ ] **[record]** Summarized the full feature list and module/concept layout; got explicit overall confirmation. @@ -45,7 +45,7 @@ A box is met only when its source confirms it. Return `APPROVED` only when every - [ ] **[record]** Read `references/phase-2-tech.md` and walked its topics in order (language, frameworks, storage, external services, structure/architecture, other constraints, anything else). - [ ] **[disk]** Every requirement authored into `***implementation reqs***` at the right scope (shared → template, module-specific → module). -- [ ] **[disk]** No `***test reqs***` or `***acceptance tests***` written in this phase. +- [ ] **[disk]** No `***test reqs***` or test plain files written in this phase. - [ ] **[record]** Summarized the tech stack and architecture; got explicit overall confirmation. ## Phase 3 — How is testing done? @@ -54,7 +54,7 @@ A box is met only when its source confirms it. Return `APPROVED` only when every - [ ] **[disk]** Honored the hard partition — every `:UnitTests:` fact in `***implementation reqs***`, every `:ConformanceTests:` fact in `***test reqs***`; never shared a bullet. - [ ] **[record]** Walked topics in order (unit framework, unit types/architecture, conformance decision, prepare-environment decision, layout, execution, other constraints, anything else). - [ ] **[disk]** Generated the needed scripts under `test_scripts/` via the `implement-*-script` skills and added each `*-script:` entry to the right `config.yaml`. -- [ ] **[both]** Conformance decision was asked explicitly; if yes, walked every Phase-1 functional spec one at a time for acceptance tests via `add-acceptance-test` (acceptance tests present on disk when the decision was yes). +- [ ] **[both]** Conformance decision was asked explicitly; if yes, walked every Phase-1 functionality one at a time via `add-test-scenarios` (a test plain file with at least one conformance scenario on disk for every functionality when the decision was yes). - [ ] **[record]** Prepare-environment decision was asked explicitly and recorded. - [ ] **[record]** Recapped the testing strategy; got explicit overall confirmation. - [ ] **[record]** Ran `check-plain-env`; it returned `PASS`, or `WARN`/`FAIL` with each remaining item explicitly acknowledged by the user (re-invoked after any install). diff --git a/forge/skills/forge-plain/references/phase-1-product.md b/forge/skills/forge-plain/references/phase-1-product.md index 73eeb0e..502899a 100644 --- a/forge/skills/forge-plain/references/phase-1-product.md +++ b/forge/skills/forge-plain/references/phase-1-product.md @@ -10,10 +10,10 @@ The moment the user answers, write the resulting snippet to disk using the right - **Module structure** — create or update the `.plain` file(s) (single module, template + modules, or chained modules). Set up YAML frontmatter (`import`, `requires`, `description`) and the proposed module name. If a template is used, create it in `template/` **without** a `***functional specs***` section. Use the `create-import-module` skill where applicable. - **`***definitions***`** — add or refine concepts (entities, attributes, relationships). Define every concept before it is referenced. Use the `add-concept` skill. -- **`***functional specs***`** — translate each answer into a single chronological, incremental spec (≤200 lines of code change, language-agnostic, no conflicts). Use `add-functional-spec` for one new spec; use `add-functional-specs` only when a single answer naturally decomposes into a tight cluster of specs flowing from the same answer (e.g. list / create / delete CRUD on one entity). **Never hand-author the `***functional specs***` section directly.** +- **`***functional specs***`** — translate each answer into a single chronological, incremental spec (≤200 lines of code change, language-agnostic, no conflicts), headed by a func-id per `semantic-ids.md`. Use `add-functional-spec` for one new spec; use `add-functional-specs` only when a single answer naturally decomposes into a tight cluster of specs flowing from the same answer (e.g. list / create / delete CRUD on one entity). **Never hand-author the `***functional specs***` section directly.** - **`***implementation reqs***`** — only a requirement that can be stated without naming a technology: an error-handling or retry policy, an idempotency or ordering guarantee, a data-format or precision rule. Use the `add-implementation-requirement` skill, on the module whose behavior it constrains. The moment stating it takes a language, framework, library, storage engine, or deployment target, it is a Phase 2 answer — note it and steer back. -Do **not** add `***test reqs***` or `***acceptance tests***` in this phase — they belong to later phases. +Do **not** add `***test reqs***` or test plain files in this phase — they belong to later phases. ## Topics (in order) diff --git a/forge/skills/forge-plain/references/phase-2-tech.md b/forge/skills/forge-plain/references/phase-2-tech.md index 7b878e6..eee9674 100644 --- a/forge/skills/forge-plain/references/phase-2-tech.md +++ b/forge/skills/forge-plain/references/phase-2-tech.md @@ -9,7 +9,7 @@ Write each requirement into `***implementation reqs***`: - Shared, stack-wide reqs (language, framework, architecture, coding standards) go on the template module if one exists. - Module-specific reqs (e.g. a data-storage choice or an external integration only one module uses) go on the module that needs them. -Do **not** add `***test reqs***` or `***acceptance tests***` in this phase — they belong to later phases. +Do **not** add `***test reqs***` or test plain files in this phase — they belong to later phases. ## Topics (in order) diff --git a/forge/skills/forge-plain/references/phase-3-testing.md b/forge/skills/forge-plain/references/phase-3-testing.md index b627b2f..912f28b 100644 --- a/forge/skills/forge-plain/references/phase-3-testing.md +++ b/forge/skills/forge-plain/references/phase-3-testing.md @@ -1,6 +1,6 @@ # Phase 3 — How should testing be done? -Gather the testing strategy. This phase covers `***test reqs***`, `***acceptance tests***`, the scripts under `test_scripts/`, and the `config.yaml` file(s) that wire them in. Run the core loop (ask → author → review) from `SKILL.md` for each topic below, in order. When the user has no preference, propose a default that fits the language and stack chosen in Phase 2 and ask them to confirm. +Gather the testing strategy. This phase covers `***test reqs***`, the test plain files under `tests/`, the scripts under `test_scripts/`, and the `config.yaml` file(s) that wire them in. Run the core loop (ask → author → review) from `SKILL.md` for each topic below, in order. When the user has no preference, propose a default that fits the language and stack chosen in Phase 2 and ask them to confirm. ## Hard partition (applies to every topic) @@ -36,10 +36,10 @@ template_dir: template - Author a `:UnitTests:` framework requirement in `***implementation reqs***` at the appropriate scope (template if shared, otherwise on the module) — e.g. "`:UnitTests:` should use pytest" plus "`:UnitTests:` are run via `pytest tests/`". Generate `run_unittests` (and any framework config it needs, e.g. `pytest.ini`, `jest.config.js`) via `implement-unit-testing-script`. Add the `unittests-script:` entry to the relevant `config.yaml`(s), creating each file if it doesn't exist yet. - Review the framework req, the generated script paths, and the new `config.yaml` entry. 2. **Unit-test types and architecture mapping** — which of unit tests and integration tests the user wants, and how tests map to the Phase 2 architectural layers (e.g. one test module per service, repository tests with an in-memory store). Author a `:UnitTests:` scope / architecture requirement in `***implementation reqs***`, phrased in terms of `:UnitTests:` so the partition is visible. Review that requirement. -3. **Conformance testing** — explicitly ask whether conformance / end-to-end tests should be part of the project. This drives whether `run_conformance_tests` is generated and whether `***acceptance tests***` are authored. If the user is unsure, briefly explain the tradeoff (extra scripts + per-spec acceptance tests vs. lighter setup) and let them choose. - - If **yes**: author a conformance-testing requirement in `***test reqs***` (framework, execution command, any constraints); generate `run_conformance_tests` via `implement-conformance-testing-script`; add the `conformance-tests-script:` entry to the relevant `config.yaml`(s); then **walk every functional spec authored in Phase 1, one at a time** — for each spec, ask one `AskUserQuestion` whether it needs concrete verification, and if yes author one acceptance test under that spec via `add-acceptance-test`, then review that acceptance test as a snippet before moving to the next spec. Never bulk-write acceptance tests; never ask about more than one spec per call. - - If **no**: record the decision; skip the conformance script, its config entry, and acceptance-test authoring entirely. - - Review the conformance req (if any), the new script and config entry (if any), and each acceptance test snippet (if any). +3. **Conformance testing** — explicitly ask whether conformance / end-to-end tests should be part of the project. This drives whether `run_conformance_tests` is generated and whether test plain files with conformance scenarios are authored. If the user is unsure, briefly explain the tradeoff (extra scripts + one test plain file per functionality vs. lighter setup) and let them choose. + - If **yes**: author a conformance-testing requirement in `***test reqs***` (framework, execution command, any constraints); generate `run_conformance_tests` via `implement-conformance-testing-script`; add the `conformance-tests-script:` entry to the relevant `config.yaml`(s); then **walk every functionality authored in Phase 1, one at a time** — for each one, invoke `add-test-scenarios` to write its test plain file with at least one conformance scenario, then review those scenarios as a snippet with one `AskUserQuestion` and apply the answer before moving to the next functionality. Never bulk-write scenarios; never ask about more than one functionality per call. + - If **no**: record the decision; skip the conformance script, its config entry, and test plain file authoring entirely. + - Review the conformance req (if any), the new script and config entry (if any), and each test plain file snippet (if any). 4. **Environment preparation script** — explicitly ask whether a `prepare_environment` script should be generated. It is the single entry point for installing dependencies and setting up fixtures/services before tests run. If the user is unsure, note it is recommended when there are dependencies to install or services to start, and skippable when the project genuinely has nothing to prepare. - If **yes**: generate `prepare_environment` via `implement-prepare-environment-script`; add the `prepare-environment-script:` entry to the relevant `config.yaml`(s); if the script's responsibilities are non-trivial and worth pinning in the spec, add a brief `***test reqs***` entry describing what `prepare_environment` is responsible for. - If **no**: record the decision; skip the script and the config entry. diff --git a/forge/skills/init-config-file/SKILL.md b/forge/skills/init-config-file/SKILL.md index 17d36b3..d66a38c 100644 --- a/forge/skills/init-config-file/SKILL.md +++ b/forge/skills/init-config-file/SKILL.md @@ -53,7 +53,7 @@ These keys reflect choices made in Phase 3 of `forge-plain`: | `conformance-tests-script` | path (string) | — | Include when the user opted into conformance testing in Phase 3. | | `prepare-environment-script` | path (string) | — | Include only when both (a) the user opted into a prepare-environment script and (b) `conformance-tests-script` is also set. Setting prepare without conformance is a hard `plain-healthcheck` failure. | | `test-script-timeout` | int (seconds) | `120` | Include only when the user explicitly raised/lowered the default. | -| `template-dir` | path (string) | — | Include whenever the project has an `import` module or a custom template directory (e.g. `template/`). Required for projects with shared templates. `plain-parser check` reads it too, so `plain-healthcheck` finds the templates. | +| `template-dir` | path (string) | — | Include whenever the project has an import module or a custom template directory (e.g. `template/`). Required for projects with shared templates. `plain-parser check` reads it too, so `plain-healthcheck` finds the templates. | | `build-folder` | string | `plain_modules` | Include only when the user picked a non-default folder name. Must differ from `build-dest`. The renderer writes `//code` (implementation + unit tests) and `//tests` (conformance tests, one folder per functional spec). | | `build-dest` | string | `dist` | **Always include with the value `dist`.** This skill pins the output destination explicitly so every project's `config.yaml` has the same, predictable target folder, visible in the file. Must differ from `build-folder`. | @@ -73,7 +73,7 @@ Before emitting anything, state the planned split to the user (e.g. "I'll emit ` ### Step 1 — Inventory -1. List every `.plain` file in the repo and identify the top modules (modules not `requires`-ed by anything else) — same procedure as `plain-healthcheck` Step 1. +1. List every `.plain` file in the repo and identify the top modules (modules not `requires`-ed by any other module) — same procedure as `plain-healthcheck` Step 1. 2. For each top module, determine which part it belongs to (single-stack → one part; multi-part → one part per top module). 3. List every script under `test_scripts/` and group them by part (e.g. `*_python.sh` belongs to the backend part, `*_js.sh` belongs to the frontend part). 4. Identify the template directory (typically `template/`) and any custom resource directories (typically `resources/`). diff --git a/forge/skills/init-plain-project/SKILL.md b/forge/skills/init-plain-project/SKILL.md index f0ea5cb..2ba1bcc 100644 --- a/forge/skills/init-plain-project/SKILL.md +++ b/forge/skills/init-plain-project/SKILL.md @@ -17,21 +17,22 @@ Always use the skill `load-plain-reference` to retrieve the ***plain syntax rule ## Scope -This skill is intentionally **minimal**. It produces a runnable project skeleton — not a complete spec. **No `***definitions***` sections are written by this skill — not in the template module, not in the top module.** No functional specs, no concepts, no acceptance tests, no validation. The user will fill those in afterwards with `add-feature`, `add-concept`, `add-functional-spec`, etc. +This skill is intentionally **minimal**. It produces a runnable project skeleton — not a complete spec. **No `***definitions***` sections are written by this skill — not in the template module, not in the top module.** No functional specs, no concepts, no test plain files, no validation. The user will fill those in afterwards with `add-feature`, `add-concept`, `add-functional-spec`, etc. What this skill writes: - `template/base.plain` — an import module with the project's base `***implementation reqs***` and `***test reqs***`. -- `.plain` — a top-level module that imports `base`. Frontmatter only — no body sections. The user adds `***functional specs***` later. +- `.plain` — a top-level module that imports `base`. Frontmatter only — no body sections. The user adds `***functional specs***` later, each with its test plain file under `tests//`. - `test_scripts/run_unittests_.` — always. - `test_scripts/run_conformance_tests_.` — only if conformance testing is enabled. - `test_scripts/prepare_environment_.` — only if conformance testing is enabled **and** the user opts in. - `config.yaml` at the project root — wired to whichever scripts were generated. +- `tests/` — an empty folder at the project root, only if conformance testing is enabled. `add-functional-spec` later writes each functionality's test plain file to `tests//.plain`. What this skill does **not** do: - Run `plain-healthcheck`. -- Author concepts, functional specs, or acceptance tests. +- Author concepts, functional specs, or test plain files. - Probe the host environment (`check-plain-env` is a separate step the user can run later). ## Workflow @@ -51,7 +52,7 @@ Follow up with a free-form prompt for anything the options didn't cover (e.g. sp Use **AskUserQuestion** for one tight batch covering: - **Unit testing framework** — propose the canonical one for the chosen language (pytest, Jest, Go `testing`, JUnit, ...) and let the user override. -- **Conformance testing** — enabled or not. If the user is unsure, briefly explain it adds a `run_conformance_tests` script and per-spec acceptance tests later. +- **Conformance testing** — enabled or not. If the user is unsure, briefly explain it adds a `run_conformance_tests` script and, for every functionality added later, a test plain file with conformance scenarios. - **Prepare-environment script** — only ask this if conformance testing was enabled in the previous question. Generate one or skip. Recommend "yes" when dependencies need installing (most stacks); recommend "skip" when there is genuinely nothing to set up. If conformance testing is disabled, skip this question entirely and do not generate a prepare-environment script. ### 3. Author `template/base.plain` @@ -90,7 +91,11 @@ Body: **none**. The file ends after the closing `---` of the frontmatter. Do **n The file must still be created on disk — an empty body is fine, but the file itself is required. -### 5. Generate the testing scripts +### 5. Create the `tests/` folder + +Only if conformance testing is enabled: create an empty `tests/` folder at the project root. Test plain files land in `tests//` as functionalities are added. If conformance testing is disabled, create nothing. + +### 6. Generate the testing scripts Call the corresponding implementation skills in order. Each skill knows how to write the right Bash or PowerShell variant for the host OS. @@ -98,7 +103,7 @@ Call the corresponding implementation skills in order. Each skill knows how to w - `implement-conformance-testing-script` — only if conformance testing was enabled. If a prepare-environment script will also exist, choose the **activate-only** variant; otherwise the **install-inline** variant. - `implement-prepare-environment-script` — only if conformance testing was enabled **and** the user opted in. -### 6. Write `config.yaml` +### 7. Write `config.yaml` Create `config.yaml` at the project root with **only the keys for scripts that were actually generated**. Valid keys: @@ -113,12 +118,12 @@ Always include `template_dir: template` because the project has a template impor Do **not** invoke `init-config-file` or `plain-healthcheck` here — keeping this skill light is the whole point. The user can run them later if they want a strict canonicalization or validation. -### 7. Recap +### 8. Recap Tell the user what was created and what's next: -- Files written: `template/base.plain`, `.plain`, the test scripts, `config.yaml`. -- Suggested next steps: add concepts with `add-concept`, add functional specs with `add-functional-spec` or `add-functional-specs`, or jump straight into `add-feature`. +- Files written: `template/base.plain`, `.plain`, the test scripts, `config.yaml`, and the `tests/` folder when conformance testing is enabled. +- Suggested next steps: add concepts with `add-concept`, add functional specs with `add-functional-spec` or `add-functional-specs` (each new functionality gets its test plain file under `tests//` when conformance testing is enabled), or jump straight into `add-feature`. - Mention that the project has not been validated — they can run `plain-healthcheck` when they want validation. ## Validation checklist diff --git a/forge/skills/init-plain-project/references/checklist.md b/forge/skills/init-plain-project/references/checklist.md index 7c875c1..5239687 100644 --- a/forge/skills/init-plain-project/references/checklist.md +++ b/forge/skills/init-plain-project/references/checklist.md @@ -23,6 +23,7 @@ Use this to verify the scaffold was produced correctly — never as a substitute - [ ] Generated `implement-unit-testing-script` (always). - [ ] Generated `implement-conformance-testing-script` only if conformance was enabled (activate-only variant when a prepare-environment script will also exist, otherwise install-inline). - [ ] Generated `implement-prepare-environment-script` only if conformance was enabled **and** the user opted in. +- [ ] Created an empty `tests/` folder at the project root only when conformance testing is enabled. - [ ] Wrote `config.yaml` at the project root with only the keys for scripts actually generated, plus `template_dir: template`, using the correct `.sh`/`.ps1` extension for the host OS. - [ ] Did **not** run `init-config-file`, `plain-healthcheck`, or `check-plain-env` (out of scope for this skill). diff --git a/forge/skills/load-plain-reference/SKILL.md b/forge/skills/load-plain-reference/SKILL.md index b7ba05b..beda63c 100644 --- a/forge/skills/load-plain-reference/SKILL.md +++ b/forge/skills/load-plain-reference/SKILL.md @@ -2,7 +2,7 @@ name: load-plain-reference description: >- Loads the applicable ***plain authoring rules and operational references for the current task. - Covers section ownership, concepts, modules, resources, integrations, rendering, and testing + Covers section ownership, concepts, func-ids, test plain files, modules, resources, integrations, rendering, and testing without loading unrelated guidance. Use when authoring, editing, reviewing, or debugging .plain files, or before invoking another skill that reads or writes .plain content. --- @@ -42,7 +42,8 @@ All paths are relative to this `SKILL.md`. | New module, file skeleton, section placement, or comments | `../../rules/module-structure.md` | | Moving facts between sections (see `resolve-section-ownership`) | `../../rules/module-structure.md`, plus the rule file of each section involved | | Definitions or concept usage | `../../rules/definitions.md` | -| Functional specs or acceptance tests | `../../rules/func-specs.md` | +| Functional specs | `../../rules/func-specs.md`, `../../rules/semantic-ids.md` | +| Test plain files or scenarios | `../../rules/test-plain-files.md`, `../../rules/semantic-ids.md` | | Implementation requirements or unit tests | `../../rules/impl-reqs.md` | | Test requirements or conformance tests | `../../rules/test-reqs.md` | | `import` modules | `../../rules/import-modules.md` | @@ -52,6 +53,7 @@ All paths are relative to this `SKILL.md`. | Linked files or `resources/` | `../../rules/linked-resources.md` | | Any `.plain` text or example | `../../rules/bullet-continuation.md`, `../../rules/concise-specs.md` | | Drafting or trimming spec wording, or reviewing a spec for verbosity | `../../rules/concise-specs-examples.md` | +| A file that may be in the old syntax (nested `***acceptance tests***`, `- ` functional specs) | `../../rules/module-structure.md` (*Old syntax*), then the `migrate-plain-syntax` skill | For a whole-file review or a task spanning several sections, load the union of the applicable rule files. Do not load every rule by default. diff --git a/forge/skills/load-plain-reference/references/project-model.md b/forge/skills/load-plain-reference/references/project-model.md index 72b74c3..b374979 100644 --- a/forge/skills/load-plain-reference/references/project-model.md +++ b/forge/skills/load-plain-reference/references/project-model.md @@ -14,10 +14,11 @@ those specifications. ```text *.plain # Root specification modules template/*.plain # Reusable import modules +tests//.plain # Test plain files, one per functionality resources/ # Linked text artifacts such as schemas and fixtures plain_modules/ # Generated output, one directory per module /code/ # implementation and unit tests - /tests/ # conformance tests, one folder per functional spec + /tests/ # conformance tests, one folder per functionality, one test per scenario test_scripts/ # Unit, environment-preparation, and conformance runners config.yaml # renderer configuration ``` diff --git a/forge/skills/load-plain-reference/references/rendering-and-testing.md b/forge/skills/load-plain-reference/references/rendering-and-testing.md index 595f656..8ad5cc4 100644 --- a/forge/skills/load-plain-reference/references/rendering-and-testing.md +++ b/forge/skills/load-plain-reference/references/rendering-and-testing.md @@ -6,8 +6,10 @@ Functional specs render from top to bottom. When rendering one functional spec, the previous specs but not future specs. A required module's functional specs also count as previous specs according to the module rules. -Each functional spec receives its own conformance-test folder. After a new spec renders, the -renderer runs conformance tests for previous specs to detect regressions. A new failure can mean: +The renderer reads the test plain files under `tests/` and generates exactly the scenarios they hold, +one labelled test per scenario-id; it never adds a test of its own. Each functionality receives its own +conformance-test folder, named by its func-id. After a new spec renders, the renderer runs conformance +tests for previous specs to detect regressions. A new failure can mean: 1. The generated implementation does not satisfy the specification. 2. The generated conformance test does not accurately test the specification. @@ -24,7 +26,7 @@ and debugged, but never edited directly. Apply fixes as follows: - Behavior or implementation/unit-test guidance: edit definitions, functional specs, or implementation requirements as appropriate. - Conformance-test guidance: edit test requirements. -- Acceptance-test guidance: edit the acceptance tests nested under their owning functional spec. +- Scenario guidance: edit the scenarios in the functionality's test plain file with `add-test-scenarios`. Re-render after correcting the source specification. @@ -37,7 +39,7 @@ Test scripts are renderer entry points as well as developer utilities: - `prepare_environment_` optionally prepares a reusable system-temporary environment for conformance testing once per render. - `run_conformance_tests_` receives the generated build folder and one functionality's - conformance-test folder (`plain_modules//tests/`). It may run repeatedly during a render. + conformance-test folder (`plain_modules//tests/`). It may run repeatedly during a render. Invoke the corresponding `implement-*-script` skill when creating or changing a test script. Those skills own exact staging, cleanup, dependency-installation, shell, and exit-code contracts. @@ -50,7 +52,7 @@ Run project scripts from the repository root, using the filenames configured for ./test_scripts/run_unittests.sh plain_modules//code ./test_scripts/prepare_environment.sh plain_modules//code ./test_scripts/run_conformance_tests.sh \ - plain_modules//code plain_modules//tests/ + plain_modules//code plain_modules//tests/ ``` The renderer resolves script arguments to absolute paths. Test scripts must treat input directories diff --git a/forge/skills/migrate-plain-syntax/SKILL.md b/forge/skills/migrate-plain-syntax/SKILL.md new file mode 100644 index 0000000..09f2f61 --- /dev/null +++ b/forge/skills/migrate-plain-syntax/SKILL.md @@ -0,0 +1,159 @@ +--- +name: migrate-plain-syntax +description: >- + Convert a ***plain project from the old syntax to the current one: give + every functional spec a # func-id heading, move nested ***acceptance tests*** + into test plain files under tests//, draft conformance scenarios + with the user for every functionality. Use when a skill or plain-parser + reports the old syntax, or when the user asks to migrate or upgrade old + .plain files. +--- + +# Migrate Plain Syntax + +Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. This skill needs `module-structure.md`, `func-specs.md`, `semantic-ids.md`, `test-plain-files.md`, `requires-modules.md`, and `import-modules.md`. + +## What changes + +| Old syntax | Current syntax | +|---|---| +| `***acceptance tests***` nested under a functional spec | `***acceptance tests***` section of the functionality's test plain file `tests//.plain` | +| A functional spec is a top-level `- ` bullet with nested ` - ` sub-bullets | A functional spec is a `# ` heading, a statement line, and indented `- ` clarification bullets | +| The renderer invents conformance tests | `***conformance tests***` section of the test plain file holds at least one scenario per functionality | + +There is no backward compatibility: a project in the old syntax does not render, and no other skill edits it. The migration converts the whole project in one pass. + +## Scope + +- Every `.plain` file of the project: the root modules, the import modules in the template directory, and every module reachable through `requires` and `import`. A half-migrated project fails the parser. +- Spec text is copied verbatim; only its shape changes. Rewording, trimming, and splitting are separate work for `add-feature`, `break-down-func-spec`, or `resolve-section-ownership` afterwards. +- Generated output under `plain_modules/` is never edited. Nothing is rendered. + +## Workflow + +### 1. Inventory and confirm + +1. List every `.plain` file in the project. Follow the `requires` and `import` chains from each root module so no module is missed. +2. For each file, record which signs of the old syntax it holds: nested `***acceptance tests***` sections, a `***functional specs***` section whose specs are `- ` bullets with no `# ` headings. +3. Read every `config.yaml` to learn whether conformance tests are enabled for each module (a `conformance-tests-script` entry). +4. Show the user the inventory (files, signs of the old syntax, conformance on or off per module) and get explicit confirmation before writing anything. + +### 2. Give every functionality a func-id + +For each root module, walk the `***functional specs***` section top to bottom: + +1. For every top-level `- ` bullet (a functional spec written as a bullet), propose a func-id per `semantic-ids.md`: a short slug that names the behavior (`add-task`, `export-csv`), unique in the module. Show the module's full list of proposed ids to the user in one `AskUserQuestion` and apply their edits. +2. Rewrite each functionality in place: + - The `# ` heading on its own line. + - The bullet text, without the leading `- `, as the statement line. A bare continuation line of the bullet is de-indented and kept as part of the statement. + - Each nested ` - ` sub-bullet kept as it is: it is already a clarification indented by two spaces, and deeper nesting keeps its indentation. + - One blank line between functionalities. +3. Keep comment lines (`>`) and Liquid tags where they are. +4. Cut each nested `***acceptance tests***` block out of the core spec and keep its bullets for step 3, together with the func-id they belonged to. + +### 3. Move the acceptance tests into test plain files + +For every functionality that had nested acceptance tests, create `tests//.plain` with the frontmatter keys `module:` (the core spec's file name without `.plain`) and `func_id:` and both sections, holding one scenario per old acceptance test, sorted by its content: a test of the expected use (valid input, the main path) goes under `***conformance tests***`; a test of use that is not expected (invalid input, wrong or missing authentication, missing data) goes under `***acceptance tests***`: + +- A `# ` heading per test, with a slug derived from the test text, unique in the file. +- The old test text as the scenario body, verbatim: the bullet text becomes the statement line and its nested sub-bullets keep their indentation. Free text is valid scenario text. + +Show each created file to the user as a snippet, with the split, and apply their edits before moving on. + +### 4. Draft conformance scenarios + +When conformance tests are enabled for a module, every functionality needs at least one conformance scenario. Walk the module's functionalities in order and invoke `add-test-scenarios` for each one; it creates the test plain file when step 3 did not, drafts the scenarios the spec still lacks in both sections, and reviews them with the user one functionality at a time. + +When `plain_modules//tests/` holds conformance tests from an earlier render, read the folder of the functionality first: each generated test is a candidate scenario the user can keep, edit, or drop, which is faster than drafting from the spec alone. + +When conformance tests are not enabled for a module, skip this step and say so; the test plain files created in step 3 keep an empty `***conformance tests***` section. + +### 5. Verify + +1. No `***functional specs***` section holds a `- ` bullet before its first `# ` heading; every functionality starts with a `# ` heading. +2. No core spec holds an `***acceptance tests***` section. +3. Func-ids are unique per module; scenario-ids are unique per functionality. +4. Every test plain file binds to an existing functionality; when conformance tests are enabled, every functionality has at least one conformance scenario. +5. The number of functionalities per module equals the number of top-level bullets before the migration, and every old acceptance test appears once as a scenario, in the section its content decides. +6. Run `plain-healthcheck`. Until the plain-parser release that parses the current syntax is installed, its parser step reports that it is suspended; the other checks still run. + +### 6. Report + +Per module: functionalities migrated, acceptance tests moved, test plain files created, conformance scenarios drafted, and anything the user still has to decide. Remind the user that the project renders only with a renderer that reads test plain files. + +## Rules + +- One pass over the whole project; never leave a module in the old syntax. +- Spec text verbatim; the shape changes, the words do not. +- The order of functionalities is unchanged; func-ids carry no order. +- Func-ids are proposed by this skill and approved by the user before they are written. +- Old acceptance tests are sorted by content: expected use becomes a conformance scenario, the rest become acceptance scenarios, and the user confirms the split; `add-test-scenarios` then drafts the scenarios the spec still lacks. +- Nothing under `plain_modules/` is edited. + +## Format + +Before: + +```plain +--- +requires: + - core +import: + - stack +--- + +***functional specs*** + +- A :User: can add a :Task:. Only valid :Task: items are added. + - The name must be at least 3 characters long. + + ***acceptance tests*** + - Adding a :Task: named "Buy milk" to an empty :TaskList: shows one :Task:. + - Adding a :Task: named "Go" is rejected and :TaskList: stays empty. +``` + +After, the core spec: + +```plain +--- +requires: + - core +import: + - base +--- + +***functional specs*** + +# add-task +A :User: can add a :Task:. Only valid :Task: items are added. + - The name must be at least 3 characters long. +``` + +After, `tests//add-task.plain`, with the two old acceptance tests sorted by content: + +```plain +--- +module: +func_id: add-task +--- + +***conformance tests*** + +# add-to-empty-list +Adding a :Task: named "Buy milk" to an empty :TaskList: shows one :Task:. + +***acceptance tests*** + +# reject-short-name +Adding a :Task: named "Go" is rejected and :TaskList: stays empty. +``` + +## Validation Checklist + +- [ ] Every `.plain` file of the project was inventoried and the user confirmed the pass +- [ ] Every functionality has a `# ` heading approved by the user; text verbatim; order unchanged +- [ ] No `***acceptance tests***` section remains in a core spec; every old acceptance test is a scenario in a test plain file, in the section its content decides, confirmed by the user +- [ ] Every functionality has at least one conformance scenario when conformance tests are enabled for its module +- [ ] Func-ids unique per module; scenario-ids unique per functionality +- [ ] `plain-healthcheck` run; its report shown to the user +- [ ] Nothing under `plain_modules/` was edited diff --git a/forge/skills/plain-healthcheck/SKILL.md b/forge/skills/plain-healthcheck/SKILL.md index 7336c16..63df5fa 100644 --- a/forge/skills/plain-healthcheck/SKILL.md +++ b/forge/skills/plain-healthcheck/SKILL.md @@ -3,8 +3,8 @@ name: plain-healthcheck description: >- Verification gate for a ***plain project. Verifies that every `config.yaml` exists and points at scripts that actually live in `test_scripts/`, parses - every top module with `plain-parser check`, and checks every module for what - the parser can't judge (section ownership, bullets, linked resources). Run this whenever anything in the project is finalized — + every top module with `plain-parser check`, and checks every module and test plain file for + what the parser can't judge (section ownership, bullets, scenarios, linked resources). Run this whenever anything in the project is finalized — including (but not limited to) the end of `forge-plain`, the end of `add-feature`, after `debug-specs`, after any single-skill edit that finalizes a concept, functional spec, requirement, template, or config — @@ -22,7 +22,7 @@ Run this skill **whenever anything in the ***plain project is finalized** and th - **End of `forge-plain`** (Phase 4) — before handing off the render target. - **End of `add-feature`** (Phase 3 final review) — before declaring the feature done. - **End of `debug-specs`** — after applying a fix, before telling the user to re-render. -- **After finalizing any single edit** that changes the renderable surface — e.g. after `add-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-acceptance-test`, `add-template`, `add-resource`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, `refactor-module`, `create-import-module`, `create-requires-module`, or any of the `implement-*-testing-script` skills. +- **After finalizing any single edit** that changes the renderable surface — e.g. after `add-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-requirement`, `add-test-scenarios`, `add-template`, `add-resource`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, `refactor-module`, `create-import-module`, `create-requires-module`, or any of the `implement-*-testing-script` skills. - **After hand-editing** a `.plain` file, a `config.yaml`, or anything under `test_scripts/`. - **On demand** — whenever the user asks whether the project is in a renderable state. @@ -40,9 +40,10 @@ The skill is a **detect → fix → re-run** loop. It does not stop at the first 2. Identify **top modules** — every module that is not `requires`-ed by any other module. A single-stack project has one top module; a multi-part project (e.g. backend + frontend) has one top module per part. 3. List every `config.yaml` in the repo (root and per-part directories such as `backend/`, `frontend/`). 4. List every script under `test_scripts/`. -5. Pair each top module with the `config.yaml` that governs it. The pairing rule is: the config file in the same directory as the top module wins; failing that, the repo-root `config.yaml`. A multi-part project must have one config per part — record any top module that has no governing config as a failure. +5. List every test plain file: each `.plain` file under `tests/` (and under any other folder the user names) whose frontmatter has `module:` and `func_id:`. Record the pair it binds to. +6. Pair each top module with the `config.yaml` that governs it. The pairing rule is: the config file in the same directory as the top module wins; failing that, the repo-root `config.yaml`. A multi-part project must have one config per part — record any top module that has no governing config as a failure. -Print a one-line inventory summary so the rest of the run is easy to follow, e.g. `Top modules: backend/api.plain (config: backend/config.yaml), frontend/web.plain (config: frontend/config.yaml). Scripts in test_scripts/: 4.` +Print a one-line inventory summary so the rest of the run is easy to follow, e.g. `Top modules: backend/api.plain (config: backend/config.yaml), frontend/web.plain (config: frontend/config.yaml). Scripts in test_scripts/: 4. Test plain files: 12.` ### Step 2 — Validate every `config.yaml` @@ -77,24 +78,40 @@ Step 3 has two halves: a mechanical parse by `plain-parser`, then the checks tha [`plain-parser`](https://pypi.org/project/plain-parser/) is the ***plain parser. It parses a module together with its whole `import` / `requires` chain, so running it on every top module covers every module in the project. 1. **Check it is installed.** Run `plain-parser --help`. If the command is not found, look for it where `plain-forge install` puts it before giving up: `~/.local/bin/plain-parser` (`%USERPROFILE%\.local\bin\plain-parser.exe` on Windows), or the directory printed by `uv tool dir --bin`. If it is there, run it by that full path for the rest of this step and tell the user to add the directory to `PATH`. If it is nowhere, stop and report `FAIL` with the install command (`pipx install plain-parser` or `uv tool install plain-parser`; it needs Python 3.11 or newer). Do **not** fall back to checking these rules by reading the files — without the parser there is no syntax gate. -2. **Run it from the top module's own directory**, once per top module: +2. **Check that it parses the current syntax.** Write this probe module as `probe.plain` into a fresh temporary directory and run `plain-parser check probe.plain` there: + + ```plain + ***implementation reqs*** + + - :Implementation: is written in Python 3.12. + + ***functional specs*** + + # probe + :Implementation: starts. + ``` + + If it prints `OK`, continue. If it reports an error, the installed plain-parser predates the current syntax (`# func-id` headings, test plain files). Then record one failure against the project — `plain-parser does not parse the current ***plain syntax; install the release that does and run plain-healthcheck again` — skip steps 3 and 4 of 3a, still run 3b, and report `FAIL`. Never rewrite the specs into an older syntax to satisfy the old parser. +3. **Run it from the top module's own directory**, once per top module: ```bash cd && plain-parser check .plain ``` Add `--config-name ` when the governing config is not named `config.yaml`. `plain-parser` reads `template-dir` from the config next to the `.plain` file or in the working directory, and exits `2` when it finds one in both — running from the module's own directory avoids that in multi-part projects. -3. **Read the result.** Exit `0` prints `: OK`. Exit `1` prints `Error: …` on stderr — record that message verbatim as a failure against the top module. Exit `2` is a usage or config error (ambiguous or malformed config) — record it against the config. +4. **Read the result.** Exit `0` prints `: OK`. Exit `1` prints `Error: …` on stderr — record that message verbatim as a failure against the top module. Exit `2` is a usage or config error (ambiguous or malformed config) — record it against the config. -`plain-parser` checks, among other things: frontmatter, that every `import` / `requires` entry exists and the graph has no cycles, that `requires` modules have one fixed order, that imported modules have no functional specs, section headings (allowed, not duplicated, `***definitions***` first, `***acceptance tests***` nested under a functional spec), concepts (defined, exported and required correctly), that a module with functional specs has implementation reqs, and that every linked resource and template resolves to an existing, relative, non-`.plain` file. It does not always report every error in one run, so after fixing what it reported, run it again until it prints `OK`. +`plain-parser` checks, among other things: frontmatter, that every `import` / `requires` entry exists and the graph has no cycles, that `requires` modules have one fixed order, that imported modules have no functional specs, section headings (allowed, not duplicated, `***definitions***` first), `# func-id` headings in `***functional specs***` with unique func-ids, test plain files (frontmatter `module:` and `func_id:` naming an existing functionality, both sections present, unique scenario-ids), concepts (defined, exported and required correctly), that a module with functional specs has implementation reqs, and that every linked resource and template resolves to an existing, relative, non-`.plain` file. It does not always report every error in one run, so after fixing what it reported, run it again until it prints `OK`. #### 3b. Check what the parser can't judge Check every `.plain` module against the rules loaded by `load-plain-reference`: 1. **Section ownership.** No fact sits in a section that doesn't own it — `:UnitTests:` facts only in `***implementation reqs***`, `:ConformanceTests:` facts only in `***test reqs***`, technology only in `***implementation reqs***`. -2. **Bullets.** Every line in every section is a valid `- ` list item or a correctly indented sub-bullet — no bare continuation lines. +2. **Bullets and headings.** Every line in `***definitions***`, `***implementation reqs***`, and `***test reqs***` is a valid `- ` list item or a correctly indented sub-bullet — no bare continuation lines. Every functionality in `***functional specs***` is a `# ` heading followed by its body, with its `- ` clarifications indented by two spaces; a `- ` under a heading that is not indented is fixed by indenting it. 3. **Linked resources.** Every linked resource is a single **text** file (not a binary), and no URL or folder path appears in the surrounding spec text unless it is a runtime value the generated software itself uses. +4. **Test plain files.** Every test plain file binds to an existing functionality (`module:` names a module, `func_id:` one of its `# func-id` headings) and has both sections in order. Scenario-ids are unique per functionality across every file bound to it. When the module's `config.yaml` has a `conformance-tests-script`, every functionality has at least one conformance scenario; a functionality with none is a failure that names `add-test-scenarios`. A scenario under `***conformance tests***` that describes use that is not expected (invalid parameters, wrong or missing authentication, missing data), or one under `***acceptance tests***` that describes the expected use, is a failure that names `add-test-scenarios`. +5. **Old syntax.** An `***acceptance tests***` section in a core spec, or a `***functional specs***` section written as `- ` bullets with no `# ` headings, is a failure that names `migrate-plain-syntax`. Do not fix it inline. Functional-spec complexity is checked when a spec is authored (`add-functional-spec(s)` → `analyze-if-func-spec-too-complex`). Re-run `analyze-if-func-spec-too-complex` here only for a spec that was hand-edited since it was authored. @@ -105,7 +122,7 @@ Treat 3a and 3b together as a hard gate: the healthcheck **only passes** when `p Iterate until it passes: 1. Identify the offending `.plain` file, the line, and the kind of issue: missing or duplicated concept, cyclic definition, broken `import`/`requires`, misplaced fact, bare continuation line, missing resource or template, etc. -2. Fix only the `.plain` files (or the relevant `config.yaml` / template) using the appropriate edit skill — `add-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `resolve-section-ownership`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, or an inline edit. **Never** modify generated code under `plain_modules//code/` or `plain_modules//tests/`. +2. Fix only the `.plain` files (or the relevant `config.yaml` / template) using the appropriate edit skill — `add-concept`, `add-functional-spec`, `add-functional-specs`, `add-implementation-requirement`, `add-test-scenarios`, `resolve-section-ownership`, `resolve-spec-conflict`, `break-down-func-spec`, `consolidate-concepts`, or an inline edit — or `migrate-plain-syntax` for a file in the old syntax. **Never** modify generated code under `plain_modules//code/` or `plain_modules//tests/`. 3. If you are uncertain about ***plain syntax for the failing construct, re-load `load-plain-reference` before fixing. 4. Re-run the failed checks — for a `plain-parser` failure, re-run `plain-parser check`. Repeat until they pass. @@ -115,7 +132,7 @@ If the failure is something the healthcheck cannot reasonably fix on its own (e. Emit one of: -- **`PASS`** — followed by a short summary of what was checked: `N config.yaml(s) validated`, `M module(s) checked (plain-parser OK for every top module)`, `K scripts referenced`. The caller (`forge-plain`, `add-feature`, `debug-specs`) can then continue to its hand-off step. +- **`PASS`** — followed by a short summary of what was checked: `N config.yaml(s) validated`, `M module(s) checked (plain-parser OK for every top module)`, `T test plain file(s) bound`, `K scripts referenced`. The caller (`forge-plain`, `add-feature`, `debug-specs`) can then continue to its hand-off step. - **`FAIL`** — followed by a numbered list of every unresolved problem. Each entry must include: the file (config / `.plain` / script) it applies to, the concrete issue, and what the user needs to decide if the healthcheck couldn't resolve it. The verdict goes on the first line so callers can pattern-match without parsing the whole report. @@ -136,6 +153,8 @@ The verdict goes on the first line so callers can pattern-match without parsing - [ ] No `config.yaml` mixes stacks (e.g. Python + JS scripts in the same file) - [ ] No `config.yaml` declares `prepare-environment-script` without also declaring `conformance-tests-script` - [ ] Every `test_scripts/*` file is referenced by some `config.yaml` (or surfaced as a warning) +- [ ] The parser probe passed, or the report says 3a is suspended and names the plain-parser release that is needed - [ ] `plain-parser check` printed `OK` for every top module, run from that module's directory -- [ ] Every module passes every check in Step 3b (section ownership, bullets, linked resources) +- [ ] Every module passes every check in Step 3b (section ownership, bullets and headings, linked resources, test plain files, old syntax) +- [ ] Every test plain file binds to an existing functionality, and every functionality has at least one conformance scenario when conformance tests are enabled - [ ] Verdict (`PASS` / `FAIL`) is on the first line, with a numbered list of remaining problems if it failed diff --git a/forge/skills/refactor-module/SKILL.md b/forge/skills/refactor-module/SKILL.md index ab36fa6..a260f6f 100644 --- a/forge/skills/refactor-module/SKILL.md +++ b/forge/skills/refactor-module/SKILL.md @@ -11,6 +11,8 @@ description: >- Always use the skill `load-plain-reference` to retrieve the ***plain syntax rules — but only if you haven't done so yet. +If a target `.plain` file is in the old syntax (see *Old syntax* in `module-structure.md`), stop, name the file, and point the user to `migrate-plain-syntax`; do not edit it. + ## When to Use - A module has many functional specs spanning multiple distinct domains or feature areas. @@ -29,7 +31,7 @@ The `.plain` file to refactor, plus guidance from the user on the desired groupi ## Phase 1 — Analyze the Current Module 1. **Read the entire `.plain` file** — frontmatter, definitions, implementation reqs, test reqs, and all functional specs. Also read any `import` and `requires` chains to understand the full context. -2. **Inventory the functional specs.** Number each spec and note which `:Concepts:` it references. Build a dependency map: which specs depend on earlier specs (by referencing behavior or state they introduced)? +2. **Inventory the functional specs.** List each functionality by its func-id and note which `:Concepts:` it references. Build a dependency map: which specs depend on earlier specs (by referencing behavior or state they introduced)? 3. **Inventory the definitions.** For each concept, note which functional specs reference it. Identify concepts that are used across multiple logical groups vs. concepts used only within one group. 4. **Identify logical groups.** Look for natural seams — clusters of functional specs that share concepts, represent a cohesive domain, or describe a single feature area. Common groupings: - Data model setup and CRUD operations @@ -50,7 +52,7 @@ The modules form a `requires` chain that preserves the original chronological or ``` original-module ↓ splits into -module-group-a (no requires — this is the new base) +module-group-a (no requires — this is the chain root) module-group-b (requires: module-group-a) module-group-c (requires: module-group-b) ``` @@ -86,7 +88,7 @@ If many concepts are shared across all modules, consider creating a new import m Summarize for the user: - Number of resulting modules and their names -- Which functional specs go in each module (listed by number or short description) +- Which functional specs go in each module (listed by func-id) - The `requires` chain order - Any new import module being created - Which concepts are exported from each module @@ -96,20 +98,20 @@ Get explicit user confirmation before proceeding. ## Phase 3 — Execute the Split -Create the new modules in order, from the base of the chain upward. +Create the new modules in order, from the root of the chain upward. ### 3a. Create or update the import template (if needed) If shared definitions or reqs are being moved to a new import template, create it first using the `create-import-module` skill. -### 3b. Create the base module +### 3b. Create the chain root Create the first module in the chain: 1. Set the YAML frontmatter — inherit the original module's `import` references. Add `exported_concepts` for any concepts that downstream modules need. 2. Add the `***definitions***` section with concepts used by this module's specs and concepts that need to be exported. 3. Add `***implementation reqs***` and `***test reqs***` specific to this module (if not fully covered by the import template). 4. Add the `***functional specs***` assigned to this module, preserving their original order and exact wording. -5. Carry over any `***acceptance tests***` nested under the moved functional specs. +5. Move the test plain files of the moved functionalities to `tests//` and set their `module:` key to the new module name; func-ids and scenario text stay unchanged. ### 3c. Create subsequent modules @@ -119,7 +121,7 @@ For each subsequent module in the chain: 3. Add `***definitions***` for concepts local to this module. Do **not** redefine concepts already available via `requires` (exported) or `import`. 4. Add `***implementation reqs***` and `***test reqs***` specific to this module. 5. Add the `***functional specs***` assigned to this module, preserving their original order and exact wording. -6. Carry over any `***acceptance tests***`. +6. Move the test plain files of the moved functionalities the same way. ### 3d. Handle the original module file @@ -135,12 +137,12 @@ This is the most critical verification. Walk through every functional spec from Checklist: - [ ] Total spec count across all new modules equals the original spec count -- [ ] Each spec's text is identical to the original (no rewording) -- [ ] Each spec's acceptance tests (if any) are carried over intact +- [ ] Each spec's text is identical to the original (no rewording) and keeps its func-id +- [ ] Each functionality's test plain file moved with it, with `module:` updated and scenarios intact ### 4b. Chronological ordering check -Verify that the chronological order of all functional specs — when read across the requires chain from base to leaf — matches the original ordering exactly. A spec must not appear before a spec it depends on. +Verify that the chronological order of all functional specs — when read across the requires chain from root to leaf — matches the original ordering exactly. A spec must not appear before a spec it depends on. ### 4c. Concept availability check @@ -169,7 +171,7 @@ Read each new `.plain` file in full and present the final structure to the user Creating many tiny modules with 1–2 specs each adds overhead without clarity. Aim for modules with 3–10 functional specs each, grouped around a cohesive theme. ### Breaking chronological order -Specs that set up foundational behavior (entry point, database init, core CRUD) must stay in the base module even if they touch concepts from multiple domains. Only split when specs are cleanly separable. +Specs that set up foundational behavior (entry point, database init, core CRUD) must stay in the chain root even if they touch concepts from multiple domains. Only split when specs are cleanly separable. ### Forgetting exported_concepts If a concept defined in module A is referenced by a spec in module B (which requires A), the concept must be in A's `exported_concepts`. Missing exports cause rendering failures. @@ -177,21 +179,21 @@ If a concept defined in module A is referenced by a spec in module B (which requ ### Modifying spec text during the split This is a refactoring, not a rewrite. Spec text must be copied verbatim. If a spec needs rewording to work in its new module context, that is a sign the split boundary is wrong — adjust the grouping instead. -### Losing acceptance tests -Acceptance tests are nested under their parent functional spec. When moving a spec to a new module, its acceptance tests must move with it. +### Losing test plain files +A test plain file binds to its functionality by `module:` and `func_id:`. When a functionality moves to a new module, its test plain file moves to `tests//` and its `module:` key changes; otherwise the renderer reports a pair that matches no functionality. ## Validation Checklist - [ ] User confirmed the split plan before execution - [ ] All original functional specs are present in exactly one new module — none lost - [ ] Spec text is identical to the original — no rewording -- [ ] Acceptance tests moved with their parent specs +- [ ] Test plain files moved with their functionalities and rebound to the new module - [ ] Chronological order preserved across the requires chain - [ ] Every referenced `:Concept:` is available in each module (local, import, or exported) - [ ] `exported_concepts` are correctly set on each module - [ ] No concept name collisions - [ ] Each module has at least one functional spec and one implementation req -- [ ] `requires` chain is correctly ordered (base → leaf) +- [ ] `requires` chain is correctly ordered (root → leaf) - [ ] Original module removed or updated - [ ] Downstream modules (if any) updated to require the chain tip, plus any earlier chain modules whose exported concepts they reference - [ ] User approved the final result diff --git a/forge/skills/resolve-section-ownership/SKILL.md b/forge/skills/resolve-section-ownership/SKILL.md index a2b4495..d6b0209 100644 --- a/forge/skills/resolve-section-ownership/SKILL.md +++ b/forge/skills/resolve-section-ownership/SKILL.md @@ -36,7 +36,7 @@ The bullet, section, or `.plain` file to resolve. 4. **Route each fact** by the *Section ownership* table in `module-structure.md`. Where a fact resists the table, apply the question it encodes: **would a caller of the finished software observe this?** Yes → `***functional specs***`. No, it is how the result is reached → `***implementation reqs***`. It says what a term means → `***definitions***`. It shapes `:ConformanceTests:` → `***test reqs***`. 5. **Split a fact that carries both a mechanism and its consequence.** The consequence is observable and becomes the functional spec; the mechanism becomes the implementation req. *"Sends the same idempotency key on every retry, so a retried write creates at most one record"* is two facts, not one. 6. **Give each moved fact a subject that exists in the built system.** A fact leaving a functional spec often loses its subject: name the component that owns the behavior, and add it to `***definitions***` if it is not defined yet. `:Implementation:` is the whole codebase — use it only for a fact that holds everywhere. -7. **Author each fact into its owning section** with the right edit skill — `add-concept`, `add-implementation-requirement`, `add-test-requirement`, `add-acceptance-test`. **Never hand-author `***functional specs***`**: a fact moving *into* that section goes through `add-functional-spec`, which runs the complexity and conflict analyzers for you. +7. **Author each fact into its owning section** with the right edit skill — `add-concept`, `add-implementation-requirement`, `add-test-requirement`, `add-test-scenarios`. **Never hand-author `***functional specs***`**: a fact moving *into* that section goes through `add-functional-spec`, which runs the complexity and conflict analyzers for you. 8. **Remove each fact from where it no longer belongs.** If a bullet is left stating nothing observable, delete the bullet; do not leave a stub. Deleting the last bullet of a section deletes the section. 9. **Verify nothing was lost and nothing was duplicated.** Walk every fact from the original text and confirm it appears in exactly one section. A fact that now reads as obvious in its new home is still a cut decision — make it deliberately, per `concise-specs.md`. 10. **Run `plain-healthcheck`.** Moving facts changes the renderable surface; do not finish on a `FAIL`. @@ -48,7 +48,7 @@ The bullet, section, or `.plain` file to resolve. - **`add-functional-spec` reports a conflict** → resolve it with `resolve-spec-conflict` **before** removing the fact from its old section, so no version of it is lost in between. - **A fact appears to belong to two sections** → it is two facts; split it per step 5. If it still resists, it is a mechanism whose consequence has not been written down — write the consequence, and the split follows. - **The owning section does not exist in the file** → create it in canonical order. An import module carries no `***functional specs***` at all (see `import-modules.md`), so an observable fact found there belongs on the root module that imports it. -- **The move empties a functional spec** → delete the spec rather than leaving a placeholder, and check whether any acceptance test nested under it must move or go with it. +- **The move empties a functional spec** → delete the spec rather than leaving a placeholder, and move or delete its test plain file with `add-test-scenarios`. ## Worked examples diff --git a/forge/skills/resolve-spec-conflict/SKILL.md b/forge/skills/resolve-spec-conflict/SKILL.md index ec8cb96..aa64d49 100644 --- a/forge/skills/resolve-spec-conflict/SKILL.md +++ b/forge/skills/resolve-spec-conflict/SKILL.md @@ -31,7 +31,7 @@ There are three possible outcomes — determine which one applies **before** mak | Outcome | Symptom | Fix | |---------|---------|-----| | **Implementation is incorrect** | Generated code doesn't match the spec's intent. The specs themselves are fine. | Clarify the ambiguous spec so the renderer interprets it correctly. Re-render. | -| **Conformance tests are incorrect** | The tests don't accurately verify the spec. The implementation is actually correct. | Adjust `***test reqs***` or `***acceptance tests***` to guide better test generation. Re-render. | +| **Conformance tests are incorrect** | The tests don't accurately verify the spec. The implementation is actually correct. | Adjust `***test reqs***`, or the scenarios in the test plain file via `add-test-scenarios`, to guide better test generation. Re-render. | | **Requirements truly conflict** | The two specs are inherently contradictory — no implementation can satisfy both. | One or both specs must be revised. See Step 3. | **How to diagnose:** @@ -49,25 +49,29 @@ Before (ambiguous): ```plain ***functional specs*** -- All :Resource: items are returned. +# list-resources +All :Resource: items are returned. -- Only active :Resource: items are returned. +# list-active-resources +Only active :Resource: items are returned. ``` After (disambiguated): ```plain ***functional specs*** -- All :Resource: items are returned when no filter is specified. +# list-resources +All :Resource: items are returned when no filter is specified. -- When the "active" filter is specified, only active :Resource: items are returned. +# list-active-resources +When the "active" filter is specified, only active :Resource: items are returned. ``` **Strategy B: Revise the newer spec.** If the new spec introduced the conflict, rewrite it to be compatible with the established behavior from earlier specs. **Strategy C: Revise the older spec.** If the older spec was under-specified and the new requirement reveals a better design, update the older spec. This is more disruptive — all conformance tests for that spec and everything after it may need re-rendering. -**Strategy D: Merge into one spec.** If the two specs describe overlapping behavior, combine them into a single, clear spec. Remove the redundant one. Be mindful of the 200 LOC limit. +**Strategy D: Merge into one spec.** If the two specs describe overlapping behavior, combine them into a single, clear spec. Remove the redundant one and its test plain file, moving any still valid scenario into the kept functionality's file. Be mindful of the 200 LOC limit. ### Step 4: Validate the Resolution @@ -75,7 +79,8 @@ After editing: 1. Re-read the full `***functional specs***` section to confirm no new conflicts were introduced. 2. Check that any revised spec still respects the 200 LOC limit. 3. Check that chronological ordering still makes sense (earlier specs should not depend on later ones). -4. If resolving via test adjustments, verify the `***test reqs***` or `***acceptance tests***` changes are appropriate. +4. If resolving via test adjustments, verify the `***test reqs***` or test plain file changes are appropriate. +5. For every revised functionality, invoke `add-test-scenarios` to bring its test plain file in step with the new text. ## Prevention (for use during add-functional-spec / add-functional-specs) @@ -95,3 +100,4 @@ Before adding a new spec, run through this quick conflict check: - [ ] No new conflicts introduced by the fix - [ ] Chronological ordering preserved - [ ] All referenced `:Concepts:` are still defined +- [ ] Test plain files of the revised functionalities brought in step via `add-test-scenarios`